From d380da75d4eca868239e7cd0b7e4c3983e026afa Mon Sep 17 00:00:00 2001 From: bipproduction Date: Wed, 12 Nov 2025 13:55:14 +0800 Subject: [PATCH] tambahannnya --- .gitignore | 4 + bak.txt | 323 ++++++++++++++++++ .../lib/mcp_tool_convert.js | 8 +- .../nodes/OpenapiMcpServer.js | 105 ++++-- n8n-nodes-openapi-mcp-server/package.json | 2 +- src/nodes/OpenapiMcpServer.ts | 124 +++++-- 6 files changed, 510 insertions(+), 56 deletions(-) create mode 100644 bak.txt diff --git a/.gitignore b/.gitignore index a14702c..e47ea46 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,7 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # Finder (MacOS) folder config .DS_Store + + +# hasil build +n8n-nodes-openapi-mcp-server \ No newline at end of file diff --git a/bak.txt b/bak.txt new file mode 100644 index 0000000..df454ab --- /dev/null +++ b/bak.txt @@ -0,0 +1,323 @@ +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}`; + + // Jika tidak forceRefresh, gunakan cache + if (!forceRefresh && toolsCache.has(cacheKey)) { + return toolsCache.get(cacheKey)!; + } + + console.log(`[MCP] 🔄 Refreshing tools from ${openapiUrl} ...`); + const fetched = await getMcpTools(openapiUrl, filterTag); + + // 🟢 Log jumlah & daftar tools + 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; + }; +}; + +// ====================================================== +// Eksekusi Tool HTTP +// ====================================================== +async function executeTool( + tool: any, + args: Record = {}, + baseUrl: string, + token?: string +) { + const x = tool["x-props"] || {}; + const method = (x.method || "GET").toUpperCase(); + const path = x.path || `/${tool.name}`; + const url = `${baseUrl}${path}`; + + const opts: RequestInit = { + method, + headers: { + "Content-Type": "application/json", + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }, + }; + + if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) { + opts.body = JSON.stringify(args || {}); + } + + 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, + path, + data, + }; +} + +// ====================================================== +// JSON-RPC Handler (per node, per request) +// ====================================================== +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": + // 🟢 Tambahkan jumlah dan daftar nama tools di respons + return { + jsonrpc: "2.0", + id, + result: { + count: tools.length, + names: tools.map(t => t.name), + tools: tools.map((t) => ({ + name: t.name, + description: t.description, + inputSchema: t.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` }, + }; + } + + try { + const baseUrl = credentials?.baseUrl; + const token = credentials?.token; + + const result = await executeTool( + tool, + params?.arguments || {}, + baseUrl, + token + ); + + return { + jsonrpc: "2.0", + id, + result: { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2), + }, + ], + }, + }; + } 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` }, + }; + } +} + +// ====================================================== +// NODE MCP TRIGGER +// ====================================================== +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", + }, + // 🟢 Tambahan agar terlihat jumlah tools di UI + { + displayName: 'Available Tools (auto-refresh)', + name: 'toolList', + type: 'options', + typeOptions: { + loadOptionsMethod: 'refreshToolList', + refreshOnOpen: true, // setiap node dibuka auto refresh + }, + default: '', + description: 'Daftar tools yang berhasil dimuat dari OpenAPI', + }, + ], + }; + + // ================================================== + // LoadOptions untuk tampil di dropdown + // ================================================== + methods = { + loadOptions: { + // 🟢 otomatis refetch setiap kali node dibuka + 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); // force refresh + + return 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; + + // 🟢 selalu refresh (agar node terbaru) + 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, + }; + } +} diff --git a/n8n-nodes-openapi-mcp-server/lib/mcp_tool_convert.js b/n8n-nodes-openapi-mcp-server/lib/mcp_tool_convert.js index dfaf110..4eae962 100644 --- a/n8n-nodes-openapi-mcp-server/lib/mcp_tool_convert.js +++ b/n8n-nodes-openapi-mcp-server/lib/mcp_tool_convert.js @@ -10,7 +10,7 @@ const lodash_1 = __importDefault(require("lodash")); * Convert OpenAPI 3.x JSON spec into MCP-compatible tool definitions (without run()). * Hanya menyertakan endpoint yang memiliki tag berisi "mcp". */ -function convertOpenApiToMcpTools(openApiJson) { +function convertOpenApiToMcpTools(openApiJson, filterTag) { var _a, _b, _c; const tools = []; const paths = openApiJson.paths || {}; @@ -21,7 +21,7 @@ function convertOpenApiToMcpTools(openApiJson) { for (const [method, operation] of Object.entries(methods)) { const tags = Array.isArray(operation.tags) ? operation.tags : []; // ✅ exclude semua yang tidak punya tag atau tag-nya tidak mengandung "mcp" - if (!tags.length || !tags.some(t => t.toLowerCase().includes("mcp"))) + if (!tags.length || !tags.some(t => t.toLowerCase().includes(filterTag))) continue; const rawName = lodash_1.default.snakeCase(operation.operationId || `${method}_${path}`) || "unnamed_tool"; const name = cleanToolName(rawName); @@ -71,9 +71,9 @@ function cleanToolName(name) { /** * Ambil OpenAPI JSON dari endpoint dan konversi ke tools MCP */ -async function getMcpTools(url) { +async function getMcpTools(url, filterTag) { const data = await fetch(url); const openApiJson = await data.json(); - const tools = convertOpenApiToMcpTools(openApiJson); + const tools = convertOpenApiToMcpTools(openApiJson, filterTag); return tools; } diff --git a/n8n-nodes-openapi-mcp-server/nodes/OpenapiMcpServer.js b/n8n-nodes-openapi-mcp-server/nodes/OpenapiMcpServer.js index 675de9c..619b39f 100644 --- a/n8n-nodes-openapi-mcp-server/nodes/OpenapiMcpServer.js +++ b/n8n-nodes-openapi-mcp-server/nodes/OpenapiMcpServer.js @@ -2,12 +2,28 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.OpenapiMcpServer = void 0; const mcp_tool_convert_1 = require("../lib/mcp_tool_convert"); -let tools = []; // ✅ cache global tools +// ====================================================== +// Cache tools per URL +// ====================================================== +const toolsCache = new Map(); // ====================================================== // Load OpenAPI → MCP Tools // ====================================================== -async function loadTools(openapiUrl) { - tools = await (0, mcp_tool_convert_1.getMcpTools)(openapiUrl); +async function loadTools(openapiUrl, filterTag, forceRefresh = false) { + const cacheKey = `${openapiUrl}::${filterTag}`; + // Jika tidak forceRefresh, gunakan cache + if (!forceRefresh && toolsCache.has(cacheKey)) { + return toolsCache.get(cacheKey); + } + console.log(`[MCP] 🔄 Refreshing tools from ${openapiUrl} ...`); + const fetched = await (0, mcp_tool_convert_1.getMcpTools)(openapiUrl, filterTag); + // 🟢 Log jumlah & daftar tools + console.log(`[MCP] ✅ Loaded ${fetched.length} tools`); + if (fetched.length > 0) { + console.log(`[MCP] Tools: ${fetched.map((t) => t.name).join(", ")}`); + } + toolsCache.set(cacheKey, fetched); + return fetched; } // ====================================================== // Eksekusi Tool HTTP @@ -38,9 +54,9 @@ async function executeTool(tool, args = {}, baseUrl, token) { }; } // ====================================================== -// JSON-RPC Handler +// JSON-RPC Handler (per node, per request) // ====================================================== -async function handleMCPRequest(request) { +async function handleMCPRequest(request, tools) { const { id, method, params, credentials } = request; switch (method) { case "initialize": @@ -58,12 +74,22 @@ async function handleMCPRequest(request) { jsonrpc: "2.0", id, result: { - tools: tools.map((t) => ({ - name: t.name, - description: t.description, - inputSchema: t.inputSchema, - "x-props": t["x-props"], - })), + tools: tools.map((t) => { + var _a; + const inputSchema = typeof t.inputSchema === "object" && ((_a = t.inputSchema) === null || _a === void 0 ? void 0 : _a.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": { @@ -122,15 +148,12 @@ class OpenapiMcpServer { group: ['trigger'], version: 1, description: 'Runs an MCP Server inside n8n', - icon: 'fa:server', + icon: 'file:icon.svg', defaults: { name: 'OpenAPI MCP Server' }, credentials: [ - { - name: "openapiMcpServerCredentials", - required: true, - }, + { name: "openapiMcpServerCredentials", required: true }, ], inputs: [], outputs: ['main'], @@ -156,26 +179,66 @@ class OpenapiMcpServer { default: "", placeholder: "https://example.com/openapi.json", }, + { + displayName: "Default Filter", + name: "defaultFilter", + type: "string", + default: "", + placeholder: "mcp | tag", + }, + // 🟢 Tambahan agar terlihat jumlah tools di UI + { + displayName: 'Available Tools (auto-refresh)', + name: 'toolList', + type: 'options', + typeOptions: { + loadOptionsMethod: 'refreshToolList', + refreshOnOpen: true, // setiap node dibuka auto refresh + }, + default: '', + description: 'Daftar tools yang berhasil dimuat dari OpenAPI', + }, ], }; + // ================================================== + // LoadOptions untuk tampil di dropdown + // ================================================== + this.methods = { + loadOptions: { + // 🟢 otomatis refetch setiap kali node dibuka + async refreshToolList() { + const openapiUrl = this.getNodeParameter("openapiUrl", 0); + const filterTag = this.getNodeParameter("defaultFilter", 0); + if (!openapiUrl) { + return [{ name: "❌ No OpenAPI URL provided", value: "" }]; + } + const tools = await loadTools(openapiUrl, filterTag, true); // force refresh + return tools.map((t) => ({ + name: t.name, + value: t.name, + description: t.description, + })); + }, + }, + }; } // ================================================== // WEBHOOK HANDLER // ================================================== async webhook() { const openapiUrl = this.getNodeParameter("openapiUrl", 0); - if (!tools.length) { - await loadTools(openapiUrl); - } + const filterTag = this.getNodeParameter("defaultFilter", 0); + // 🟢 selalu refresh (agar node terbaru) + const tools = await loadTools(openapiUrl, filterTag, true); const creds = await this.getCredentials("openapiMcpServerCredentials"); const body = this.getBodyData(); if (Array.isArray(body)) { - const responses = body.map((r) => handleMCPRequest(Object.assign(Object.assign({}, r), { credentials: creds }))); + const responses = body.map((r) => handleMCPRequest(Object.assign(Object.assign({}, r), { credentials: creds }), tools)); return { webhookResponse: await Promise.all(responses), }; } - const single = await handleMCPRequest(Object.assign(Object.assign({}, body), { credentials: creds })); + const single = await handleMCPRequest(Object.assign(Object.assign({}, body), { credentials: creds }), tools); return { webhookResponse: single, }; diff --git a/n8n-nodes-openapi-mcp-server/package.json b/n8n-nodes-openapi-mcp-server/package.json index 20e1522..bff8303 100644 --- a/n8n-nodes-openapi-mcp-server/package.json +++ b/n8n-nodes-openapi-mcp-server/package.json @@ -1,6 +1,6 @@ { "name": "n8n-nodes-openapi-mcp-server", - "version": "1.1.2", + "version": "1.1.15", "keywords": [ "n8n", "n8n-nodes" diff --git a/src/nodes/OpenapiMcpServer.ts b/src/nodes/OpenapiMcpServer.ts index 9ed9bc5..26c697b 100644 --- a/src/nodes/OpenapiMcpServer.ts +++ b/src/nodes/OpenapiMcpServer.ts @@ -3,17 +3,40 @@ import { INodeTypeDescription, IWebhookFunctions, IWebhookResponseData, + ILoadOptionsFunctions, + INodePropertyOptions, } from 'n8n-workflow'; - import { getMcpTools } from "../lib/mcp_tool_convert"; -let tools: any[] = []; // ✅ cache global tools +// ====================================================== +// Cache tools per URL +// ====================================================== +const toolsCache = new Map(); // ====================================================== // Load OpenAPI → MCP Tools // ====================================================== -async function loadTools(openapiUrl: string, filterTag: string) { - tools = await getMcpTools(openapiUrl, filterTag); +async function loadTools(openapiUrl: string, filterTag: string, forceRefresh = false): Promise { + const cacheKey = `${openapiUrl}::${filterTag}`; + + // Jika tidak forceRefresh, gunakan cache + if (!forceRefresh && toolsCache.has(cacheKey)) { + return toolsCache.get(cacheKey)!; + } + + console.log(`[MCP] 🔄 Refreshing tools from ${openapiUrl} ...`); + const fetched = await getMcpTools(openapiUrl, filterTag); + + // 🟢 Log jumlah & daftar tools + 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; } // ====================================================== @@ -24,7 +47,7 @@ type JSONRPCRequest = { id: string | number; method: string; params?: any; - credentials?: any; // ✅ tambahan (inject credential) + credentials?: any; }; type JSONRPCResponse = { @@ -81,10 +104,11 @@ async function executeTool( } // ====================================================== -// JSON-RPC Handler +// JSON-RPC Handler (per node, per request) // ====================================================== async function handleMCPRequest( - request: JSONRPCRequest + request: JSONRPCRequest, + tools: any[] ): Promise { const { id, method, params, credentials } = request; @@ -105,12 +129,23 @@ async function handleMCPRequest( jsonrpc: "2.0", id, result: { - tools: tools.map((t) => ({ - name: t.name, - description: t.description, - inputSchema: t.inputSchema, - "x-props": t["x-props"], - })), + 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"], + }; + }), }, }; @@ -184,17 +219,11 @@ export class OpenapiMcpServer implements INodeType { defaults: { name: 'OpenAPI MCP Server' }, - credentials: [ - { - name: "openapiMcpServerCredentials", - required: true, - }, + { name: "openapiMcpServerCredentials", required: true }, ], - inputs: [], outputs: ['main'], - webhooks: [ { name: 'default', @@ -203,7 +232,6 @@ export class OpenapiMcpServer implements INodeType { path: '={{$parameter["path"]}}', }, ], - properties: [ { displayName: "Path", @@ -224,10 +252,47 @@ export class OpenapiMcpServer implements INodeType { type: "string", default: "", placeholder: "mcp | tag", - } + }, + // 🟢 Tambahan agar terlihat jumlah tools di UI + { + displayName: 'Available Tools (auto-refresh)', + name: 'toolList', + type: 'options', + typeOptions: { + loadOptionsMethod: 'refreshToolList', + refreshOnOpen: true, // setiap node dibuka auto refresh + }, + default: '', + description: 'Daftar tools yang berhasil dimuat dari OpenAPI', + }, ], }; + // ================================================== + // LoadOptions untuk tampil di dropdown + // ================================================== + methods = { + loadOptions: { + // 🟢 otomatis refetch setiap kali node dibuka + 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); // force refresh + + return tools.map((t) => ({ + name: t.name, + value: t.name, + description: t.description, + })); + }, + }, + }; + // ================================================== // WEBHOOK HANDLER // ================================================== @@ -235,9 +300,8 @@ export class OpenapiMcpServer implements INodeType { const openapiUrl = this.getNodeParameter("openapiUrl", 0) as string; const filterTag = this.getNodeParameter("defaultFilter", 0) as string; - if (!tools.length) { - await loadTools(openapiUrl, filterTag); - } + // 🟢 selalu refresh (agar node terbaru) + const tools = await loadTools(openapiUrl, filterTag, true); const creds = await this.getCredentials("openapiMcpServerCredentials") as { baseUrl: string; @@ -248,17 +312,17 @@ export class OpenapiMcpServer implements INodeType { if (Array.isArray(body)) { const responses = body.map((r) => - handleMCPRequest({ ...r, credentials: creds }) + handleMCPRequest({ ...r, credentials: creds }, tools) ); return { webhookResponse: await Promise.all(responses), }; } - const single = await handleMCPRequest({ - ...(body as JSONRPCRequest), - credentials: creds, - }); + const single = await handleMCPRequest( + { ...(body as JSONRPCRequest), credentials: creds }, + tools + ); return { webhookResponse: single,