From b2bb780a5a895fe50a0c415aa524c5c777ba0637 Mon Sep 17 00:00:00 2001 From: bipproduction Date: Wed, 19 Nov 2025 11:15:11 +0800 Subject: [PATCH] build --- OpenapiMcpServer.ts.txt | 329 ++++++++++++++++++++++++++++++++++ src/nodes/OpenapiMcpServer.ts | 95 +++++++--- x.json | 18 -- x.sh | 6 + 4 files changed, 405 insertions(+), 43 deletions(-) create mode 100644 OpenapiMcpServer.ts.txt delete mode 100644 x.json create mode 100644 x.sh diff --git a/OpenapiMcpServer.ts.txt b/OpenapiMcpServer.ts.txt new file mode 100644 index 0000000..1a52ced --- /dev/null +++ b/OpenapiMcpServer.ts.txt @@ -0,0 +1,329 @@ +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; + }; +}; + +// ====================================================== +// 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 +// ====================================================== +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` }, + }; + } + + try { + const baseUrl = credentials?.baseUrl; + const token = credentials?.token; + + const result = await executeTool( + tool, + params?.arguments || {}, + baseUrl, + token + ); + + const data = result.data.data; + const isObject = typeof data === "object" && data !== null; + + return { + jsonrpc: "2.0", + id, + result: { + content: [ + isObject + ? { type: "json", data } + : { type: "text", text: JSON.stringify(data || result.data || result) }, + ], + }, + }; + } 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, + }; + } +} diff --git a/src/nodes/OpenapiMcpServer.ts b/src/nodes/OpenapiMcpServer.ts index 1a52ced..f84e399 100644 --- a/src/nodes/OpenapiMcpServer.ts +++ b/src/nodes/OpenapiMcpServer.ts @@ -50,15 +50,11 @@ type JSONRPCResponse = { jsonrpc: "2.0"; id: string | number; result?: any; - error?: { - code: number; - message: string; - data?: any; - }; + error?: { code: number; message: string; data?: any }; }; // ====================================================== -// Eksekusi Tool HTTP +// EXECUTE TOOL — SUPPORT PATH, QUERY, HEADER, BODY, COOKIE // ====================================================== async function executeTool( tool: any, @@ -68,24 +64,75 @@ async function executeTool( ) { const x = tool["x-props"] || {}; const method = (x.method || "GET").toUpperCase(); - const path = x.path || `/${tool.name}`; - const url = `${baseUrl}${path}`; + let path = x.path || `/${tool.name}`; - const opts: RequestInit = { - method, - headers: { - "Content-Type": "application/json", - ...(token ? { Authorization: `Bearer ${token}` } : {}), - }, + const query: Record = {}; + const headers: Record = { + "Content-Type": "application/json", + ...(token ? { Authorization: `Bearer ${token}` } : {}), }; - if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) { - opts.body = JSON.stringify(args || {}); + 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(); @@ -94,6 +141,7 @@ async function executeTool( success: res.ok, status: res.status, method, + url, path, data, }; @@ -168,17 +216,16 @@ async function handleMCPRequest( token ); - const data = result.data.data; - const isObject = typeof data === "object" && data !== null; + const content = result.data?.data ?? result.data; return { jsonrpc: "2.0", id, result: { content: [ - isObject - ? { type: "json", data } - : { type: "text", text: JSON.stringify(data || result.data || result) }, + typeof content === "object" + ? { type: "json", data: content } + : { type: "text", text: JSON.stringify(content) }, ], }, }; @@ -214,9 +261,7 @@ export class OpenapiMcpServer implements INodeType { version: 1, description: 'Runs an MCP Server inside n8n', icon: 'file:icon.svg', - defaults: { - name: 'OpenAPI MCP Server' - }, + defaults: { name: 'OpenAPI MCP Server' }, credentials: [ { name: "openapiMcpServerCredentials", required: true }, ], diff --git a/x.json b/x.json deleted file mode 100644 index a1d3bfe..0000000 --- a/x.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "model": "gpt-4.1", - "tools": [ - { - "type": "web_search_preview" - } - ], - "input": [ - { - "role": "system", - "content": "Kamu adalah AI agent yang hanya menjawab berdasarkan tools websearch preview berdasarkan tag [indonesia, bali, badung, abiansemal, darmasaba, 2025], jika tidak menggunakan , jangan menjawab selain hasil dari tools , dilarang percakapan basabasi , jika tidak ada hasil dari tool cukup jawab dengan \"\" jika ada hasil dari tool gunakan format berikut # LAPORAN WEB PREVIEW AGENT\n\n" - }, - { - "role": "user", - "content": "{{ $('map_data').item.json.message_text }}" - } - ] -} diff --git a/x.sh b/x.sh new file mode 100644 index 0000000..dba6f24 --- /dev/null +++ b/x.sh @@ -0,0 +1,6 @@ +FAHMI=628123833845 +JUNAIDIL=62811380873 +curl --get \ + --data-urlencode "nom=$JUNAIDIL" \ + --data-urlencode "text=ngetes doang gak usah dibales" \ + https://wa.wibudev.com/code