-
Notifications
You must be signed in to change notification settings - Fork 50
fix: Actorized MCP servers have 30 seconds timeout to connect #272
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5f18d34
4591042
693de47
22f4c9d
8ad0741
2ca73dd
752021e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export class TimeoutError extends Error { | ||
override readonly name = 'TimeoutError'; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -180,12 +180,12 @@ export async function getNormalActorsAsTools( | |
Actor description: ${actorDefinitionPruned.description} | ||
Instructions: ${ACTOR_ADDITIONAL_INSTRUCTIONS}`, | ||
inputSchema: actorDefinitionPruned.input | ||
// So Actor without input schema works - MCP client expects JSON schema valid output | ||
|| { | ||
type: 'object', | ||
properties: {}, | ||
required: [], | ||
}, | ||
// So Actor without input schema works - MCP client expects JSON schema valid output | ||
|| { | ||
type: 'object', | ||
properties: {}, | ||
required: [], | ||
}, | ||
// Additional props true to allow skyfire-pay-id | ||
ajvValidate: fixedAjvCompile(ajv, { ...actorDefinitionPruned.input, additionalProperties: true }), | ||
memoryMbytes: memoryMbytes > ACTOR_MAX_MEMORY_MBYTES ? ACTOR_MAX_MEMORY_MBYTES : memoryMbytes, | ||
|
@@ -207,21 +207,22 @@ async function getMCPServersAsTools( | |
/** | ||
* This is case for the Skyfire request without any Apify token, we do not support | ||
* standby Actors in this case so we can skip MCP servers since they would fail anyway (they are standby Actors). | ||
*/ | ||
*/ | ||
if (apifyToken === null || apifyToken === undefined) { | ||
return []; | ||
} | ||
|
||
const actorsMCPServerTools: ToolEntry[] = []; | ||
for (const actorInfo of actorsInfo) { | ||
// Process all actors in parallel | ||
const actorToolPromises = actorsInfo.map(async (actorInfo) => { | ||
const actorId = actorInfo.actorDefinitionPruned.id; | ||
if (!actorInfo.webServerMcpPath) { | ||
log.warning('Actor does not have a web server MCP path, skipping', { | ||
actorFullName: actorInfo.actorDefinitionPruned.actorFullName, | ||
actorId, | ||
}); | ||
continue; | ||
return []; | ||
} | ||
|
||
const mcpServerUrl = await getActorMCPServerURL( | ||
actorInfo.actorDefinitionPruned.id, // Real ID of the Actor | ||
actorInfo.webServerMcpPath, | ||
|
@@ -232,17 +233,25 @@ async function getMCPServersAsTools( | |
mcpServerUrl, | ||
}); | ||
|
||
let client: Client | undefined; | ||
let client: Client | null = null; | ||
try { | ||
client = await connectMCPClient(mcpServerUrl, apifyToken); | ||
if (!client) { | ||
// Skip this Actor, connectMCPClient will log the error | ||
return []; | ||
} | ||
const serverTools = await getMCPServerTools(actorId, client, mcpServerUrl); | ||
actorsMCPServerTools.push(...serverTools); | ||
return serverTools; | ||
} finally { | ||
if (client) await client.close(); | ||
} | ||
} | ||
}); | ||
|
||
return actorsMCPServerTools; | ||
// Wait for all actors to be processed in parallel | ||
const actorToolsArrays = await Promise.all(actorToolPromises); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. notice: for the most use cases it should be fine, but we should be aware of rate limits There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I didn't find any rate limiting docs in the public docs on internal Notion. What type of limits do you mean? |
||
|
||
// Flatten the arrays of tools | ||
return actorToolsArrays.flat(); | ||
} | ||
|
||
export async function getActorsAsTools( | ||
|
@@ -382,10 +391,13 @@ The step parameter enforces this workflow - you cannot call an Actor without fir | |
if (isActorMcpServer) { | ||
// MCP server: list tools | ||
const mcpServerUrl = mcpServerUrlOrFalse; | ||
let client: Client | undefined; | ||
let client: Client | null = null; | ||
// Nested try to ensure client is closed | ||
try { | ||
client = await connectMCPClient(mcpServerUrl, apifyToken); | ||
if (!client) { | ||
return buildMCPResponse([`Failed to connect to MCP server ${mcpServerUrl}`]); | ||
} | ||
const toolsResponse = await client.listTools(); | ||
|
||
const toolsInfo = toolsResponse.tools.map((tool) => `**${tool.name}**\n${tool.description || 'No description'}\nInput Schema: ${JSON.stringify(tool.inputSchema, null, 2)}`, | ||
|
@@ -451,9 +463,12 @@ The step parameter enforces this workflow - you cannot call an Actor without fir | |
} | ||
|
||
const mcpServerUrl = mcpServerUrlOrFalse; | ||
let client: Client | undefined; | ||
let client: Client | null = null; | ||
try { | ||
client = await connectMCPClient(mcpServerUrl, apifyToken); | ||
if (!client) { | ||
return buildMCPResponse([`Failed to connect to MCP server ${mcpServerUrl}`]); | ||
} | ||
|
||
const result = await client.callTool({ | ||
name: mcpToolName, | ||
|
@@ -495,7 +510,7 @@ The step parameter enforces this workflow - you cannot call an Actor without fir | |
if (!callResult) { | ||
// Receivers of cancellation notifications SHOULD NOT send a response for the cancelled request | ||
// https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/cancellation#behavior-requirements | ||
return { }; | ||
return {}; | ||
} | ||
|
||
const content = buildActorResponseContent(actorName, callResult); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit/question: Why is the try block here? We are just catching and then re-throwing. I would remove the try block for the SSE.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I throw exception, connection will not be opened.
When TimeoutError occurs, only warning will be logged to us and connection will be opened, but without this MCP server.