import { INodeType, INodeTypeDescription, IWebhookFunctions, IWebhookResponseData, ILoadOptionsFunctions, INodePropertyOptions, } from 'n8n-workflow'; import { getMcpTools } from "../lib/mcp_tool_convert"; // ====================================================== // Cache tools per URL // ====================================================== const toolsCache = new Map(); // ====================================================== // Load OpenAPI → MCP Tools // ====================================================== async function loadTools(openapiUrl: string, filterTag: string, forceRefresh = false): Promise { const cacheKey = `${openapiUrl}::${filterTag}`; if (!forceRefresh && toolsCache.has(cacheKey)) { return toolsCache.get(cacheKey)!; } console.log(`[MCP] 🔄 Refreshing tools from ${openapiUrl} ...`); const fetched = await getMcpTools(openapiUrl, filterTag); console.log(`[MCP] ✅ Loaded ${fetched.length} tools`); if (fetched.length > 0) { console.log(`[MCP] Tools: ${fetched.map((t: any) => t.name).join(", ")}`); } toolsCache.set(cacheKey, fetched); return fetched; } // ====================================================== // JSON-RPC Types // ====================================================== type JSONRPCRequest = { jsonrpc: "2.0"; id: string | number; method: string; params?: any; credentials?: any; }; type JSONRPCResponse = { jsonrpc: "2.0"; id: string | number; result?: any; error?: { code: number; message: string; data?: any }; }; // ====================================================== // EXECUTE TOOL — SUPPORT PATH, QUERY, HEADER, BODY, COOKIE // ====================================================== async function executeTool( tool: any, args: Record = {}, baseUrl: string, token?: string ) { const x = tool["x-props"] || {}; const method = (x.method || "GET").toUpperCase(); let path = x.path || `/${tool.name}`; const query: Record = {}; const headers: Record = { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}), }; let bodyPayload: any = undefined; // ====================================================== // Pisahkan args berdasarkan OpenAPI parameter location // ====================================================== if (Array.isArray(x.parameters)) { for (const p of x.parameters) { const name = p.name; const value = args[name]; if (value === undefined) continue; switch (p.in) { case "path": path = path.replace(`{${name}}`, encodeURIComponent(value)); break; case "query": query[name] = value; break; case "header": headers[name] = value; break; case "cookie": headers["Cookie"] = `${name}=${value}`; break; case "body": case "requestBody": bodyPayload = value; break; default: break; } } } else { // fallback → semua args dianggap body bodyPayload = args; } // ====================================================== // Build Final URL // ====================================================== let url = `${baseUrl}${path}`; const qs = new URLSearchParams(query).toString(); if (qs) url += `?${qs}`; // ====================================================== // Build Request Options // ====================================================== const opts: RequestInit = { method, headers }; if (["POST", "PUT", "PATCH", "DELETE"].includes(method) && bodyPayload !== undefined) { opts.body = JSON.stringify(bodyPayload); } console.log(`[MCP] → Calling ${method} ${url}`); const res = await fetch(url, opts); const contentType = res.headers.get("content-type") || ""; const data = contentType.includes("application/json") ? await res.json() : await res.text(); return { success: res.ok, status: res.status, method, url, path, data, }; } // ====================================================== // JSON-RPC Handler // ====================================================== async function handleMCPRequest( request: JSONRPCRequest, tools: any[] ): Promise { const { id, method, params, credentials } = request; switch (method) { case "initialize": return { jsonrpc: "2.0", id, result: { protocolVersion: "2024-11-05", capabilities: { tools: {} }, serverInfo: { name: "n8n-mcp-server", version: "1.0.0" }, }, }; case "tools/list": return { jsonrpc: "2.0", id, result: { tools: tools.map((t) => { const inputSchema = typeof t.inputSchema === "object" && t.inputSchema?.type === "object" ? t.inputSchema : { type: "object", properties: {}, required: [], }; return { name: t.name, description: t.description || "No description provided", inputSchema, "x-props": t["x-props"], }; }), }, }; case "tools/call": { const toolName = params?.name; const tool = tools.find((t) => t.name === toolName); if (!tool) { return { jsonrpc: "2.0", id, error: { code: -32601, message: `Tool '${toolName}' not found` }, }; } // Converter MCP content yang valid function convertToMcpContent(data: any) { // Jika string → text if (typeof data === "string") { return { type: "text", text: data, }; } // Jika kirim tipe khusus image if (data?.__mcp_type === "image") { return { type: "image", data: data.base64, mimeType: data.mimeType || "image/png", }; } // Jika audio if (data?.__mcp_type === "audio") { return { type: "audio", data: data.base64, mimeType: data.mimeType || "audio/mpeg", }; } // Jika resource link if (data?.__mcp_type === "resource_link") { return { type: "resource_link", name: data.name || "resource", uri: data.uri, }; } // Jika object biasa → jadikan resource if (typeof data === "object") { return { type: "resource", resource: data, }; } // fallback → text stringified return { type: "text", text: JSON.stringify(data, null, 2), }; } try { const baseUrl = credentials?.baseUrl; const token = credentials?.token; const result = await executeTool( tool, params?.arguments || {}, baseUrl, token ); const raw = result.data?.data ?? result.data; return { jsonrpc: "2.0", id, result: { content: [convertToMcpContent(raw)], }, }; } catch (err: any) { return { jsonrpc: "2.0", id, error: { code: -32603, message: err.message }, }; } } case "ping": return { jsonrpc: "2.0", id, result: {} }; default: return { jsonrpc: "2.0", id, error: { code: -32601, message: `Method '${method}' not found` }, }; } } // ====================================================== // MCP TRIGGER NODE // ====================================================== export class OpenapiMcpServer implements INodeType { description: INodeTypeDescription = { displayName: 'OpenAPI MCP Server', name: 'openapiMcpServer', group: ['trigger'], version: 1, description: 'Runs an MCP Server inside n8n', icon: 'file:icon.svg', defaults: { name: 'OpenAPI MCP Server' }, credentials: [ { name: "openapiMcpServerCredentials", required: true }, ], inputs: [], outputs: ['main'], webhooks: [ { name: 'default', httpMethod: 'POST', responseMode: 'onReceived', path: '={{$parameter["path"]}}', }, ], properties: [ { displayName: "Path", name: "path", type: "string", default: "mcp", }, { displayName: "OpenAPI URL", name: "openapiUrl", type: "string", default: "", placeholder: "https://example.com/openapi.json", }, { displayName: "Default Filter", name: "defaultFilter", type: "string", default: "", placeholder: "mcp | tag", }, { displayName: 'Available Tools (auto-refresh)', name: 'toolList', type: 'options', typeOptions: { loadOptionsMethod: 'refreshToolList', refreshOnOpen: true, }, default: 'all', description: 'Daftar tools yang berhasil dimuat dari OpenAPI', }, ], }; // ================================================== // LoadOptions // ================================================== methods = { loadOptions: { async refreshToolList(this: ILoadOptionsFunctions): Promise { const openapiUrl = this.getNodeParameter("openapiUrl", 0) as string; const filterTag = this.getNodeParameter("defaultFilter", 0) as string; if (!openapiUrl) { return [{ name: "❌ No OpenAPI URL provided", value: "" }]; } const tools = await loadTools(openapiUrl, filterTag, true); return [ { name: "All Tools", value: "all" }, ...tools.map((t) => ({ name: t.name, value: t.name, description: t.description, })), ]; }, }, }; // ================================================== // Webhook Handler // ================================================== async webhook(this: IWebhookFunctions): Promise { const openapiUrl = this.getNodeParameter("openapiUrl", 0) as string; const filterTag = this.getNodeParameter("defaultFilter", 0) as string; const tools = await loadTools(openapiUrl, filterTag, true); const creds = await this.getCredentials("openapiMcpServerCredentials") as { baseUrl: string; token: string; }; const body = this.getBodyData(); if (Array.isArray(body)) { const responses = body.map((r) => handleMCPRequest({ ...r, credentials: creds }, tools) ); return { webhookResponse: await Promise.all(responses), }; } const single = await handleMCPRequest( { ...(body as JSONRPCRequest), credentials: creds }, tools ); return { webhookResponse: single, }; } }