UNPKG

n8n-nodes-mcp-client

Version:

4Runr's custom MCP Client node for n8n — connects to a self-hosted MCP server via SSE and streams tool definitions to n8n AI Agents.

259 lines (258 loc) 11.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.McpClient = void 0; // n8n types are resolved at runtime in the n8n environment const n8n_workflow_1 = require("n8n-workflow"); const axios_1 = __importDefault(require("axios")); // Use require for EventSource for compatibility const EventSource = require('eventsource'); class McpClient { constructor() { this.description = { displayName: '4Runr MCP Client', name: 'mcpClient', group: ['input'], version: 1, description: 'Streams tool definitions from a 4Runr MCP Server via SSE. Built to power dynamic AI agents using the MCP protocol.', defaults: { name: '4Runr MCP Client', color: '#00b894', }, inputs: [], outputs: ["main" /* NodeConnectionType.Main */], icon: 'file:images/n8n4Runr.svg', usableAsTool: true, credentials: [ { name: 'McpClientApi', required: true, displayName: '4Runr MCP Access', }, ], properties: [ { displayName: 'Node Mode', name: 'nodeMode', type: 'options', options: [ { name: 'Discover Tools', value: 'discover', description: 'Stream and list available tools from the MCP server', }, { name: 'Execute Tool', value: 'execute', description: 'Execute a tool on the MCP server', }, ], default: 'discover', description: 'Choose whether to discover tools or execute a tool', }, { displayName: 'Tool Name', name: 'toolName', type: 'string', default: '', description: 'Name of the tool to execute (as received from the server)', required: false, displayOptions: { show: { nodeMode: ['execute'], }, }, typeOptions: { allowExpression: true, }, }, { displayName: 'Tool Parameters', name: 'toolParameters', type: 'json', default: '{}', description: 'Parameters for the selected tool (as JSON)', required: false, displayOptions: { show: { nodeMode: ['execute'], }, }, typeOptions: { allowExpression: true, }, }, { displayName: 'Tool Name Mapping (Optional)', name: 'toolNameMapping', type: 'json', default: '{}', description: 'Optional mapping of tool names to aliases (e.g., {"calendar.check_availability": "Check Availability"})', required: false, }, ], }; } async execute() { const credentials = await this.getCredentials('McpClientApi'); const { sseUrl, sseTimeout, headers, messageEndpoint } = credentials; const nodeMode = this.getNodeParameter('nodeMode', 0); console.log(`[McpClient] Starting execution with config:`, { sseUrl, sseTimeout, messageEndpoint, nodeMode, hasHeaders: !!headers }); if (nodeMode === 'discover') { return new Promise((resolve, reject) => { const eventSourceOptions = {}; if (headers) { try { const parsedHeaders = typeof headers === 'string' ? JSON.parse(headers) : headers; eventSourceOptions.headers = parsedHeaders; console.log('[McpClient] Using headers:', parsedHeaders); } catch (err) { console.error('[McpClient] Failed to parse headers:', err); throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Failed to parse headers: ' + err.message); } } console.log(`[McpClient] Connecting to SSE: ${sseUrl}`); const eventSource = new EventSource(sseUrl, eventSourceOptions); let timeoutId; if (sseTimeout) { timeoutId = setTimeout(() => { console.error('[McpClient] SSE connection timed out after', sseTimeout, 'ms'); eventSource.close(); reject(new n8n_workflow_1.NodeOperationError(this.getNode(), `SSE connection timed out after ${sseTimeout}ms`)); }, sseTimeout); } eventSource.onopen = () => { if (timeoutId) clearTimeout(timeoutId); console.log('[McpClient] SSE connection opened successfully'); }; eventSource.onerror = (err) => { console.error('[McpClient] SSE connection error:', err); eventSource.close(); reject(new n8n_workflow_1.NodeOperationError(this.getNode(), 'Failed to connect to SSE endpoint: ' + err.message)); }; eventSource.onmessage = (event) => { console.log('[McpClient] Received message event:', { type: event.type, data: event.data, lastEventId: event.lastEventId }); }; eventSource.addEventListener('tools', (event) => { console.log('[McpClient] Received tools event:', { data: event.data, lastEventId: event.lastEventId }); try { const parsed = JSON.parse(event.data); console.log('[McpClient] Parsed tools data:', parsed); if (!parsed.tools || !Array.isArray(parsed.tools)) { throw new Error('Invalid tools data: missing or invalid tools array'); } let tools = parsed.tools.map((tool) => { console.log('[McpClient] Processing tool:', tool); return { name: tool.name, description: tool.description, parameters: tool.parameters || {}, }; }); console.log(`[McpClient] Emitting ${tools.length} tools:`, tools); eventSource.close(); resolve([this.helpers.returnJsonArray(tools)]); } catch (err) { console.error('[McpClient] Failed to process tools event:', err); eventSource.close(); reject(new n8n_workflow_1.NodeOperationError(this.getNode(), 'Failed to process tools event: ' + err.message)); } }); }); } else if (nodeMode === 'execute') { const toolName = this.getNodeParameter('toolName', 0); const toolParameters = this.getNodeParameter('toolParameters', 0); console.log('[McpClient] Executing tool:', { toolName, toolParameters }); const result = await McpClient.executeToolCall(this, toolName, toolParameters, credentials); return [this.helpers.returnJsonArray([result])]; } else { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unknown node mode: ${nodeMode}`); } } // --- Tool Execution Logic --- /** * Executes a tool call by POSTing to the MCP server's message endpoint. * Throws a clear error if toolName or parameters are missing/invalid. */ static async executeToolCall(context, toolName, parameters, credentials) { const { messageEndpoint, headers } = credentials; console.log('[McpClient] Executing tool call:', { toolName, parameters, messageEndpoint, hasHeaders: !!headers }); if (!toolName || typeof toolName !== 'string') { throw new n8n_workflow_1.NodeOperationError(context.getNode(), 'Tool name is required and must be a string.'); } if (!parameters || typeof parameters !== 'object') { throw new n8n_workflow_1.NodeOperationError(context.getNode(), 'Tool parameters are required and must be an object.'); } const payload = { toolCall: { toolName, parameters, }, }; console.log('[McpClient] Preparing POST request:', { url: messageEndpoint, payload }); const axiosConfig = { headers: { 'Content-Type': 'application/json', ...(headers ? (typeof headers === 'string' ? JSON.parse(headers) : headers) : {}), }, }; try { console.log('[McpClient] Sending POST request...'); const response = await axios_1.default.post(messageEndpoint, payload, axiosConfig); console.log('[McpClient] Received response:', { status: response.status, statusText: response.statusText, data: response.data }); return response.data; } catch (error) { console.error('[McpClient] Tool call failed:', { error: error.message, response: error.response?.data, status: error.response?.status, statusText: error.response?.statusText }); let errorMessage = 'Tool call failed: '; if (error.response?.data?.message) { errorMessage += error.response.data.message; } else if (error.response?.statusText) { errorMessage += `${error.response.status} ${error.response.statusText}`; } else { errorMessage += error.message; } throw new n8n_workflow_1.NodeOperationError(context.getNode(), errorMessage); } } } exports.McpClient = McpClient;