mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-12 16:10:30 +02:00
117 lines
3.2 KiB
TypeScript
117 lines
3.2 KiB
TypeScript
import { DynamicStructuredTool, type DynamicStructuredToolInput } from '@langchain/core/tools';
|
|
import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
import { CompatibilityCallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
import { Toolkit } from '@langchain/classic/agents';
|
|
import { type IDataObject } from 'n8n-workflow';
|
|
import { z } from 'zod';
|
|
|
|
import { convertJsonSchemaToZod } from '@utils/schemaParsing';
|
|
|
|
import type { McpToolIncludeMode } from './types';
|
|
import type { McpTool } from '../shared/types';
|
|
|
|
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, timeout: number, onError: (error: string) => void) =>
|
|
async (args: IDataObject) => {
|
|
let result: Awaited<ReturnType<Client['callTool']>>;
|
|
|
|
function handleError(error: unknown) {
|
|
const errorDescription =
|
|
getErrorDescriptionFromToolCall(error) ?? `Failed to execute tool "${name}"`;
|
|
onError(errorDescription);
|
|
return errorDescription;
|
|
}
|
|
|
|
try {
|
|
result = await client.callTool({ name, arguments: args }, CompatibilityCallToolResultSchema, {
|
|
timeout,
|
|
});
|
|
} catch (error) {
|
|
return handleError(error);
|
|
}
|
|
|
|
if (result.isError) {
|
|
return handleError(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: DynamicStructuredTool[]) {
|
|
super();
|
|
}
|
|
}
|