Skip to content

Commit da8f1d8

Browse files
committed
Merge branch 'master' into feat/update-tool-desc
# Conflicts: # src/tools/actor.ts
2 parents 24c5e0d + cdffc3e commit da8f1d8

File tree

15 files changed

+323
-97
lines changed

15 files changed

+323
-97
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,9 @@ storage/key_value_stores/default/*
1818
# Added by Apify CLI
1919
.venv
2020
.env
21+
22+
# Aider coding agent files
2123
.aider*
24+
25+
# Ignore MCP config for Opencode client
26+
opencode.json

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,27 @@
22

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

5+
## [0.4.14](https://github.com/apify/apify-mcp-server/releases/tag/v0.4.14) (2025-09-19)
6+
7+
8+
## [0.4.13](https://github.com/apify/apify-mcp-server/releases/tag/v0.4.13) (2025-09-19)
9+
10+
### 🚀 Features
11+
12+
- 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)
13+
14+
15+
## [0.4.12](https://github.com/apify/apify-mcp-server/releases/tag/v0.4.12) (2025-09-18)
16+
17+
### 🚀 Features
18+
19+
- 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)
20+
21+
### 🐛 Bug Fixes
22+
23+
- 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)
24+
25+
526
## [0.4.10](https://github.com/apify/apify-mcp-server/releases/tag/v0.4.10) (2025-09-15)
627

728
### 🚀 Features

manifest.json

Lines changed: 6 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.10",
4+
"version": "0.4.14",
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,11 @@
2424
"url": "https://github.com/apify/apify-mcp-server"
2525
},
2626
"license": "MIT",
27+
"privacy_policies": [
28+
"https://docs.apify.com/legal/privacy-policy",
29+
"https://docs.apify.com/legal/gdpr-information",
30+
"https://docs.apify.com/legal"
31+
],
2732
"repository": {
2833
"type": "git",
2934
"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.10",
3+
"version": "0.4.14",
44
"type": "module",
55
"description": "Apify MCP Server",
66
"engines": {
@@ -37,7 +37,7 @@
3737
"dependencies": {
3838
"@apify/datastructures": "^2.0.3",
3939
"@apify/log": "^2.5.16",
40-
"@modelcontextprotocol/sdk": "^1.17.4",
40+
"@modelcontextprotocol/sdk": "^1.18.1",
4141
"@types/cheerio": "^0.22.35",
4242
"@types/turndown": "^5.0.5",
4343
"ajv": "^8.17.1",

src/const.ts

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

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

src/errors.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export class TimeoutError extends Error {
2+
override readonly name = 'TimeoutError';
3+
}

src/mcp/client.ts

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/
44

55
import log from '@apify/log';
66

7+
import { TimeoutError } from '../errors.js';
8+
import { ACTORIZED_MCP_CONNECTION_TIMEOUT_MSEC } from './const.js';
79
import { getMCPServerID } from './utils.js';
810

911
/**
@@ -12,16 +14,55 @@ import { getMCPServerID } from './utils.js';
1214
*/
1315
export async function connectMCPClient(
1416
url: string, token: string,
15-
): Promise<Client> {
17+
): Promise<Client | null> {
18+
let client: Client;
1619
try {
17-
return await createMCPStreamableClient(url, token);
18-
} catch {
20+
client = await createMCPStreamableClient(url, token);
21+
return client;
22+
} catch (error) {
23+
// If streamable HTTP transport fails on not timeout error, continue with SSE transport
24+
if (error instanceof TimeoutError) {
25+
log.warning('Connection to MCP server using streamable HTTP transport timed out', { url });
26+
return null;
27+
}
28+
1929
// If streamable HTTP transport fails, fall back to SSE transport
2030
log.debug('Streamable HTTP transport failed, falling back to SSE transport', {
2131
url,
2232
});
23-
return await createMCPSSEClient(url, token);
2433
}
34+
35+
try {
36+
client = await createMCPSSEClient(url, token);
37+
return client;
38+
} catch (error) {
39+
if (error instanceof TimeoutError) {
40+
log.warning('Connection to MCP server using SSE transport timed out', { url });
41+
return null;
42+
}
43+
44+
log.error('Failed to connect to MCP server using SSE transport', { cause: error });
45+
throw error;
46+
}
47+
}
48+
49+
async function withTimeout<T>(millis: number, promise: Promise<T>): Promise<T> {
50+
let timeoutPid: NodeJS.Timeout;
51+
const timeout = new Promise<never>((_resolve, reject) => {
52+
timeoutPid = setTimeout(
53+
() => reject(new TimeoutError(`Timed out after ${millis} ms.`)),
54+
millis,
55+
);
56+
});
57+
58+
return Promise.race([
59+
promise,
60+
timeout,
61+
]).finally(() => {
62+
if (timeoutPid) {
63+
clearTimeout(timeoutPid);
64+
}
65+
});
2566
}
2667

2768
/**
@@ -47,7 +88,7 @@ async function createMCPSSEClient(
4788
headers.set('authorization', `Bearer ${token}`);
4889
return fetch(input, { ...init, headers });
4990
},
50-
// We have to cast to "any" to use it, since it's non-standard
91+
// We have to cast to "any" to use it, since it's non-standard
5192
} as any, // eslint-disable-line @typescript-eslint/no-explicit-any
5293
});
5394

@@ -56,7 +97,7 @@ async function createMCPSSEClient(
5697
version: '1.0.0',
5798
});
5899

59-
await client.connect(transport);
100+
await withTimeout(ACTORIZED_MCP_CONNECTION_TIMEOUT_MSEC, client.connect(transport));
60101

61102
return client;
62103
}
@@ -82,7 +123,7 @@ async function createMCPStreamableClient(
82123
version: '1.0.0',
83124
});
84125

85-
await client.connect(transport);
126+
await withTimeout(ACTORIZED_MCP_CONNECTION_TIMEOUT_MSEC, client.connect(transport));
86127

87128
return client;
88129
}

src/mcp/const.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export const MAX_TOOL_NAME_LENGTH = 64;
22
export const SERVER_ID_LENGTH = 8;
33
export const EXTERNAL_TOOL_CALL_TIMEOUT_MSEC = 120_000; // 2 minutes
4+
export const ACTORIZED_MCP_CONNECTION_TIMEOUT_MSEC = 30_000; // 30 seconds
45

56
export const LOG_LEVEL_MAP: Record<string, number> = {
67
debug: 0,

src/mcp/server.ts

Lines changed: 34 additions & 21 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
});
@@ -548,9 +551,19 @@ export class ActorsMcpServer {
548551

549552
if (tool.type === 'actor-mcp') {
550553
const serverTool = tool.tool as ActorMcpTool;
551-
let client: Client | undefined;
554+
let client: Client | null = null;
552555
try {
553556
client = await connectMCPClient(serverTool.serverUrl, apifyToken);
557+
if (!client) {
558+
const msg = `Failed to connect to MCP server ${serverTool.serverUrl}`;
559+
log.error(msg);
560+
await this.server.sendLoggingMessage({ level: 'error', data: msg });
561+
return {
562+
content: [
563+
{ type: 'text', text: msg },
564+
],
565+
};
566+
}
554567

555568
// Only set up notification handlers if progressToken is provided by the client
556569
if (progressToken) {

0 commit comments

Comments
 (0)