Skip to content

Commit 2083760

Browse files
authored
Merge branch 'master' into feature/github_registry
2 parents a3f81cb + 4aeaf2d commit 2083760

File tree

12 files changed

+229
-78
lines changed

12 files changed

+229
-78
lines changed

.gitignore

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,13 @@ storage/key_value_stores/default/*
1818
# Added by Apify CLI
1919
.venv
2020
.env
21+
22+
# Aider coding agent files
2123
.aider*
2224

25+
2326
# MCP registry private key
24-
key.pem
27+
key.pem
28+
29+
# Ignore MCP config for Opencode client
30+
opencode.json

CHANGELOG.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,22 @@
22

33
All notable changes to this project will be documented in this file.
44

5-
## [0.4.11](https://github.com/apify/apify-mcp-server/releases/tag/v0.4.11) (2025-09-18)
5+
## [0.4.13](https://github.com/apify/apify-mcp-server/releases/tag/v0.4.13) (2025-09-19)
66

7+
### 🚀 Features
8+
9+
- Update sdk to 1.18.1 to fix write after end ([#279](https://github.com/apify/apify-mcp-server/pull/279)) ([559354a](https://github.com/apify/apify-mcp-server/commit/559354afe513a74a20b5a0bd3efd6f15f909248a)) by [@MQ37](https://github.com/MQ37)
10+
11+
12+
## [0.4.12](https://github.com/apify/apify-mcp-server/releases/tag/v0.4.12) (2025-09-18)
13+
14+
### 🚀 Features
15+
16+
- Call-actor add support for MCP server Actors ([#274](https://github.com/apify/apify-mcp-server/pull/274)) ([84a8f8f](https://github.com/apify/apify-mcp-server/commit/84a8f8f37aadbbf017c2cc002718a858a09b9190)) by [@MQ37](https://github.com/MQ37), closes [#247](https://github.com/apify/apify-mcp-server/issues/247)
17+
18+
### 🐛 Bug Fixes
19+
20+
- Duplicate skyfire description when listing tools multiple times ([#277](https://github.com/apify/apify-mcp-server/pull/277)) ([aecc147](https://github.com/apify/apify-mcp-server/commit/aecc147e31a01d4fbab90930fd1c5682f96274b6)) by [@MQ37](https://github.com/MQ37)
721

822
## [0.4.10](https://github.com/apify/apify-mcp-server/releases/tag/v0.4.10) (2025-09-15)
923

manifest.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"dxt_version": "0.1",
33
"name": "Apify",
4-
"version": "0.4.11",
4+
"version": "0.4.13",
55
"description": "Extract data from any website using thousands of tools from the Apify Store.",
66
"long_description": "Apify is the world's largest marketplace of tools for web scraping, data extraction, and web automation. You can extract structured data from social media, e-commerce, search engines, maps, travel sites, or any other website.",
77
"keywords": [
@@ -24,6 +24,7 @@
2424
"url": "https://github.com/apify/apify-mcp-server"
2525
},
2626
"license": "MIT",
27+
"privacy_policies": ["https://docs.apify.com/legal/privacy-policy", "https://docs.apify.com/legal/gdpr-information", "https://docs.apify.com/legal"],
2728
"repository": {
2829
"type": "git",
2930
"url": "https://github.com/apify/apify-mcp-server"

package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@apify/actors-mcp-server",
3-
"version": "0.4.11",
3+
"version": "0.4.13",
44
"type": "module",
55
"description": "Apify MCP Server",
66
"mcpName": "com.apify/apify-mcp-server",
@@ -38,7 +38,7 @@
3838
"dependencies": {
3939
"@apify/datastructures": "^2.0.3",
4040
"@apify/log": "^2.5.16",
41-
"@modelcontextprotocol/sdk": "^1.17.4",
41+
"@modelcontextprotocol/sdk": "^1.18.1",
4242
"@types/cheerio": "^0.22.35",
4343
"@types/turndown": "^5.0.5",
4444
"ajv": "^8.17.1",

src/const.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ export const APIFY_DOCS_CACHE_MAX_SIZE = 500;
7272
export const APIFY_DOCS_CACHE_TTL_SECS = 60 * 60; // 1 hour
7373
export const GET_HTML_SKELETON_CACHE_TTL_SECS = 5 * 60; // 5 minutes
7474
export const GET_HTML_SKELETON_CACHE_MAX_SIZE = 200;
75+
export const MCP_SERVER_CACHE_MAX_SIZE = 500;
76+
export const MCP_SERVER_CACHE_TTL_SECS = 30 * 60; // 30 minutes
7577

7678
export const ACTOR_PRICING_MODEL = {
7779
/** Rental Actors */

src/mcp/server.ts

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,29 @@ export class ActorsMcpServer {
262262
for (const wrap of tools) {
263263
this.tools.set(wrap.tool.name, wrap);
264264
}
265+
// Handle Skyfire mode modifications once per tool upsert
266+
if (this.options.skyfireMode) {
267+
for (const wrap of tools) {
268+
if (wrap.type === 'actor'
269+
|| (wrap.type === 'internal' && wrap.tool.name === HelperTools.ACTOR_CALL)
270+
|| (wrap.type === 'internal' && wrap.tool.name === HelperTools.ACTOR_OUTPUT_GET)) {
271+
// Add Skyfire instructions to description if not already present
272+
if (!wrap.tool.description.includes(SKYFIRE_TOOL_INSTRUCTIONS)) {
273+
wrap.tool.description += `\n\n${SKYFIRE_TOOL_INSTRUCTIONS}`;
274+
}
275+
// Add skyfire-pay-id property if not present
276+
if (wrap.tool.inputSchema && 'properties' in wrap.tool.inputSchema) {
277+
const props = wrap.tool.inputSchema.properties as Record<string, unknown>;
278+
if (!props['skyfire-pay-id']) {
279+
props['skyfire-pay-id'] = {
280+
type: 'string',
281+
description: SKYFIRE_PAY_ID_PROPERTY_DESCRIPTION,
282+
};
283+
}
284+
}
285+
}
286+
}
287+
}
265288
if (shouldNotifyToolsChangedHandler) this.notifyToolsChangedHandler();
266289
return tools;
267290
}
@@ -419,26 +442,6 @@ export class ActorsMcpServer {
419442
* @returns {object} - The response object containing the tools.
420443
*/
421444
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
422-
/**
423-
* Hack for the Skyfire agentic payments, we check if Skyfire mode is enabled we ad-hoc add
424-
* the `skyfire-pay-id` input property to all Actor tools and `call-actor` and `get-actor-output` tool.
425-
*/
426-
if (this.options.skyfireMode) {
427-
for (const toolEntry of this.tools.values()) {
428-
if (toolEntry.type === 'actor'
429-
|| (toolEntry.type === 'internal' && toolEntry.tool.name === HelperTools.ACTOR_CALL)
430-
|| (toolEntry.type === 'internal' && toolEntry.tool.name === HelperTools.ACTOR_OUTPUT_GET)) {
431-
if (toolEntry.tool.inputSchema && 'properties' in toolEntry.tool.inputSchema) {
432-
(toolEntry.tool.inputSchema.properties as Record<string, unknown>)['skyfire-pay-id'] = {
433-
type: 'string',
434-
description: SKYFIRE_PAY_ID_PROPERTY_DESCRIPTION,
435-
};
436-
}
437-
// Update description to include Skyfire instructions
438-
toolEntry.tool.description += `\n\n${SKYFIRE_TOOL_INSTRUCTIONS}`;
439-
}
440-
}
441-
}
442445
const tools = Array.from(this.tools.values()).map((tool) => getToolPublicFieldOnly(tool.tool));
443446
return { tools };
444447
});

src/state.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import {
55
APIFY_DOCS_CACHE_TTL_SECS,
66
GET_HTML_SKELETON_CACHE_MAX_SIZE,
77
GET_HTML_SKELETON_CACHE_TTL_SECS,
8+
MCP_SERVER_CACHE_MAX_SIZE,
9+
MCP_SERVER_CACHE_TTL_SECS,
810
} from './const.js';
911
import type { ActorDefinitionPruned, ApifyDocsSearchResult } from './types.js';
1012
import { TTLLRUCache } from './utils/ttl-lru.js';
@@ -15,3 +17,9 @@ export const searchApifyDocsCache = new TTLLRUCache<ApifyDocsSearchResult[]>(API
1517
export const fetchApifyDocsCache = new TTLLRUCache<string>(APIFY_DOCS_CACHE_MAX_SIZE, APIFY_DOCS_CACHE_TTL_SECS);
1618
/** Stores HTML content per URL so we can paginate the tool output */
1719
export const getHtmlSkeletonCache = new TTLLRUCache<string>(GET_HTML_SKELETON_CACHE_MAX_SIZE, GET_HTML_SKELETON_CACHE_TTL_SECS);
20+
/**
21+
* Stores MCP server resolution per actor:
22+
* - false: not an MCP server
23+
* - string: MCP server URL
24+
*/
25+
export const mcpServerCache = new TTLLRUCache<boolean | string>(MCP_SERVER_CACHE_MAX_SIZE, MCP_SERVER_CACHE_TTL_SECS);

src/tools/actor.ts

Lines changed: 100 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@ import { connectMCPClient } from '../mcp/client.js';
1818
import { getMCPServerTools } from '../mcp/proxy.js';
1919
import { actorDefinitionPrunedCache } from '../state.js';
2020
import type { ActorDefinitionStorage, ActorInfo, ApifyToken, DatasetItem, ToolEntry } from '../types.js';
21-
import { ensureOutputWithinCharLimit, getActorDefinitionStorageFieldNames } from '../utils/actor.js';
21+
import { ensureOutputWithinCharLimit, getActorDefinitionStorageFieldNames, getActorMcpUrlCached } from '../utils/actor.js';
2222
import { fetchActorDetails } from '../utils/actor-details.js';
2323
import { buildActorResponseContent } from '../utils/actor-response.js';
2424
import { ajv } from '../utils/ajv.js';
25+
import { buildMCPResponse } from '../utils/mcp.js';
2526
import type { ProgressTracker } from '../utils/progress.js';
2627
import type { JsonSchemaProperty } from '../utils/schema-generation.js';
2728
import { generateSchemaFromItems } from '../utils/schema-generation.js';
@@ -329,12 +330,19 @@ MANDATORY TWO-STEP WORKFLOW:
329330
330331
Step 1: Get Actor Info (step="info", default)
331332
• First call this tool with step="info" to get Actor details and input schema
332-
• This returns the Actor description, documentation, and required input schema
333+
• For regular Actors: returns the Actor input schema
334+
• For MCP server Actors: returns list of available tools with their schemas
333335
• You MUST do this step first - it's required to understand how to call the Actor
334336
335-
Step 2: Call Actor (step="call")
337+
Step 2: Call Actor (step="call")
336338
• Only after step 1, call again with step="call" and proper input based on the schema
337-
• This executes the Actor and returns the results
339+
• For regular Actors: executes the Actor and returns results
340+
• For MCP server Actors: use format "actor-name:tool-name" to call specific tools
341+
342+
MCP SERVER ACTORS:
343+
• For MCP server actors, step="info" lists available tools instead of input schema
344+
• To call an MCP tool, use actor name format: "actor-name:tool-name" with step="call"
345+
• Example: actor="apify/my-mcp-actor:search-tool", step="call", input={...}
338346
339347
The step parameter enforces this workflow - you cannot call an Actor without first getting its info.`,
340348
inputSchema: zodToJsonSchema(callActorArgs),
@@ -347,29 +355,66 @@ The step parameter enforces this workflow - you cannot call an Actor without fir
347355
const { args, apifyToken, progressTracker, extra, apifyMcpServer } = toolArgs;
348356
const { actor: actorName, step, input, callOptions } = callActorArgs.parse(args);
349357

358+
// Parse special format: actor:tool
359+
const mcpToolMatch = actorName.match(/^(.+):(.+)$/);
360+
let baseActorName = actorName;
361+
let mcpToolName: string | undefined;
362+
363+
if (mcpToolMatch) {
364+
baseActorName = mcpToolMatch[1];
365+
mcpToolName = mcpToolMatch[2];
366+
}
367+
368+
// For definition resolution we always use token-based client; Skyfire is only for actual Actor runs
369+
const apifyClientForDefinition = new ApifyClient({ token: apifyToken });
370+
// Resolve MCP server URL
371+
const needsMcpUrl = mcpToolName !== undefined || step === 'info';
372+
const mcpServerUrlOrFalse = needsMcpUrl ? await getActorMcpUrlCached(baseActorName, apifyClientForDefinition) : false;
373+
const isActorMcpServer = mcpServerUrlOrFalse && typeof mcpServerUrlOrFalse === 'string';
374+
375+
// Standby Actors, thus MCPs, are not supported in Skyfire mode
376+
if (isActorMcpServer && apifyMcpServer.options.skyfireMode) {
377+
return buildMCPResponse([`MCP server Actors are not supported in Skyfire mode. Please use a regular Apify token without Skyfire.`]);
378+
}
379+
350380
try {
351381
if (step === 'info') {
352-
const apifyClient = new ApifyClient({ token: apifyToken });
353-
// Step 1: Return Actor card and schema directly
354-
const details = await fetchActorDetails(apifyClient, actorName);
355-
if (!details) {
356-
return {
357-
content: [{ type: 'text', text: `Actor information for '${actorName}' was not found. Please check the Actor ID or name and ensure the Actor exists.` }],
358-
};
382+
if (isActorMcpServer) {
383+
// MCP server: list tools
384+
const mcpServerUrl = mcpServerUrlOrFalse;
385+
let client: Client | undefined;
386+
// Nested try to ensure client is closed
387+
try {
388+
client = await connectMCPClient(mcpServerUrl, apifyToken);
389+
const toolsResponse = await client.listTools();
390+
391+
const toolsInfo = toolsResponse.tools.map((tool) => `**${tool.name}**\n${tool.description || 'No description'}\nInput Schema: ${JSON.stringify(tool.inputSchema, null, 2)}`,
392+
).join('\n\n');
393+
394+
return buildMCPResponse([`This is an MCP Server Actor with the following tools:\n\n${toolsInfo}\n\nTo call a tool, use step="call" with actor name format: "${baseActorName}:{toolName}"`]);
395+
} finally {
396+
if (client) await client.close();
397+
}
398+
} else {
399+
// Regular actor: return schema
400+
const details = await fetchActorDetails(apifyClientForDefinition, baseActorName);
401+
if (!details) {
402+
return buildMCPResponse([`Actor information for '${baseActorName}' was not found. Please check the Actor ID or name and ensure the Actor exists.`]);
403+
}
404+
const content = [
405+
{ type: 'text', text: `**Input Schema:**\n${JSON.stringify(details.inputSchema, null, 0)}` },
406+
];
407+
/**
408+
* Add Skyfire instructions also in the info step since clients are most likely truncating the long tool description of the call-actor.
409+
*/
410+
if (apifyMcpServer.options.skyfireMode) {
411+
content.push({
412+
type: 'text',
413+
text: SKYFIRE_TOOL_INSTRUCTIONS,
414+
});
415+
}
416+
return { content };
359417
}
360-
const content = [
361-
{ type: 'text', text: `**Input Schema:**\n${JSON.stringify(details.inputSchema, null, 0)}` },
362-
];
363-
/**
364-
* Add Skyfire instructions also in the info step since clients are most likely truncating the long tool description of the call-actor.
365-
*/
366-
if (apifyMcpServer.options.skyfireMode) {
367-
content.push({
368-
type: 'text',
369-
text: SKYFIRE_TOOL_INSTRUCTIONS,
370-
});
371-
}
372-
return { content };
373418
}
374419

375420
/**
@@ -396,32 +441,45 @@ The step parameter enforces this workflow - you cannot call an Actor without fir
396441

397442
// Step 2: Call the Actor
398443
if (!input) {
399-
return {
400-
content: [
401-
{ type: 'text', text: `Input is required when step="call". Please provide the input parameter based on the Actor's input schema.` },
402-
],
403-
};
444+
return buildMCPResponse([`Input is required when step="call". Please provide the input parameter based on the Actor's input schema.`]);
404445
}
405446

447+
// Handle MCP tool calls
448+
if (mcpToolName) {
449+
if (!isActorMcpServer) {
450+
return buildMCPResponse([`Actor '${baseActorName}' is not an MCP server.`]);
451+
}
452+
453+
const mcpServerUrl = mcpServerUrlOrFalse;
454+
let client: Client | undefined;
455+
try {
456+
client = await connectMCPClient(mcpServerUrl, apifyToken);
457+
458+
const result = await client.callTool({
459+
name: mcpToolName,
460+
arguments: input,
461+
});
462+
463+
return { content: result.content };
464+
} finally {
465+
if (client) await client.close();
466+
}
467+
}
468+
469+
// Handle regular Actor calls
406470
const [actor] = await getActorsAsTools([actorName], apifyClient);
407471

408472
if (!actor) {
409-
return {
410-
content: [
411-
{ type: 'text', text: `Actor '${actorName}' not found.` },
412-
],
413-
};
473+
return buildMCPResponse([`Actor '${actorName}' was not found.`]);
414474
}
415475

416476
if (!actor.tool.ajvValidate(input)) {
417477
const { errors } = actor.tool.ajvValidate;
418478
if (errors && errors.length > 0) {
419-
return {
420-
content: [
421-
{ type: 'text', text: `Input validation failed for Actor '${actorName}': ${errors.map((e) => e.message).join(', ')}` },
422-
{ type: 'text', text: `Input Schema:\n${JSON.stringify(actor.tool.inputSchema)}` },
423-
],
424-
};
479+
return buildMCPResponse([
480+
`Input validation failed for Actor '${actorName}': ${errors.map((e) => e.message).join(', ')}`,
481+
`Input Schema:\n${JSON.stringify(actor.tool.inputSchema)}`,
482+
]);
425483
}
426484
}
427485

@@ -444,12 +502,8 @@ The step parameter enforces this workflow - you cannot call an Actor without fir
444502

445503
return { content };
446504
} catch (error) {
447-
log.error('Error with Actor operation', { error, actorName, step });
448-
return {
449-
content: [
450-
{ type: 'text', text: `Error with Actor operation: ${error instanceof Error ? error.message : String(error)}` },
451-
],
452-
};
505+
log.error('Failed to call Actor', { error, actorName, step });
506+
return buildMCPResponse([`Failed to call Actor '${actorName}': ${error instanceof Error ? error.message : String(error)}`]);
453507
}
454508
},
455509
},

src/utils/actor-response.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { CallActorGetDatasetResult } from '../tools/actor';
1+
import type { CallActorGetDatasetResult } from '../tools/actor.js';
22

33
/**
44
* Builds the response content for Actor tool calls.

0 commit comments

Comments
 (0)