261 lines
8.9 KiB
Plaintext
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];
|
|
}
|
|
}
|