mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
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:
@@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user