diff --git a/bin/build.ts b/bin/build.ts new file mode 100644 index 0000000..0187fe0 --- /dev/null +++ b/bin/build.ts @@ -0,0 +1,86 @@ + +import { execSync } from "node:child_process"; +import { readdir, rm, mkdir, cp } from "node:fs/promises"; +import { join, relative, dirname } from "node:path"; + +const SRC = "src"; +const DIST = "dist"; + +const ASSET_EXT = [ + ".svg", ".png", ".jpg", ".jpeg", ".webp", ".gif", + ".json", ".html", ".css", ".txt", ".ico", ".md" +]; + +// Recursively scan directory tree +async function walk(dir: string): Promise { + const entries = await readdir(dir, { withFileTypes: true }); + const files: string[] = []; + + for (const entry of entries) { + const full = join(dir, entry.name); + if (entry.isDirectory()) { + files.push(...await walk(full)); + } else { + files.push(full); + } + } + + return files; +} + +async function build() { + console.log("๐Ÿงน Cleaning dist/..."); + + await rm(DIST, { recursive: true, force: true }); + await mkdir(DIST, { recursive: true }); + + console.log("๐Ÿ” Scanning src/..."); + + const allFiles = await walk(SRC); + + const tsFiles = allFiles.filter(f => + f.endsWith(".ts") || f.endsWith(".tsx") || f.endsWith(".js") + ); + + const assets = allFiles.filter(f => + ASSET_EXT.some(ext => f.toLowerCase().endsWith(ext)) + ); + + console.log("โšก Building & Minifying TypeScript..."); + + for (const file of tsFiles) { + const rel = relative(SRC, file); + const outDir = join(DIST, dirname(rel)); + + await mkdir(outDir, { recursive: true }); + + await Bun.build({ + entrypoints: [file], + outdir: outDir, + splitting: false, + minify: true, // โ† minify otomatis + sourcemap: "external", + target: "browser" + }); + + console.log(" โœ” Built:", rel); + } + + console.log("๐Ÿ“ Copying assets..."); + + for (const file of assets) { + const rel = relative(SRC, file); + const dest = join(DIST, rel); + + await mkdir(dirname(dest), { recursive: true }); + await cp(file, dest); + + console.log(" โœ” Copied:", rel); + } + + console.log("๐ŸŽ‰ Build complete!"); +} + +execSync("npm version patch", { stdio: 'inherit' }) +build() + diff --git a/bin/publish.ts b/bin/publish.ts new file mode 100644 index 0000000..efc47ed --- /dev/null +++ b/bin/publish.ts @@ -0,0 +1,3 @@ +import { execSync } from "child_process"; + +execSync("npm publish", { stdio: 'inherit' }) \ No newline at end of file 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 4eae962..9ca7147 100644 --- a/n8n-nodes-openapi-mcp-server/lib/mcp_tool_convert.js +++ b/n8n-nodes-openapi-mcp-server/lib/mcp_tool_convert.js @@ -7,73 +7,319 @@ exports.convertOpenApiToMcpTools = convertOpenApiToMcpTools; exports.getMcpTools = getMcpTools; 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". + * Convert OpenAPI 3.x JSON spec into MCP-compatible tool definitions. */ function convertOpenApiToMcpTools(openApiJson, filterTag) { - var _a, _b, _c; const tools = []; + if (!openApiJson || typeof openApiJson !== "object") { + console.warn("Invalid OpenAPI JSON"); + return tools; + } const paths = openApiJson.paths || {}; + if (Object.keys(paths).length === 0) { + console.warn("No paths found in OpenAPI spec"); + return tools; + } for (const [path, methods] of Object.entries(paths)) { - // โœ… skip semua path internal MCP + if (!path || typeof path !== "string") + continue; if (path.startsWith("/mcp")) continue; + if (!methods || typeof methods !== "object") + continue; 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(filterTag))) + const validMethods = ["get", "post", "put", "delete", "patch", "head", "options"]; + if (!validMethods.includes(method.toLowerCase())) continue; - const rawName = lodash_1.default.snakeCase(operation.operationId || `${method}_${path}`) || "unnamed_tool"; - const name = cleanToolName(rawName); - const description = operation.description || - operation.summary || - `Execute ${method.toUpperCase()} ${path}`; - const schema = ((_c = (_b = (_a = operation.requestBody) === null || _a === void 0 ? void 0 : _a.content) === null || _b === void 0 ? void 0 : _b["application/json"]) === null || _c === void 0 ? void 0 : _c.schema) || { - type: "object", - properties: {}, - additionalProperties: true, - }; - const tool = { - name, - description, - "x-props": { - method: method.toUpperCase(), - path, - operationId: operation.operationId, - tag: tags[0], - deprecated: operation.deprecated || false, - summary: operation.summary, - }, - inputSchema: Object.assign(Object.assign({}, schema), { additionalProperties: true, $schema: "http://json-schema.org/draft-07/schema#" }), - }; - tools.push(tool); + if (!operation || typeof operation !== "object") + continue; + const tags = Array.isArray(operation.tags) ? operation.tags : []; + if (!tags.length || !tags.some(t => typeof t === "string" && t.toLowerCase().includes(filterTag))) + continue; + try { + const tool = createToolFromOperation(path, method, operation, tags); + if (tool) { + tools.push(tool); + } + } + catch (error) { + console.error(`Error creating tool for ${method.toUpperCase()} ${path}:`, error); + continue; + } } } return tools; } /** - * Bersihkan nama agar valid untuk digunakan sebagai tool name - * - hapus karakter spesial - * - ubah slash jadi underscore - * - hilangkan prefix umum (get_, post_, api_, dll) - * - rapikan underscore berganda + * Buat MCP tool dari operation OpenAPI + */ +function createToolFromOperation(path, method, operation, tags) { + try { + const rawName = lodash_1.default.snakeCase(operation.operationId || `${method}_${path}`) || "unnamed_tool"; + const name = cleanToolName(rawName); + if (!name || name === "unnamed_tool") { + console.warn(`Invalid tool name for ${method} ${path}`); + return null; + } + const description = operation.description || + operation.summary || + `Execute ${method.toUpperCase()} ${path}`; + // โœ… Extract schema berdasarkan method + let schema; + if (method.toLowerCase() === "get") { + // โœ… Untuk GET, ambil dari parameters (query/path) + schema = extractParametersSchema(operation.parameters || []); + } + else { + // โœ… Untuk POST/PUT/etc, ambil dari requestBody + schema = extractRequestBodySchema(operation); + } + const inputSchema = createInputSchema(schema); + return { + name, + description, + "x-props": { + method: method.toUpperCase(), + path, + operationId: operation.operationId, + tag: tags[0], + deprecated: operation.deprecated || false, + summary: operation.summary, + }, + inputSchema, + }; + } + catch (error) { + console.error(`Failed to create tool from operation:`, error); + return null; + } +} +/** + * Extract schema dari parameters (untuk GET requests) + */ +function extractParametersSchema(parameters) { + var _a; + if (!Array.isArray(parameters) || parameters.length === 0) { + return null; + } + const properties = {}; + const required = []; + for (const param of parameters) { + if (!param || typeof param !== "object") + continue; + // โœ… Support path, query, dan header parameters + if (["path", "query", "header"].includes(param.in)) { + const paramName = param.name; + if (!paramName || typeof paramName !== "string") + continue; + properties[paramName] = { + type: ((_a = param.schema) === null || _a === void 0 ? void 0 : _a.type) || "string", + description: param.description || `${param.in} parameter: ${paramName}`, + }; + // โœ… Copy field tambahan dari schema + if (param.schema) { + const allowedFields = ["examples", "example", "default", "enum", "pattern", "minLength", "maxLength", "minimum", "maximum", "format"]; + for (const field of allowedFields) { + if (param.schema[field] !== undefined) { + properties[paramName][field] = param.schema[field]; + } + } + } + if (param.required === true) { + required.push(paramName); + } + } + } + if (Object.keys(properties).length === 0) { + return null; + } + return { + type: "object", + properties, + required, + }; +} +/** + * Extract schema dari requestBody (untuk POST/PUT/etc requests) + */ +function extractRequestBodySchema(operation) { + var _a, _b; + if (!((_a = operation.requestBody) === null || _a === void 0 ? void 0 : _a.content)) { + return null; + } + const content = operation.requestBody.content; + const contentTypes = [ + "application/json", + "multipart/form-data", + "application/x-www-form-urlencoded", + "text/plain", + ]; + for (const contentType of contentTypes) { + if ((_b = content[contentType]) === null || _b === void 0 ? void 0 : _b.schema) { + return content[contentType].schema; + } + } + for (const [_, value] of Object.entries(content)) { + if (value === null || value === void 0 ? void 0 : value.schema) { + return value.schema; + } + } + return null; +} +/** + * Buat input schema yang valid untuk MCP + */ +function createInputSchema(schema) { + const defaultSchema = { + type: "object", + properties: {}, + additionalProperties: false, + }; + if (!schema || typeof schema !== "object") { + return defaultSchema; + } + try { + const properties = {}; + const required = []; + const originalRequired = Array.isArray(schema.required) ? schema.required : []; + if (schema.properties && typeof schema.properties === "object") { + for (const [key, prop] of Object.entries(schema.properties)) { + if (!key || typeof key !== "string") + continue; + try { + const cleanProp = cleanProperty(prop); + if (cleanProp) { + properties[key] = cleanProp; + // โœ… PERBAIKAN: Check optional flag dengan benar + const isOptional = (prop === null || prop === void 0 ? void 0 : prop.optional) === true || (prop === null || prop === void 0 ? void 0 : prop.optional) === "true"; + const isInRequired = originalRequired.includes(key); + // โœ… Hanya masukkan ke required jika memang required DAN bukan optional + if (isInRequired && !isOptional) { + required.push(key); + } + } + } + catch (error) { + console.error(`Error cleaning property ${key}:`, error); + continue; + } + } + } + return { + type: "object", + properties, + required, + additionalProperties: false, + }; + } + catch (error) { + console.error("Error creating input schema:", error); + return defaultSchema; + } +} +/** + * Bersihkan property dari field custom + */ +function cleanProperty(prop) { + if (!prop || typeof prop !== "object") { + return { type: "string" }; + } + try { + const cleaned = { + type: prop.type || "string", + }; + const allowedFields = [ + "description", + "examples", + "example", + "default", + "enum", + "pattern", + "minLength", + "maxLength", + "minimum", + "maximum", + "format", + "multipleOf", + "exclusiveMinimum", + "exclusiveMaximum", + ]; + for (const field of allowedFields) { + if (prop[field] !== undefined && prop[field] !== null) { + cleaned[field] = prop[field]; + } + } + if (prop.properties && typeof prop.properties === "object") { + cleaned.properties = {}; + for (const [key, value] of Object.entries(prop.properties)) { + const cleanedNested = cleanProperty(value); + if (cleanedNested) { + cleaned.properties[key] = cleanedNested; + } + } + if (Array.isArray(prop.required)) { + cleaned.required = prop.required.filter((r) => typeof r === "string"); + } + } + if (prop.items) { + cleaned.items = cleanProperty(prop.items); + } + if (Array.isArray(prop.oneOf)) { + cleaned.oneOf = prop.oneOf.map(cleanProperty).filter(Boolean); + } + if (Array.isArray(prop.anyOf)) { + cleaned.anyOf = prop.anyOf.map(cleanProperty).filter(Boolean); + } + if (Array.isArray(prop.allOf)) { + cleaned.allOf = prop.allOf.map(cleanProperty).filter(Boolean); + } + return cleaned; + } + catch (error) { + console.error("Error cleaning property:", error); + return null; + } +} +/** + * Bersihkan nama tool */ function cleanToolName(name) { - return name - .replace(/[{}]/g, "") - .replace(/[^a-zA-Z0-9_]/g, "_") - .replace(/_+/g, "_") - .replace(/^_|_$/g, "") - .replace(/^(get|post|put|delete|patch|api)_/i, "") - .replace(/^(get_|post_|put_|delete_|patch_|api_)+/gi, "") - .replace(/(^_|_$)/g, ""); + if (!name || typeof name !== "string") { + return "unnamed_tool"; + } + try { + return name + .replace(/[{}]/g, "") + .replace(/[^a-zA-Z0-9_]/g, "_") + .replace(/_+/g, "_") + .replace(/^_|_$/g, "") + .replace(/^(get|post|put|delete|patch|api)_/i, "") + .replace(/^(get_|post_|put_|delete_|patch_|api_)+/gi, "") + .replace(/(^_|_$)/g, "") + || "unnamed_tool"; + } + catch (error) { + console.error("Error cleaning tool name:", error); + return "unnamed_tool"; + } } /** * Ambil OpenAPI JSON dari endpoint dan konversi ke tools MCP */ async function getMcpTools(url, filterTag) { - const data = await fetch(url); - const openApiJson = await data.json(); - const tools = convertOpenApiToMcpTools(openApiJson, filterTag); - return tools; + try { + console.log(`Fetching OpenAPI spec from: ${url}`); + const response = await fetch(url); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const openApiJson = await response.json(); + const tools = convertOpenApiToMcpTools(openApiJson, filterTag); + console.log(`โœ… Successfully generated ${tools.length} MCP tools`); + return tools; + } + catch (error) { + console.error("Error fetching MCP tools:", error); + throw error; + } } diff --git a/n8n-nodes-openapi-mcp-server/nodes/OpenapiMcpServer.js b/n8n-nodes-openapi-mcp-server/nodes/OpenapiMcpServer.js index 619b39f..25a40c7 100644 --- a/n8n-nodes-openapi-mcp-server/nodes/OpenapiMcpServer.js +++ b/n8n-nodes-openapi-mcp-server/nodes/OpenapiMcpServer.js @@ -106,15 +106,16 @@ async function handleMCPRequest(request, tools) { const baseUrl = credentials === null || credentials === void 0 ? void 0 : credentials.baseUrl; const token = credentials === null || credentials === void 0 ? void 0 : credentials.token; const result = await executeTool(tool, (params === null || params === void 0 ? void 0 : params.arguments) || {}, baseUrl, token); + const data = result.data.data; + const isObject = typeof data === "object" && data !== null; return { jsonrpc: "2.0", id, result: { content: [ - { - type: "text", - text: JSON.stringify(result, null, 2), - }, + isObject + ? { type: "json", data: data } + : { type: "text", text: JSON.stringify(data || result.data || result) }, ], }, }; diff --git a/n8n-nodes-openapi-mcp-server/package.json b/n8n-nodes-openapi-mcp-server/package.json index bff8303..8143270 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.15", + "version": "1.1.19", "keywords": [ "n8n", "n8n-nodes" diff --git a/package.json b/package.json index cad6178..1cae79b 100644 --- a/package.json +++ b/package.json @@ -1,29 +1,37 @@ - - { - "name": "n8n-mcp-server", +{ + "name": "n8n-nodes-openapi-mcp-server", "type": "module", "private": true, + "version": "1.0.1", "scripts": { - "gen": "bunx tsc && cp package.txt n8n-nodes-openapi-mcp-server/package.json && cp icon.svg n8n-nodes-openapi-mcp-server/nodes/icon.svg" - }, - "dependencies": { - "express": "^5.1.0", - "lodash": "^4.17.21", - "n8n-core": "^1.117.1", - "n8n-workflow": "^1.116.0", - "nock": "^14.0.10", - "ssh2": "^1.17.0" + "build": "bun bin/build.ts", + "publish": "bun bin/publish.ts" }, + "dependencies": {}, "devDependencies": { + "express": "^5.1.0", "@types/bun": "latest", "@types/lodash": "^4.17.20", "@types/express": "^5.0.5", "@types/node": "^24.10.0", "@types/ssh2": "^1.15.5", - "prettier": "^3.6.2", - "ts-node": "^10.9.2" + "n8n-core": "^1.117.1", + "n8n-workflow": "^1.116.0", + "nock": "^14.0.10", + "ssh2": "^1.17.0", + "dedent": "^1.7.0", + "lodash": "^4.17.21" }, "peerDependencies": { "typescript": "^5" + }, + "n8n": { + "nodes": [ + "dist/nodes/OpenapiMcpServer.js" + ], + "n8nNodesApiVersion": 1, + "credentials": [ + "dist/credentials/OpenapiMcpServerCredentials.credentials.js" + ] } } \ No newline at end of file diff --git a/src/lib/mcp_tool_convert.ts b/src/lib/mcp_tool_convert.ts index 4f46ddc..c55f2b3 100644 --- a/src/lib/mcp_tool_convert.ts +++ b/src/lib/mcp_tool_convert.ts @@ -15,57 +15,50 @@ interface McpTool { } /** - * Convert OpenAPI 3.x JSON spec into MCP-compatible tool definitions (without run()). - * Hanya menyertakan endpoint yang memiliki tag berisi "mcp". + * Convert OpenAPI 3.x JSON spec into MCP-compatible tool definitions. */ export function convertOpenApiToMcpTools(openApiJson: any, filterTag: string): McpTool[] { const tools: McpTool[] = []; + + if (!openApiJson || typeof openApiJson !== "object") { + console.warn("Invalid OpenAPI JSON"); + return tools; + } + const paths = openApiJson.paths || {}; + + if (Object.keys(paths).length === 0) { + console.warn("No paths found in OpenAPI spec"); + return tools; + } for (const [path, methods] of Object.entries(paths)) { - // โœ… skip semua path internal MCP + if (!path || typeof path !== "string") continue; if (path.startsWith("/mcp")) continue; - for (const [method, operation] of Object.entries(methods as any)) { + if (!methods || typeof methods !== "object") continue; + + for (const [method, operation] of Object.entries(methods)) { + const validMethods = ["get", "post", "put", "delete", "patch", "head", "options"]; + if (!validMethods.includes(method.toLowerCase())) continue; + + if (!operation || typeof operation !== "object") continue; + const tags: string[] = 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(filterTag))) continue; + if (!tags.length || !tags.some(t => + typeof t === "string" && t.toLowerCase().includes(filterTag) + )) continue; - const rawName = _.snakeCase(operation.operationId || `${method}_${path}`) || "unnamed_tool"; - const name = cleanToolName(rawName); - - const description = - operation.description || - operation.summary || - `Execute ${method.toUpperCase()} ${path}`; - - const schema = - operation.requestBody?.content?.["application/json"]?.schema || { - type: "object", - properties: {}, - additionalProperties: true, - }; - - const tool: McpTool = { - name, - description, - "x-props": { - method: method.toUpperCase(), - path, - operationId: operation.operationId, - tag: tags[0], - deprecated: operation.deprecated || false, - summary: operation.summary, - }, - inputSchema: { - ...schema, - additionalProperties: true, - $schema: "http://json-schema.org/draft-07/schema#", - }, - }; - - tools.push(tool); + try { + const tool = createToolFromOperation(path, method, operation, tags); + if (tool) { + tools.push(tool); + } + } catch (error) { + console.error(`Error creating tool for ${method.toUpperCase()} ${path}:`, error); + continue; + } } } @@ -73,29 +66,316 @@ export function convertOpenApiToMcpTools(openApiJson: any, filterTag: string): M } /** - * Bersihkan nama agar valid untuk digunakan sebagai tool name - * - hapus karakter spesial - * - ubah slash jadi underscore - * - hilangkan prefix umum (get_, post_, api_, dll) - * - rapikan underscore berganda + * Buat MCP tool dari operation OpenAPI + */ +function createToolFromOperation( + path: string, + method: string, + operation: any, + tags: string[] +): McpTool | null { + try { + const rawName = _.snakeCase(operation.operationId || `${method}_${path}`) || "unnamed_tool"; + const name = cleanToolName(rawName); + + if (!name || name === "unnamed_tool") { + console.warn(`Invalid tool name for ${method} ${path}`); + return null; + } + + const description = + operation.description || + operation.summary || + `Execute ${method.toUpperCase()} ${path}`; + + // โœ… Extract schema berdasarkan method + let schema; + if (method.toLowerCase() === "get") { + // โœ… Untuk GET, ambil dari parameters (query/path) + schema = extractParametersSchema(operation.parameters || []); + } else { + // โœ… Untuk POST/PUT/etc, ambil dari requestBody + schema = extractRequestBodySchema(operation); + } + + const inputSchema = createInputSchema(schema); + + return { + name, + description, + "x-props": { + method: method.toUpperCase(), + path, + operationId: operation.operationId, + tag: tags[0], + deprecated: operation.deprecated || false, + summary: operation.summary, + }, + inputSchema, + }; + } catch (error) { + console.error(`Failed to create tool from operation:`, error); + return null; + } +} + +/** + * Extract schema dari parameters (untuk GET requests) + */ +function extractParametersSchema(parameters: any[]): any { + if (!Array.isArray(parameters) || parameters.length === 0) { + return null; + } + + const properties: any = {}; + const required: string[] = []; + + for (const param of parameters) { + if (!param || typeof param !== "object") continue; + + // โœ… Support path, query, dan header parameters + if (["path", "query", "header"].includes(param.in)) { + const paramName = param.name; + if (!paramName || typeof paramName !== "string") continue; + + properties[paramName] = { + type: param.schema?.type || "string", + description: param.description || `${param.in} parameter: ${paramName}`, + }; + + // โœ… Copy field tambahan dari schema + if (param.schema) { + const allowedFields = ["examples", "example", "default", "enum", "pattern", "minLength", "maxLength", "minimum", "maximum", "format"]; + for (const field of allowedFields) { + if (param.schema[field] !== undefined) { + properties[paramName][field] = param.schema[field]; + } + } + } + + if (param.required === true) { + required.push(paramName); + } + } + } + + if (Object.keys(properties).length === 0) { + return null; + } + + return { + type: "object", + properties, + required, + }; +} + +/** + * Extract schema dari requestBody (untuk POST/PUT/etc requests) + */ +function extractRequestBodySchema(operation: any): any { + if (!operation.requestBody?.content) { + return null; + } + + const content = operation.requestBody.content; + + const contentTypes = [ + "application/json", + "multipart/form-data", + "application/x-www-form-urlencoded", + "text/plain", + ]; + + for (const contentType of contentTypes) { + if (content[contentType]?.schema) { + return content[contentType].schema; + } + } + + for (const [_, value] of Object.entries(content)) { + if (value?.schema) { + return value.schema; + } + } + + return null; +} + +/** + * Buat input schema yang valid untuk MCP + */ +function createInputSchema(schema: any): any { + const defaultSchema = { + type: "object", + properties: {}, + additionalProperties: false, + }; + + if (!schema || typeof schema !== "object") { + return defaultSchema; + } + + try { + const properties: any = {}; + const required: string[] = []; + const originalRequired = Array.isArray(schema.required) ? schema.required : []; + + if (schema.properties && typeof schema.properties === "object") { + for (const [key, prop] of Object.entries(schema.properties)) { + if (!key || typeof key !== "string") continue; + + try { + const cleanProp = cleanProperty(prop); + if (cleanProp) { + properties[key] = cleanProp; + + // โœ… PERBAIKAN: Check optional flag dengan benar + const isOptional = prop?.optional === true || prop?.optional === "true"; + const isInRequired = originalRequired.includes(key); + + // โœ… Hanya masukkan ke required jika memang required DAN bukan optional + if (isInRequired && !isOptional) { + required.push(key); + } + } + } catch (error) { + console.error(`Error cleaning property ${key}:`, error); + continue; + } + } + } + + return { + type: "object", + properties, + required, + additionalProperties: false, + }; + } catch (error) { + console.error("Error creating input schema:", error); + return defaultSchema; + } +} + +/** + * Bersihkan property dari field custom + */ +function cleanProperty(prop: any): any | null { + if (!prop || typeof prop !== "object") { + return { type: "string" }; + } + + try { + const cleaned: any = { + type: prop.type || "string", + }; + + const allowedFields = [ + "description", + "examples", + "example", + "default", + "enum", + "pattern", + "minLength", + "maxLength", + "minimum", + "maximum", + "format", + "multipleOf", + "exclusiveMinimum", + "exclusiveMaximum", + ]; + + for (const field of allowedFields) { + if (prop[field] !== undefined && prop[field] !== null) { + cleaned[field] = prop[field]; + } + } + + if (prop.properties && typeof prop.properties === "object") { + cleaned.properties = {}; + for (const [key, value] of Object.entries(prop.properties)) { + const cleanedNested = cleanProperty(value); + if (cleanedNested) { + cleaned.properties[key] = cleanedNested; + } + } + + if (Array.isArray(prop.required)) { + cleaned.required = prop.required.filter((r: any) => typeof r === "string"); + } + } + + if (prop.items) { + cleaned.items = cleanProperty(prop.items); + } + + if (Array.isArray(prop.oneOf)) { + cleaned.oneOf = prop.oneOf.map(cleanProperty).filter(Boolean); + } + if (Array.isArray(prop.anyOf)) { + cleaned.anyOf = prop.anyOf.map(cleanProperty).filter(Boolean); + } + if (Array.isArray(prop.allOf)) { + cleaned.allOf = prop.allOf.map(cleanProperty).filter(Boolean); + } + + return cleaned; + } catch (error) { + console.error("Error cleaning property:", error); + return null; + } +} + +/** + * Bersihkan nama tool */ function cleanToolName(name: string): string { - return name - .replace(/[{}]/g, "") - .replace(/[^a-zA-Z0-9_]/g, "_") - .replace(/_+/g, "_") - .replace(/^_|_$/g, "") - .replace(/^(get|post|put|delete|patch|api)_/i, "") - .replace(/^(get_|post_|put_|delete_|patch_|api_)+/gi, "") - .replace(/(^_|_$)/g, ""); + if (!name || typeof name !== "string") { + return "unnamed_tool"; + } + + try { + return name + .replace(/[{}]/g, "") + .replace(/[^a-zA-Z0-9_]/g, "_") + .replace(/_+/g, "_") + .replace(/^_|_$/g, "") + .replace(/^(get|post|put|delete|patch|api)_/i, "") + .replace(/^(get_|post_|put_|delete_|patch_|api_)+/gi, "") + .replace(/(^_|_$)/g, "") + || "unnamed_tool"; + } catch (error) { + console.error("Error cleaning tool name:", error); + return "unnamed_tool"; + } } /** * Ambil OpenAPI JSON dari endpoint dan konversi ke tools MCP */ -export async function getMcpTools(url: string, filterTag: string) { - const data = await fetch(url); - const openApiJson = await data.json(); - const tools = convertOpenApiToMcpTools(openApiJson, filterTag); - return tools; +export async function getMcpTools(url: string, filterTag: string): Promise { + try { + + console.log(`Fetching OpenAPI spec from: ${url}`); + + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const openApiJson = await response.json(); + const tools = convertOpenApiToMcpTools(openApiJson, filterTag); + + console.log(`โœ… Successfully generated ${tools.length} MCP tools`); + + return tools; + } catch (error) { + console.error("Error fetching MCP tools:", error); + throw error; + } } + diff --git a/src/nodes/OpenapiMcpServer.ts b/src/nodes/OpenapiMcpServer.ts index 26c697b..1f50202 100644 --- a/src/nodes/OpenapiMcpServer.ts +++ b/src/nodes/OpenapiMcpServer.ts @@ -172,15 +172,17 @@ async function handleMCPRequest( token ); + const data = result.data.data; + const isObject = typeof data === "object" && data !== null; + return { jsonrpc: "2.0", id, result: { content: [ - { - type: "text", - text: JSON.stringify(result, null, 2), - }, + isObject + ? { type: "json", data: data } + : { type: "text", text: JSON.stringify(data || result.data || result) }, ], }, }; diff --git a/src/nodes/icon.svg b/src/nodes/icon.svg index 9cd22ab..aea0880 100644 --- a/src/nodes/icon.svg +++ b/src/nodes/icon.svg @@ -1,17 +1,17 @@ - - + - - - - - mcp + MCP - - \ No newline at end of file + + \ No newline at end of file diff --git a/x.sh b/x.sh new file mode 100644 index 0000000..bf4d769 --- /dev/null +++ b/x.sh @@ -0,0 +1,2 @@ +curl -X POST "https://n8n.wibudev.com/form/82848bc4-5ea2-4e5a-8bb6-3c09b94a8c5d" \ + -F "file=@/Users/bip/Downloads/bank_data_darmasaba.pdf"