import { DynamicStructuredTool, type DynamicStructuredToolInput } from '@langchain/core/tools'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; import { CompatibilityCallToolResultSchema } from '@modelcontextprotocol/sdk/types.js'; import { Toolkit } from 'langchain/agents'; import { createResultError, createResultOk, type IDataObject, type IExecuteFunctions, type Result, } from 'n8n-workflow'; import { z } from 'zod'; import { convertJsonSchemaToZod } from '@utils/schemaParsing'; import type { McpAuthenticationOption, McpTool, McpToolIncludeMode } from './types'; export async function getAllTools(client: Client, cursor?: string): Promise { const { tools, nextCursor } = await client.listTools({ cursor }); if (nextCursor) { return (tools as McpTool[]).concat(await getAllTools(client, nextCursor)); } return tools as McpTool[]; } export function getSelectedTools({ mode, includeTools, excludeTools, tools, }: { mode: McpToolIncludeMode; includeTools?: string[]; excludeTools?: string[]; tools: McpTool[]; }) { switch (mode) { case 'selected': { if (!includeTools?.length) return tools; const include = new Set(includeTools); return tools.filter((tool) => include.has(tool.name)); } case 'except': { const except = new Set(excludeTools ?? []); return tools.filter((tool) => !except.has(tool.name)); } case 'all': default: return tools; } } export const getErrorDescriptionFromToolCall = (result: unknown): string | undefined => { if (result && typeof result === 'object') { if ('content' in result && Array.isArray(result.content)) { const errorMessage = (result.content as Array<{ type: 'text'; text: string }>).find( (content) => content && typeof content === 'object' && typeof content.text === 'string', )?.text; return errorMessage; } else if ('toolResult' in result && typeof result.toolResult === 'string') { return result.toolResult; } if ('message' in result && typeof result.message === 'string') { return result.message; } } return undefined; }; export const createCallTool = (name: string, client: Client, onError: (error: string | undefined) => void) => async (args: IDataObject) => { let result: Awaited>; try { result = await client.callTool({ name, arguments: args }, CompatibilityCallToolResultSchema); } catch (error) { return onError(getErrorDescriptionFromToolCall(error)); } if (result.isError) { return onError(getErrorDescriptionFromToolCall(result)); } if (result.toolResult !== undefined) { return result.toolResult; } if (result.content !== undefined) { return result.content; } return result; }; export function mcpToolToDynamicTool( tool: McpTool, onCallTool: DynamicStructuredToolInput['func'], ): DynamicStructuredTool> { const rawSchema = convertJsonSchemaToZod(tool.inputSchema); // Ensure we always have an object schema for structured tools const objectSchema = rawSchema instanceof z.ZodObject ? rawSchema : z.object({ value: rawSchema }); return new DynamicStructuredTool({ name: tool.name, description: tool.description ?? '', schema: objectSchema, func: onCallTool, metadata: { isFromToolkit: true }, }); } export class McpToolkit extends Toolkit { constructor(public tools: Array>>) { super(); } } function safeCreateUrl(url: string, baseUrl?: string | URL): Result { try { return createResultOk(new URL(url, baseUrl)); } catch (error) { return createResultError(error); } } function normalizeAndValidateUrl(input: string): Result { const withProtocol = !/^https?:\/\//i.test(input) ? `https://${input}` : input; const parsedUrl = safeCreateUrl(withProtocol); if (!parsedUrl.ok) { return createResultError(parsedUrl.error); } return parsedUrl; } type ConnectMcpClientError = | { type: 'invalid_url'; error: Error } | { type: 'connection'; error: Error }; export async function connectMcpClient({ headers, sseEndpoint, name, version, }: { sseEndpoint: string; headers?: Record; name: string; version: number; }): Promise> { try { const endpoint = normalizeAndValidateUrl(sseEndpoint); if (!endpoint.ok) { return createResultError({ type: 'invalid_url', error: endpoint.error }); } const transport = new SSEClientTransport(endpoint.result, { eventSourceInit: { fetch: async (url, init) => await fetch(url, { ...init, headers: { ...headers, Accept: 'text/event-stream', }, }), }, requestInit: { headers }, }); const client = new Client( { name, version: version.toString() }, { capabilities: { tools: {} } }, ); await client.connect(transport); return createResultOk(client); } catch (error) { return createResultError({ type: 'connection', error }); } } export async function getAuthHeaders( ctx: Pick, authentication: McpAuthenticationOption, ): Promise<{ headers?: Record }> { switch (authentication) { case 'headerAuth': { const header = await ctx .getCredentials<{ name: string; value: string }>('httpHeaderAuth') .catch(() => null); if (!header) return {}; return { headers: { [header.name]: header.value } }; } case 'bearerAuth': { const result = await ctx .getCredentials<{ token: string }>('httpBearerAuth') .catch(() => null); if (!result) return {}; return { headers: { Authorization: `Bearer ${result.token}` } }; } case 'none': default: { return {}; } } }