mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +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 { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||||
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
||||||
|
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
||||||
import { mock } from 'jest-mock-extended';
|
import { mock } from 'jest-mock-extended';
|
||||||
import {
|
import {
|
||||||
NodeConnectionTypes,
|
NodeConnectionTypes,
|
||||||
@@ -358,5 +359,50 @@ describe('McpClientTool', () => {
|
|||||||
new NodeOperationError(supplyDataFunctions.getNode(), 'Weather unknown at location'),
|
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,
|
itemIndex,
|
||||||
) as McpAuthenticationOption;
|
) as McpAuthenticationOption;
|
||||||
const node = this.getNode();
|
const node = this.getNode();
|
||||||
|
const timeout = this.getNodeParameter('options.timeout', itemIndex, 60000) as number;
|
||||||
|
|
||||||
let serverTransport: McpServerTransport;
|
let serverTransport: McpServerTransport;
|
||||||
let endpointUrl: string;
|
let endpointUrl: string;
|
||||||
@@ -293,7 +314,7 @@ export class McpClientTool implements INodeType {
|
|||||||
logWrapper(
|
logWrapper(
|
||||||
mcpToolToDynamicTool(
|
mcpToolToDynamicTool(
|
||||||
tool,
|
tool,
|
||||||
createCallTool(tool.name, client.result, (errorMessage) => {
|
createCallTool(tool.name, client.result, timeout, (errorMessage) => {
|
||||||
const error = new NodeOperationError(node, errorMessage, { itemIndex });
|
const error = new NodeOperationError(node, errorMessage, { itemIndex });
|
||||||
void this.addOutputData(NodeConnectionTypes.AiTool, itemIndex, error);
|
void this.addOutputData(NodeConnectionTypes.AiTool, itemIndex, error);
|
||||||
this.logger.error(`McpClientTool: Tool "${tool.name}" failed to execute`, { 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 =
|
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']>>;
|
let result: Awaited<ReturnType<Client['callTool']>>;
|
||||||
|
|
||||||
function handleError(error: unknown) {
|
function handleError(error: unknown) {
|
||||||
@@ -88,7 +89,9 @@ export const createCallTool =
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
result = await client.callTool({ name, arguments: args }, CompatibilityCallToolResultSchema);
|
result = await client.callTool({ name, arguments: args }, CompatibilityCallToolResultSchema, {
|
||||||
|
timeout,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return handleError(error);
|
return handleError(error);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user