From f575427d4dfb56370badc2f2efc561d917ce97c4 Mon Sep 17 00:00:00 2001 From: jreyesr Date: Thu, 14 Aug 2025 12:28:12 -0500 Subject: [PATCH] feat(MCP Client Tool Node): Add Timeout config for the MCP Client tool (#15886) Co-authored-by: Shireen Missi <94372015+ShireenMissi@users.noreply.github.com> --- .../McpClientTool/McpClientTool.node.test.ts | 46 +++++++++++++++++++ .../mcp/McpClientTool/McpClientTool.node.ts | 23 +++++++++- .../nodes/mcp/McpClientTool/utils.ts | 7 ++- 3 files changed, 73 insertions(+), 3 deletions(-) diff --git a/packages/@n8n/nodes-langchain/nodes/mcp/McpClientTool/McpClientTool.node.test.ts b/packages/@n8n/nodes-langchain/nodes/mcp/McpClientTool/McpClientTool.node.test.ts index 67d6660982..9de5cc2add 100644 --- a/packages/@n8n/nodes-langchain/nodes/mcp/McpClientTool/McpClientTool.node.test.ts +++ b/packages/@n8n/nodes-langchain/nodes/mcp/McpClientTool/McpClientTool.node.test.ts @@ -1,5 +1,6 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; +import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js'; import { mock } from 'jest-mock-extended'; import { NodeConnectionTypes, @@ -358,5 +359,50 @@ describe('McpClientTool', () => { new NodeOperationError(supplyDataFunctions.getNode(), 'Weather unknown at location'), ); }); + + it('should support setting a timeout', async () => { + jest.spyOn(Client.prototype, 'connect').mockResolvedValue(); + const callToolSpy = jest + .spyOn(Client.prototype, 'callTool') + .mockRejectedValue( + new McpError(ErrorCode.RequestTimeout, 'Request timed out', { timeout: 200 }), + ); + jest.spyOn(Client.prototype, 'listTools').mockResolvedValue({ + tools: [ + { + name: 'SlowTool', + description: 'SlowTool throws a timeout', + inputSchema: { type: 'object', properties: { input: { type: 'string' } } }, + }, + ], + }); + + const mockNode = mock({ typeVersion: 1 }); + const supplyDataResult = await new McpClientTool().supplyData.call( + mock({ + getNode: jest.fn(() => mockNode), + getNodeParameter: jest.fn((key, _index) => { + const parameters: Record = { + 'options.timeout': 200, + }; + return parameters[key]; + }), + logger: { debug: jest.fn(), error: jest.fn() }, + addInputData: jest.fn(() => ({ index: 0 })), + }), + 0, + ); + + const tools = (supplyDataResult.response as McpToolkit).getTools(); + + await expect(tools[0].invoke({ input: 'foo' })).resolves.toEqual( + 'MCP error -32001: Request timed out', + ); + expect(callToolSpy).toHaveBeenCalledWith( + expect.any(Object), // params + expect.any(Object), // schema + expect.objectContaining({ timeout: 200 }), + ); // options + }); }); }); diff --git a/packages/@n8n/nodes-langchain/nodes/mcp/McpClientTool/McpClientTool.node.ts b/packages/@n8n/nodes-langchain/nodes/mcp/McpClientTool/McpClientTool.node.ts index dbd5113c68..3167c400fb 100644 --- a/packages/@n8n/nodes-langchain/nodes/mcp/McpClientTool/McpClientTool.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/mcp/McpClientTool/McpClientTool.node.ts @@ -213,6 +213,26 @@ export class McpClientTool implements INodeType { }, }, }, + { + displayName: 'Options', + name: 'options', + placeholder: 'Add Option', + description: 'Additional options to add', + type: 'collection', + default: {}, + options: [ + { + displayName: 'Timeout', + name: 'timeout', + type: 'number', + typeOptions: { + minValue: 1, + }, + default: 60000, + description: 'Time in ms to wait for tool calls to finish', + }, + ], + }, ], }; @@ -228,6 +248,7 @@ export class McpClientTool implements INodeType { itemIndex, ) as McpAuthenticationOption; const node = this.getNode(); + const timeout = this.getNodeParameter('options.timeout', itemIndex, 60000) as number; let serverTransport: McpServerTransport; let endpointUrl: string; @@ -293,7 +314,7 @@ export class McpClientTool implements INodeType { logWrapper( mcpToolToDynamicTool( tool, - createCallTool(tool.name, client.result, (errorMessage) => { + createCallTool(tool.name, client.result, timeout, (errorMessage) => { const error = new NodeOperationError(node, errorMessage, { itemIndex }); void this.addOutputData(NodeConnectionTypes.AiTool, itemIndex, error); this.logger.error(`McpClientTool: Tool "${tool.name}" failed to execute`, { error }); diff --git a/packages/@n8n/nodes-langchain/nodes/mcp/McpClientTool/utils.ts b/packages/@n8n/nodes-langchain/nodes/mcp/McpClientTool/utils.ts index 06cbe8ceb0..065a2fd92c 100644 --- a/packages/@n8n/nodes-langchain/nodes/mcp/McpClientTool/utils.ts +++ b/packages/@n8n/nodes-langchain/nodes/mcp/McpClientTool/utils.ts @@ -77,7 +77,8 @@ export const getErrorDescriptionFromToolCall = (result: unknown): string | undef }; export const createCallTool = - (name: string, client: Client, onError: (error: string) => void) => async (args: IDataObject) => { + (name: string, client: Client, timeout: number, onError: (error: string) => void) => + async (args: IDataObject) => { let result: Awaited>; function handleError(error: unknown) { @@ -88,7 +89,9 @@ export const createCallTool = } try { - result = await client.callTool({ name, arguments: args }, CompatibilityCallToolResultSchema); + result = await client.callTool({ name, arguments: args }, CompatibilityCallToolResultSchema, { + timeout, + }); } catch (error) { return handleError(error); }