Skip to content

Commit 874260c

Browse files
committed
Add Timeout config for the MCP Client tool
1 parent 3f9a271 commit 874260c

File tree

3 files changed

+61
-3
lines changed

3 files changed

+61
-3
lines changed

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

Lines changed: 35 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
NodeOperationError,
@@ -284,5 +285,39 @@ describe('McpClientTool', () => {
284285
headers: { Accept: 'text/event-stream', Authorization: 'Bearer my-token' },
285286
});
286287
});
288+
289+
it('should support setting a timeout', async () => {
290+
jest.spyOn(Client.prototype, 'connect').mockResolvedValue();
291+
jest
292+
.spyOn(Client.prototype, 'callTool')
293+
.mockRejectedValue(
294+
new McpError(ErrorCode.RequestTimeout, 'Request timed out', { timeout: 200 }),
295+
);
296+
jest.spyOn(Client.prototype, 'listTools').mockResolvedValue({
297+
tools: [
298+
{
299+
name: 'SlowTool',
300+
description: 'SlowTool throws a timeout',
301+
inputSchema: { type: 'object', properties: { input: { type: 'string' } } },
302+
},
303+
],
304+
});
305+
306+
const mockNode = mock<INode>({ typeVersion: 1 });
307+
const supplyDataResult = await new McpClientTool().supplyData.call(
308+
mock<ISupplyDataFunctions>({
309+
getNode: jest.fn(() => mockNode),
310+
logger: { debug: jest.fn(), error: jest.fn() },
311+
addInputData: jest.fn(() => ({ index: 0 })),
312+
}),
313+
0,
314+
);
315+
316+
const tools = (supplyDataResult.response as McpToolkit).getTools();
317+
318+
await expect(tools[0].invoke({ input: 'foo' })).rejects.toThrow(
319+
'Failed to execute tool "SlowTool"',
320+
);
321+
});
287322
});
288323
});

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

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,26 @@ export class McpClientTool implements INodeType {
173173
},
174174
},
175175
},
176+
{
177+
displayName: 'Options',
178+
name: 'options',
179+
placeholder: 'Add Option',
180+
description: 'Additional options to add',
181+
type: 'collection',
182+
default: {},
183+
options: [
184+
{
185+
displayName: 'Timeout',
186+
name: 'timeout',
187+
type: 'number',
188+
typeOptions: {
189+
minValue: 1,
190+
},
191+
default: 60000,
192+
description: 'Time in ms to wait for tool calls to finish',
193+
},
194+
],
195+
},
176196
],
177197
};
178198

@@ -188,6 +208,7 @@ export class McpClientTool implements INodeType {
188208
itemIndex,
189209
) as McpAuthenticationOption;
190210
const sseEndpoint = this.getNodeParameter('sseEndpoint', itemIndex) as string;
211+
const timeout = this.getNodeParameter('options.timeout', itemIndex, 60000) as number;
191212
const node = this.getNode();
192213
const { headers } = await getAuthHeaders(this, authentication);
193214
const client = await connectMcpClient({
@@ -242,7 +263,7 @@ export class McpClientTool implements INodeType {
242263
logWrapper(
243264
mcpToolToDynamicTool(
244265
tool,
245-
createCallTool(tool.name, client.result, (error) => {
266+
createCallTool(tool.name, client.result, timeout, (error) => {
246267
this.logger.error(`McpClientTool: Tool "${tool.name}" failed to execute`, { error });
247268
throw new NodeOperationError(node, `Failed to execute tool "${tool.name}"`, {
248269
description: error,

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,13 @@ export const getErrorDescriptionFromToolCall = (result: unknown): string | undef
7272
};
7373

7474
export const createCallTool =
75-
(name: string, client: Client, onError: (error: string | undefined) => void) =>
75+
(name: string, client: Client, timeout: number, onError: (error: string | undefined) => void) =>
7676
async (args: IDataObject) => {
7777
let result: Awaited<ReturnType<Client['callTool']>>;
7878
try {
79-
result = await client.callTool({ name, arguments: args }, CompatibilityCallToolResultSchema);
79+
result = await client.callTool({ name, arguments: args }, CompatibilityCallToolResultSchema, {
80+
timeout,
81+
});
8082
} catch (error) {
8183
return onError(getErrorDescriptionFromToolCall(error));
8284
}

0 commit comments

Comments
 (0)