Skip to content

Commit f575427

Browse files
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>
1 parent f1a87af commit f575427

File tree

3 files changed

+73
-3
lines changed

3 files changed

+73
-3
lines changed

packages/@n8n/nodes-langchain/nodes/mcp/McpClientTool/McpClientTool.node.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
22
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
3+
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
34
import { mock } from 'jest-mock-extended';
45
import {
56
NodeConnectionTypes,
@@ -358,5 +359,50 @@ describe('McpClientTool', () => {
358359
new NodeOperationError(supplyDataFunctions.getNode(), 'Weather unknown at location'),
359360
);
360361
});
362+
363+
it('should support setting a timeout', async () => {
364+
jest.spyOn(Client.prototype, 'connect').mockResolvedValue();
365+
const callToolSpy = jest
366+
.spyOn(Client.prototype, 'callTool')
367+
.mockRejectedValue(
368+
new McpError(ErrorCode.RequestTimeout, 'Request timed out', { timeout: 200 }),
369+
);
370+
jest.spyOn(Client.prototype, 'listTools').mockResolvedValue({
371+
tools: [
372+
{
373+
name: 'SlowTool',
374+
description: 'SlowTool throws a timeout',
375+
inputSchema: { type: 'object', properties: { input: { type: 'string' } } },
376+
},
377+
],
378+
});
379+
380+
const mockNode = mock<INode>({ typeVersion: 1 });
381+
const supplyDataResult = await new McpClientTool().supplyData.call(
382+
mock<ISupplyDataFunctions>({
383+
getNode: jest.fn(() => mockNode),
384+
getNodeParameter: jest.fn((key, _index) => {
385+
const parameters: Record<string, any> = {
386+
'options.timeout': 200,
387+
};
388+
return parameters[key];
389+
}),
390+
logger: { debug: jest.fn(), error: jest.fn() },
391+
addInputData: jest.fn(() => ({ index: 0 })),
392+
}),
393+
0,
394+
);
395+
396+
const tools = (supplyDataResult.response as McpToolkit).getTools();
397+
398+
await expect(tools[0].invoke({ input: 'foo' })).resolves.toEqual(
399+
'MCP error -32001: Request timed out',
400+
);
401+
expect(callToolSpy).toHaveBeenCalledWith(
402+
expect.any(Object), // params
403+
expect.any(Object), // schema
404+
expect.objectContaining({ timeout: 200 }),
405+
); // options
406+
});
361407
});
362408
});

packages/@n8n/nodes-langchain/nodes/mcp/McpClientTool/McpClientTool.node.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,26 @@ export class McpClientTool implements INodeType {
213213
},
214214
},
215215
},
216+
{
217+
displayName: 'Options',
218+
name: 'options',
219+
placeholder: 'Add Option',
220+
description: 'Additional options to add',
221+
type: 'collection',
222+
default: {},
223+
options: [
224+
{
225+
displayName: 'Timeout',
226+
name: 'timeout',
227+
type: 'number',
228+
typeOptions: {
229+
minValue: 1,
230+
},
231+
default: 60000,
232+
description: 'Time in ms to wait for tool calls to finish',
233+
},
234+
],
235+
},
216236
],
217237
};
218238

@@ -228,6 +248,7 @@ export class McpClientTool implements INodeType {
228248
itemIndex,
229249
) as McpAuthenticationOption;
230250
const node = this.getNode();
251+
const timeout = this.getNodeParameter('options.timeout', itemIndex, 60000) as number;
231252

232253
let serverTransport: McpServerTransport;
233254
let endpointUrl: string;
@@ -293,7 +314,7 @@ export class McpClientTool implements INodeType {
293314
logWrapper(
294315
mcpToolToDynamicTool(
295316
tool,
296-
createCallTool(tool.name, client.result, (errorMessage) => {
317+
createCallTool(tool.name, client.result, timeout, (errorMessage) => {
297318
const error = new NodeOperationError(node, errorMessage, { itemIndex });
298319
void this.addOutputData(NodeConnectionTypes.AiTool, itemIndex, error);
299320
this.logger.error(`McpClientTool: Tool "${tool.name}" failed to execute`, { error });

packages/@n8n/nodes-langchain/nodes/mcp/McpClientTool/utils.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ export const getErrorDescriptionFromToolCall = (result: unknown): string | undef
7777
};
7878

7979
export const createCallTool =
80-
(name: string, client: Client, onError: (error: string) => void) => async (args: IDataObject) => {
80+
(name: string, client: Client, timeout: number, onError: (error: string) => void) =>
81+
async (args: IDataObject) => {
8182
let result: Awaited<ReturnType<Client['callTool']>>;
8283

8384
function handleError(error: unknown) {
@@ -88,7 +89,9 @@ export const createCallTool =
8889
}
8990

9091
try {
91-
result = await client.callTool({ name, arguments: args }, CompatibilityCallToolResultSchema);
92+
result = await client.callTool({ name, arguments: args }, CompatibilityCallToolResultSchema, {
93+
timeout,
94+
});
9295
} catch (error) {
9396
return handleError(error);
9497
}

0 commit comments

Comments
 (0)