import { logWrapper } from '@utils/logWrapper'; import { getConnectionHintNoticeField } from '@utils/sharedFields'; import { NodeConnectionTypes, NodeOperationError, type INodeType, type INodeTypeDescription, type ISupplyDataFunctions, type SupplyData, } from 'n8n-workflow'; import { getTools } from './loadOptions'; import type { McpServerTransport, McpAuthenticationOption, McpToolIncludeMode } from './types'; import { connectMcpClient, createCallTool, getAllTools, getAuthHeaders, getSelectedTools, McpToolkit, mcpToolToDynamicTool, } from './utils'; export class McpClientTool implements INodeType { description: INodeTypeDescription = { displayName: 'MCP Client Tool', name: 'mcpClientTool', icon: { light: 'file:../mcp.svg', dark: 'file:../mcp.dark.svg', }, group: ['output'], version: [1, 1.1], description: 'Connect tools from an MCP Server', defaults: { name: 'MCP Client', }, codex: { categories: ['AI'], subcategories: { AI: ['Model Context Protocol', 'Tools'], }, alias: ['Model Context Protocol', 'MCP Client'], resources: { primaryDocumentation: [ { url: 'https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.toolmcp/', }, ], }, }, inputs: [], outputs: [{ type: NodeConnectionTypes.AiTool, displayName: 'Tools' }], credentials: [ { // eslint-disable-next-line n8n-nodes-base/node-class-description-credentials-name-unsuffixed name: 'httpBearerAuth', required: true, displayOptions: { show: { authentication: ['bearerAuth'], }, }, }, { name: 'httpHeaderAuth', required: true, displayOptions: { show: { authentication: ['headerAuth'], }, }, }, ], properties: [ getConnectionHintNoticeField([NodeConnectionTypes.AiAgent]), { displayName: 'SSE Endpoint', name: 'sseEndpoint', type: 'string', description: 'SSE Endpoint of your MCP server', placeholder: 'e.g. https://my-mcp-server.ai/sse', default: '', required: true, displayOptions: { show: { '@version': [1], }, }, }, { displayName: 'Endpoint', name: 'endpointUrl', type: 'string', description: 'Endpoint of your MCP server', placeholder: 'e.g. https://my-mcp-server.ai/mcp', default: '', required: true, displayOptions: { show: { '@version': [{ _cnd: { gte: 1.1 } }], }, }, }, { displayName: 'Server Transport', name: 'serverTransport', type: 'options', options: [ { name: 'Server Sent Events (Deprecated)', value: 'sse', }, { name: 'HTTP Streamable', value: 'httpStreamable', }, ], default: 'sse', description: 'The transport used by your endpoint', displayOptions: { show: { '@version': [{ _cnd: { gte: 1.1 } }], }, }, }, { displayName: 'Authentication', name: 'authentication', type: 'options', options: [ { name: 'Bearer Auth', value: 'bearerAuth', }, { name: 'Header Auth', value: 'headerAuth', }, { name: 'None', value: 'none', }, ], default: 'none', description: 'The way to authenticate with your endpoint', }, { displayName: 'Credentials', name: 'credentials', type: 'credentials', default: '', displayOptions: { show: { authentication: ['headerAuth', 'bearerAuth'], }, }, }, { displayName: 'Tools to Include', name: 'include', type: 'options', description: 'How to select the tools you want to be exposed to the AI Agent', default: 'all', options: [ { name: 'All', value: 'all', description: 'Also include all unchanged fields from the input', }, { name: 'Selected', value: 'selected', description: 'Also include the tools listed in the parameter "Tools to Include"', }, { name: 'All Except', value: 'except', description: 'Exclude the tools listed in the parameter "Tools to Exclude"', }, ], }, { displayName: 'Tools to Include', name: 'includeTools', type: 'multiOptions', default: [], description: 'Choose from the list, or specify IDs using an expression', typeOptions: { loadOptionsMethod: 'getTools', loadOptionsDependsOn: ['sseEndpoint'], }, displayOptions: { show: { include: ['selected'], }, }, }, { displayName: 'Tools to Exclude', name: 'excludeTools', type: 'multiOptions', default: [], description: 'Choose from the list, or specify IDs using an expression', typeOptions: { loadOptionsMethod: 'getTools', }, displayOptions: { show: { include: ['except'], }, }, }, ], }; methods = { loadOptions: { getTools, }, }; async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise { const authentication = this.getNodeParameter( 'authentication', itemIndex, ) as McpAuthenticationOption; const node = this.getNode(); let serverTransport: McpServerTransport; let endpointUrl: string; if (node.typeVersion === 1) { serverTransport = 'sse'; endpointUrl = this.getNodeParameter('sseEndpoint', itemIndex) as string; } else { serverTransport = this.getNodeParameter('serverTransport', itemIndex) as McpServerTransport; endpointUrl = this.getNodeParameter('endpointUrl', itemIndex) as string; } const { headers } = await getAuthHeaders(this, authentication); const client = await connectMcpClient({ serverTransport, endpointUrl, headers, name: node.type, version: node.typeVersion, }); const setError = (message: string, description?: string): SupplyData => { const error = new NodeOperationError(node, message, { itemIndex, description }); this.addOutputData(NodeConnectionTypes.AiTool, itemIndex, error); throw error; }; if (!client.ok) { this.logger.error('McpClientTool: Failed to connect to MCP Server', { error: client.error, }); switch (client.error.type) { case 'invalid_url': return setError('Could not connect to your MCP server. The provided URL is invalid.'); case 'connection': default: return setError('Could not connect to your MCP server'); } } this.logger.debug('McpClientTool: Successfully connected to MCP Server'); const mode = this.getNodeParameter('include', itemIndex) as McpToolIncludeMode; const includeTools = this.getNodeParameter('includeTools', itemIndex, []) as string[]; const excludeTools = this.getNodeParameter('excludeTools', itemIndex, []) as string[]; const allTools = await getAllTools(client.result); const mcpTools = getSelectedTools({ tools: allTools, mode, includeTools, excludeTools, }); if (!mcpTools.length) { return setError( 'MCP Server returned no tools', 'Connected successfully to your MCP server but it returned an empty list of tools.', ); } const tools = mcpTools.map((tool) => logWrapper( mcpToolToDynamicTool( tool, createCallTool(tool.name, client.result, (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 }); }), ), this, ), ); this.logger.debug(`McpClientTool: Connected to MCP Server with ${tools.length} tools`); const toolkit = new McpToolkit(tools); return { response: toolkit, closeFunction: async () => await client.result.close() }; } }