mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 10:02:05 +00:00
feat(MCP Client Tool Node): Add MCP Client Tool Node to connect to MCP servers over SSE (#14464)
Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in> Co-authored-by: JP van Oosten <jp@n8n.io>
This commit is contained in:
212
packages/@n8n/nodes-langchain/nodes/mcp/McpClientTool/utils.ts
Normal file
212
packages/@n8n/nodes-langchain/nodes/mcp/McpClientTool/utils.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
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 { DynamicStructuredTool, type DynamicStructuredToolInput } from 'langchain/tools';
|
||||
import {
|
||||
createResultError,
|
||||
createResultOk,
|
||||
type IDataObject,
|
||||
type IExecuteFunctions,
|
||||
type Result,
|
||||
} from 'n8n-workflow';
|
||||
import { type ZodTypeAny } from 'zod';
|
||||
|
||||
import { convertJsonSchemaToZod } from '@utils/schemaParsing';
|
||||
|
||||
import type { McpAuthenticationOption, McpTool, McpToolIncludeMode } from './types';
|
||||
|
||||
export async function getAllTools(client: Client, cursor?: string): Promise<McpTool[]> {
|
||||
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<ReturnType<Client['callTool']>>;
|
||||
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'],
|
||||
) {
|
||||
return new DynamicStructuredTool({
|
||||
name: tool.name,
|
||||
description: tool.description ?? '',
|
||||
schema: convertJsonSchemaToZod(tool.inputSchema),
|
||||
func: onCallTool,
|
||||
metadata: { isFromToolkit: true },
|
||||
});
|
||||
}
|
||||
|
||||
export class McpToolkit extends Toolkit {
|
||||
constructor(public tools: Array<DynamicStructuredTool<ZodTypeAny>>) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
function safeCreateUrl(url: string, baseUrl?: string | URL): Result<URL, Error> {
|
||||
try {
|
||||
return createResultOk(new URL(url, baseUrl));
|
||||
} catch (error) {
|
||||
return createResultError(error);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeAndValidateUrl(input: string): Result<URL, Error> {
|
||||
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<string, string>;
|
||||
name: string;
|
||||
version: number;
|
||||
}): Promise<Result<Client, ConnectMcpClientError>> {
|
||||
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<IExecuteFunctions, 'getCredentials'>,
|
||||
authentication: McpAuthenticationOption,
|
||||
): Promise<{ headers?: Record<string, string> }> {
|
||||
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 {};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user