Files
n8n-nodes-mcp-tool/McpClientTool.node.ts.v2.txt
bipproduction f07b60b310 tambahan
2025-12-06 19:39:33 +08:00

261 lines
8.9 KiB
Plaintext

// =====================
// MCP CLIENT TOOL (AI-Agent only)
// Compatible with OpenAPI-to-MCP servers
// =====================
import {
type IDataObject,
type INodeExecutionData,
type INodeType,
type INodeTypeDescription,
type INodePropertyOptions,
type ILoadOptionsFunctions,
NodeOperationError,
} from 'n8n-workflow';
/* -----------------------------
Types (MCP Spec Compliant)
----------------------------- */
interface MCPPayload {
name: string;
description?: string;
inputSchema?: IDataObject;
}
interface MCPToolsListResult {
tools: MCPPayload[];
}
interface JsonRpcRequest {
jsonrpc: '2.0';
id: string | number;
method: string;
params?: any;
}
interface JsonRpcSuccessResponse<T = any> {
jsonrpc: '2.0';
id: string | number;
result: T;
}
interface JsonRpcErrorResponse {
jsonrpc: '2.0';
id: string | number;
error: { code: number; message: string; data?: any };
}
type JsonRpcResponse<T = any> = JsonRpcSuccessResponse<T> | JsonRpcErrorResponse;
/* -----------------------------
Helpers
----------------------------- */
function parseEndpoint(endpoint: string): string {
return endpoint.trim().replace(/\/+$/, '');
}
function generateRpcId(): string {
return `n8n_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
}
/* -----------------------------
Node Definition
----------------------------- */
export class McpClientTool implements INodeType {
description: INodeTypeDescription = {
displayName: 'MCP Client Tool',
name: 'mcpClientTool',
icon: 'file:icon.svg',
group: ['transform'],
version: 1,
usableAsTool: true,
description:
'Calls tools on an MCP-compliant server using the Model Context Protocol (JSON-RPC). ' +
'Use this node ONLY inside an AI-Agent; the agent will choose the tool automatically.',
defaults: { name: 'MCP Client' },
inputs: ['main'],
outputs: ['main'],
credentials: [{ name: 'mcptool', required: false }],
properties: [
{
displayName: 'MCP Server Endpoint',
name: 'endpoint',
type: 'string',
default: 'https://cld-dkr-prod-jenna-mcp.wibudev.com/mcp',
description: 'The JSON-RPC endpoint of your MCP server',
required: true,
},
{
displayName: 'Tool Selection',
name: 'toolSelection',
type: 'options',
default: 'all',
options: [
{ name: 'All Tools', value: 'all' },
{ name: 'Selected Tools', value: 'selected' },
{ name: 'All Except Selected', value: 'except' },
],
},
{
displayName: 'Selected Tools',
name: 'selectedTools',
type: 'multiOptions',
default: [],
typeOptions: { loadOptionsMethod: 'getMcpTools' },
displayOptions: { show: { toolSelection: ['selected', 'except'] } },
},
],
};
methods = {
loadOptions: {
async getMcpTools(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const endpoint = parseEndpoint(this.getNodeParameter('endpoint', 0) as string);
const id = generateRpcId();
const request: JsonRpcRequest = { jsonrpc: '2.0', id, method: 'tools/list' };
let res: JsonRpcResponse<MCPToolsListResult>;
try {
res = await this.helpers.request({
method: 'POST',
uri: endpoint,
json: true,
body: request,
});
} catch (e) {
throw new NodeOperationError(this.getNode(), `MCP connect error: ${e}`);
}
if ('error' in res)
throw new NodeOperationError(
this.getNode(),
`MCP error: [${res.error.code}] ${res.error.message}`,
);
const tools = res.result?.tools;
if (!Array.isArray(tools))
throw new NodeOperationError(this.getNode(), 'Invalid tools list');
return tools.map((t) => ({
name: t.name,
value: t.name,
description: t.description,
}));
},
},
loadMethods: {
async getAiToolDefinitions(this: any) {
const ep = parseEndpoint(this.getNodeParameter('endpoint', 0) as string);
const id = generateRpcId();
const req: JsonRpcRequest = { jsonrpc: '2.0', id, method: 'tools/list' };
let res: JsonRpcResponse<MCPToolsListResult>;
try {
res = await this.helpers.request({
method: 'POST',
uri: ep,
json: true,
body: req,
});
} catch {
return [];
}
if ('error' in res || !Array.isArray(res.result?.tools)) return [];
const mode = this.getNodeParameter('toolSelection', 0) as string;
const selected = this.getNodeParameter('selectedTools', 0) as string[];
return res.result.tools
.filter((t) => {
if (mode === 'all') return true;
if (mode === 'selected') return selected.includes(t.name);
if (mode === 'except') return !selected.includes(t.name);
return true;
})
.map((t) => ({
name: t.name,
description: t.description || '',
parameters:
t.inputSchema || {
type: 'object',
properties: {},
},
}));
},
},
};
async execute(this: any): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const output: INodeExecutionData[] = [];
const endpoint = parseEndpoint(this.getNodeParameter('endpoint', 0) as string);
for (let i = 0; i < items.length; i++) {
const json = items[i].json ?? {};
/* ==========================
Ambil tool dari payload AI-Agent
========================== */
let toolName: string | undefined;
let args: IDataObject = {};
toolName =
json.tool ||
json.toolName ||
json.tool_name ||
json.name ||
json.function;
if (json.arguments) args = json.arguments as IDataObject;
else if (json.params) args = json.params as IDataObject;
else if (json.parameters) args = json.parameters as IDataObject;
else if (json.input) args = json.input as IDataObject;
else if (toolName && Object.keys(json).length > 0) {
const { tool, toolName: tn, tool_name, name, function: fn, ...rest } =
json;
args = rest;
}
if (!toolName || typeof toolName !== 'string') {
throw new NodeOperationError(
this.getNode(),
'Node ini hanya boleh dipakai di dalam AI-Agent. ' +
'Pastikan Agent dipasang di workflow dan node ini terdaftar sebagai tool.',
);
}
const req: JsonRpcRequest = {
jsonrpc: '2.0',
id: generateRpcId(),
method: 'tools/call',
params: { name: toolName, arguments: args },
};
try {
const res: JsonRpcResponse = await this.helpers.request({
method: 'POST',
uri: endpoint,
json: true,
body: req,
});
if ('error' in res) {
output.push({
json: { error: true, tool: toolName, ...res.error },
pairedItem: { item: i },
});
} else {
output.push({
json: { error: false, tool: toolName, result: res.result },
pairedItem: { item: i },
});
}
} catch (e: any) {
output.push({
json: { error: true, tool: toolName, message: e.message || 'Unknown' },
pairedItem: { item: i },
});
}
}
return [output];
}
}