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>
This commit is contained in:
jreyesr
2025-08-14 12:28:12 -05:00
committed by GitHub
parent f1a87af059
commit f575427d4d
3 changed files with 73 additions and 3 deletions

View File

@@ -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<INode>({ typeVersion: 1 });
const supplyDataResult = await new McpClientTool().supplyData.call(
mock<ISupplyDataFunctions>({
getNode: jest.fn(() => mockNode),
getNodeParameter: jest.fn((key, _index) => {
const parameters: Record<string, any> = {
'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
});
});
});

View File

@@ -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 });

View File

@@ -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<ReturnType<Client['callTool']>>;
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);
}