update version
This commit is contained in:
@@ -1,361 +1,334 @@
|
|||||||
// mcp_tool_convert.ts
|
import type {
|
||||||
import _ from "lodash";
|
INodeType,
|
||||||
|
INodeTypeDescription,
|
||||||
|
IExecuteFunctions,
|
||||||
|
ILoadOptionsFunctions,
|
||||||
|
INodePropertyOptions,
|
||||||
|
} from "n8n-workflow";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
/**
|
interface OpenAPISpec {
|
||||||
* ============================
|
paths: Record<string, any>;
|
||||||
* Types
|
components?: Record<string, any>;
|
||||||
* ============================
|
servers?: Array<{ url: string }>;
|
||||||
*/
|
}
|
||||||
interface McpTool {
|
|
||||||
name: string;
|
const openApiCache: Map<string, OpenAPISpec> = new Map();
|
||||||
description: string;
|
|
||||||
inputSchema: any;
|
async function loadOpenAPISpecCached(url: string): Promise<OpenAPISpec> {
|
||||||
"x-props": {
|
if (openApiCache.has(url)) return openApiCache.get(url)!;
|
||||||
method: string;
|
const res = await axios.get(url, { timeout: 20000 });
|
||||||
path: string;
|
const spec: OpenAPISpec = res.data;
|
||||||
operationId?: string;
|
openApiCache.set(url, spec);
|
||||||
tag?: string;
|
return spec;
|
||||||
deprecated?: boolean;
|
}
|
||||||
summary?: string;
|
|
||||||
parameters?: any[];
|
function getDefaultValue(type: string | undefined, schema?: any): any {
|
||||||
|
if (schema?.example !== undefined) return schema.example;
|
||||||
|
if (schema?.default !== undefined) return schema.default;
|
||||||
|
switch (type) {
|
||||||
|
case "string": return "";
|
||||||
|
case "number":
|
||||||
|
case "integer": return 0;
|
||||||
|
case "boolean": return false;
|
||||||
|
case "array": return [];
|
||||||
|
case "object": return {};
|
||||||
|
default: return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRef(obj: any, spec: OpenAPISpec): any {
|
||||||
|
if (!obj || typeof obj !== "object") return obj;
|
||||||
|
if (obj.$ref && typeof obj.$ref === "string" && obj.$ref.startsWith("#/")) {
|
||||||
|
const refPath = obj.$ref.replace(/^#\//, "").split("/");
|
||||||
|
let cur: any = spec;
|
||||||
|
for (const seg of refPath) cur = cur?.[seg];
|
||||||
|
return cur || obj;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OpenApiNode implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: "OpenApiNode - Dynamic OpenAPI",
|
||||||
|
name: "openApiNode",
|
||||||
|
icon: "file:icon.svg",
|
||||||
|
group: ["transform"],
|
||||||
|
version: 1,
|
||||||
|
subtitle: "={{$parameter['operation'] || 'select operation'}}",
|
||||||
|
description: "Dynamic UI n8n node generated from OpenAPI",
|
||||||
|
defaults: { name: "OpenApiNode" },
|
||||||
|
inputs: ["main"],
|
||||||
|
outputs: ["main"],
|
||||||
|
credentials: [
|
||||||
|
{ name: "openApiNodeApi", required: false },
|
||||||
|
],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: "OpenAPI JSON URL",
|
||||||
|
name: "openapiUrl",
|
||||||
|
type: "string",
|
||||||
|
default: "",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: "Operation",
|
||||||
|
name: "operation",
|
||||||
|
type: "options",
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsMethod: "loadOperations",
|
||||||
|
loadOptionsDependsOn: ["openapiUrl"],
|
||||||
|
},
|
||||||
|
default: "",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: "Parameters (Form)",
|
||||||
|
name: "parametersForm",
|
||||||
|
type: "fixedCollection",
|
||||||
|
placeholder: "Add Parameter",
|
||||||
|
typeOptions: { multipleValues: true },
|
||||||
|
default: {},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: "parameter",
|
||||||
|
displayName: "Parameter",
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
displayName: "Name",
|
||||||
|
name: "name",
|
||||||
|
type: "options",
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsMethod: "loadParameterNames",
|
||||||
|
loadOptionsDependsOn: ["openapiUrl", "operation"],
|
||||||
|
},
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: "In",
|
||||||
|
name: "in",
|
||||||
|
type: "options",
|
||||||
|
options: [
|
||||||
|
{ name: "query", value: "query" },
|
||||||
|
{ name: "path", value: "path" },
|
||||||
|
{ name: "header", value: "header" },
|
||||||
|
{ name: "body", value: "body" },
|
||||||
|
],
|
||||||
|
default: "query",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: "Value",
|
||||||
|
name: "value",
|
||||||
|
type: "string",
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
methods = {
|
||||||
* ============================
|
loadOptions: {
|
||||||
* Public: convertOpenApiToMcpTools
|
async loadOperations(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||||
* ============================
|
const url = (this.getNodeParameter("openapiUrl", "") as string).trim();
|
||||||
* Convert OpenAPI 3.x spec → MCP Tools
|
if (!url) return [{ name: "Provide openapiUrl first", value: "" }];
|
||||||
* - filterTag : match against operation tags
|
try {
|
||||||
*/
|
const spec = await loadOpenAPISpecCached(url);
|
||||||
export function convertOpenApiToMcpTools(openApiJson: any, filterTag: string): McpTool[] {
|
const ops: INodePropertyOptions[] = [];
|
||||||
const tools: McpTool[] = [];
|
|
||||||
|
|
||||||
if (!openApiJson || typeof openApiJson !== "object") {
|
for (const path of Object.keys(spec.paths || {})) {
|
||||||
console.warn("Invalid OpenAPI JSON");
|
const pathObj = spec.paths[path];
|
||||||
return tools;
|
for (const method of Object.keys(pathObj || {})) {
|
||||||
}
|
const op = pathObj[method];
|
||||||
|
const opId =
|
||||||
|
op.operationId ||
|
||||||
|
`${method}_${path.replace(/[{}]/g, "").replace(/\//g, "_")}`;
|
||||||
|
const title = (op.summary || opId).toString();
|
||||||
|
ops.push({
|
||||||
|
name: `${title} — ${method.toUpperCase()} ${path}`,
|
||||||
|
value: opId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ops;
|
||||||
|
} catch (err: any) {
|
||||||
|
return [{ name: `Error: ${err.message}`, value: "" }];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
const paths = openApiJson.paths || {};
|
async loadParameterNames(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||||
if (Object.keys(paths).length === 0) {
|
const url = (this.getNodeParameter("openapiUrl", "") as string).trim();
|
||||||
console.warn("No paths found in OpenAPI spec");
|
const opId = (this.getNodeParameter("operation", "") as string).trim();
|
||||||
return tools;
|
if (!url || !opId) return [{ name: "Select operation first", value: "" }];
|
||||||
}
|
|
||||||
|
|
||||||
for (const [path, methods] of Object.entries<any>(paths)) {
|
try {
|
||||||
if (!methods || typeof methods !== "object") continue;
|
const spec = await loadOpenAPISpecCached(url);
|
||||||
|
const out: INodePropertyOptions[] = [];
|
||||||
|
|
||||||
for (const [method, operation] of Object.entries<any>(methods)) {
|
outer: for (const path of Object.keys(spec.paths || {})) {
|
||||||
const valid = ["get", "post", "put", "delete", "patch", "head", "options"];
|
const pathObj = spec.paths[path];
|
||||||
if (!valid.includes(method.toLowerCase())) continue;
|
for (const method of Object.keys(pathObj || {})) {
|
||||||
|
const op = pathObj[method];
|
||||||
|
const id =
|
||||||
|
op.operationId ||
|
||||||
|
`${method}_${path.replace(/[{}]/g, "").replace(/\//g, "_")}`;
|
||||||
|
if (id !== opId) continue;
|
||||||
|
|
||||||
if (!operation || typeof operation !== "object") continue;
|
const params = [
|
||||||
|
...(pathObj.parameters || []),
|
||||||
|
...(op.parameters || []),
|
||||||
|
];
|
||||||
|
|
||||||
const tags = Array.isArray(operation.tags) ? operation.tags : [];
|
const seen = new Set<string>();
|
||||||
|
|
||||||
// Tag filter
|
for (const p of params) {
|
||||||
if (
|
if (!seen.has(p.name)) {
|
||||||
filterTag &&
|
seen.add(p.name);
|
||||||
(!tags.length ||
|
out.push({
|
||||||
!tags.some((t: string) =>
|
name: `${p.name} (in=${p.in})`,
|
||||||
t?.toLowerCase().includes(filterTag.toLowerCase())
|
value: p.name,
|
||||||
))
|
});
|
||||||
) {
|
}
|
||||||
continue;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
const bodySchema =
|
||||||
const tool = createToolFromOperation(path, method, operation, tags);
|
op.requestBody?.content?.["application/json"]?.schema;
|
||||||
if (tool) tools.push(tool);
|
if (bodySchema) {
|
||||||
} catch (err) {
|
const resolved = resolveRef(bodySchema, spec);
|
||||||
console.error(`Error building tool for ${method.toUpperCase()} ${path}`, err);
|
const props = resolved.properties || {};
|
||||||
}
|
for (const propName of Object.keys(props)) {
|
||||||
}
|
if (!seen.has(propName)) {
|
||||||
}
|
seen.add(propName);
|
||||||
|
out.push({
|
||||||
|
name: `${propName} (body)`,
|
||||||
|
value: propName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return tools;
|
break outer;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
return out;
|
||||||
* ============================
|
} catch (err: any) {
|
||||||
* Build Tool from Operation
|
return [{ name: `Error: ${err.message}`, value: "" }];
|
||||||
* ============================
|
}
|
||||||
*/
|
},
|
||||||
function createToolFromOperation(
|
|
||||||
path: string,
|
|
||||||
method: string,
|
|
||||||
operation: any,
|
|
||||||
tags: string[]
|
|
||||||
): McpTool | null {
|
|
||||||
const rawName = _.snakeCase(operation.operationId || `${method}_${path}`) || "unnamed_tool";
|
|
||||||
const name = cleanToolName(rawName);
|
|
||||||
|
|
||||||
if (name === "unnamed_tool") {
|
|
||||||
console.warn(`Invalid tool name: ${method} ${path}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const description =
|
|
||||||
operation.description ||
|
|
||||||
operation.summary ||
|
|
||||||
`Execute ${method.toUpperCase()} ${path}`;
|
|
||||||
|
|
||||||
// Build executor parameter array
|
|
||||||
const parameters: any[] = [];
|
|
||||||
|
|
||||||
if (Array.isArray(operation.parameters)) {
|
|
||||||
for (const p of operation.parameters) {
|
|
||||||
if (!p || typeof p !== "object") continue;
|
|
||||||
|
|
||||||
parameters.push({
|
|
||||||
name: p.name,
|
|
||||||
in: p.in,
|
|
||||||
required: !!p.required,
|
|
||||||
description: p.description,
|
|
||||||
schema: p.schema || { type: "string" },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Synthetic requestBody param
|
|
||||||
if (operation.requestBody?.content) {
|
|
||||||
const schema = extractPreferredContentSchema(operation.requestBody.content);
|
|
||||||
|
|
||||||
parameters.push({
|
|
||||||
name: "body",
|
|
||||||
in: "requestBody",
|
|
||||||
required: !!operation.requestBody.required,
|
|
||||||
schema: schema || { type: "object" },
|
|
||||||
description: operation.requestBody.description || "Request body",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build input schema
|
|
||||||
let schema: any = null;
|
|
||||||
|
|
||||||
const lower = method.toLowerCase();
|
|
||||||
if (["get", "delete", "head"].includes(lower)) {
|
|
||||||
schema = extractParametersSchema(operation.parameters || []);
|
|
||||||
} else {
|
|
||||||
schema = extractRequestBodySchema(operation) ||
|
|
||||||
extractParametersSchema(operation.parameters || []);
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
parameters,
|
|
||||||
},
|
},
|
||||||
inputSchema,
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
async execute(this: IExecuteFunctions) {
|
||||||
* ============================
|
const items = this.getInputData();
|
||||||
* Extract Preferred Content Schema
|
const returnData: any[] = [];
|
||||||
* ============================
|
|
||||||
*/
|
|
||||||
function extractPreferredContentSchema(content: any): any {
|
|
||||||
if (!content) return null;
|
|
||||||
|
|
||||||
const preferred = [
|
const creds = (await this.getCredentials?.("openApiNodeApi")) as
|
||||||
"application/json",
|
| { baseUrl?: string; token?: string; apiKey?: string }
|
||||||
"multipart/form-data",
|
| undefined;
|
||||||
"application/x-www-form-urlencoded",
|
|
||||||
"text/plain",
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const type of preferred) {
|
for (let i = 0; i < items.length; i++) {
|
||||||
if (content[type]?.schema) return content[type].schema;
|
try {
|
||||||
}
|
const openapiUrl = this.getNodeParameter("openapiUrl", i, "") as string;
|
||||||
|
const operationId = this.getNodeParameter("operation", i, "") as string;
|
||||||
|
const spec = await loadOpenAPISpecCached(openapiUrl);
|
||||||
|
|
||||||
const first = Object.values<any>(content)[0];
|
let foundOp: any = null;
|
||||||
return first?.schema || null;
|
let foundPath = "";
|
||||||
}
|
let foundMethod = "";
|
||||||
|
|
||||||
/**
|
outer: for (const path of Object.keys(spec.paths || {})) {
|
||||||
* ============================
|
const pathObj = spec.paths[path];
|
||||||
* Extract Parameter Schema (GET/DELETE)
|
for (const method of Object.keys(pathObj || {})) {
|
||||||
* ============================
|
const op = pathObj[method];
|
||||||
*/
|
const id =
|
||||||
function extractParametersSchema(parameters: any[]): any | null {
|
op.operationId ||
|
||||||
if (!parameters.length) return null;
|
`${method}_${path.replace(/[{}]/g, "").replace(/\//g, "_")}`;
|
||||||
|
if (id === operationId) {
|
||||||
|
foundOp = op;
|
||||||
|
foundPath = path;
|
||||||
|
foundMethod = method.toLowerCase();
|
||||||
|
break outer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const properties: any = {};
|
if (!foundOp) throw new Error("Operation not found");
|
||||||
const required: string[] = [];
|
|
||||||
|
|
||||||
for (const param of parameters) {
|
const paramsForm = this.getNodeParameter("parametersForm", i, {}) as any;
|
||||||
if (!["path", "query", "header"].includes(param.in)) continue;
|
let queryParams: Record<string, any> = {};
|
||||||
|
let bodyParams: Record<string, any> = {};
|
||||||
|
|
||||||
const name = param.name;
|
const pathParams = [
|
||||||
if (!name) continue;
|
...(spec.paths[foundPath].parameters || []),
|
||||||
|
...(foundOp.parameters || []),
|
||||||
|
];
|
||||||
|
|
||||||
const schema = param.schema || { type: "string" };
|
if (paramsForm && Array.isArray(paramsForm.parameter)) {
|
||||||
|
for (const p of paramsForm.parameter) {
|
||||||
|
if (p.in === "query") queryParams[p.name] = p.value;
|
||||||
|
else if (p.in === "body") bodyParams[p.name] = p.value;
|
||||||
|
else if (p.in === "header") queryParams[p.name] = p.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
properties[name] = {
|
let baseUrl =
|
||||||
type: schema.type || "string",
|
creds?.baseUrl ||
|
||||||
description: param.description || `${param.in} parameter: ${name}`,
|
spec.servers?.[0]?.url ||
|
||||||
...extractSchemaDetails(schema),
|
"";
|
||||||
};
|
baseUrl = baseUrl.replace(/\/+$/, "");
|
||||||
|
|
||||||
if (param.required) required.push(name);
|
let url = baseUrl + foundPath;
|
||||||
}
|
|
||||||
|
|
||||||
if (!Object.keys(properties).length) return null;
|
for (const p of pathParams) {
|
||||||
|
if (p.in === "path") {
|
||||||
|
const item = paramsForm.parameter.find((x: any) => x.name === p.name);
|
||||||
|
const val = item?.value;
|
||||||
|
if (!val) throw new Error(`Missing path param ${p.name}`);
|
||||||
|
url = url.replace(`{${p.name}}`, encodeURIComponent(val));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { type: "object", properties, required };
|
const headers: Record<string, any> = {
|
||||||
}
|
"Content-Type": "application/json",
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
if (creds?.token) headers["Authorization"] = `Bearer ${creds.token}`;
|
||||||
* ============================
|
if (creds?.apiKey) headers["X-API-Key"] = creds.apiKey;
|
||||||
* Extract RequestBody Schema
|
|
||||||
* ============================
|
|
||||||
*/
|
|
||||||
function extractRequestBodySchema(operation: any): any | null {
|
|
||||||
return extractPreferredContentSchema(operation?.requestBody?.content);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
const axiosConfig: any = {
|
||||||
* ============================
|
method: foundMethod,
|
||||||
* Create MCP Input Schema
|
url,
|
||||||
* ============================
|
headers,
|
||||||
*/
|
params: queryParams,
|
||||||
function createInputSchema(schema: any): any {
|
timeout: 30000,
|
||||||
if (!schema || typeof schema !== "object") {
|
};
|
||||||
return { type: "object", properties: {}, additionalProperties: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
const properties: any = {};
|
// === FIX: GET & HEAD tidak boleh punya body ===
|
||||||
const required: string[] = Array.isArray(schema.required) ? [...schema.required] : [];
|
if (!(foundMethod === "get" || foundMethod === "head")) {
|
||||||
|
axiosConfig.data = Object.keys(bodyParams).length ? bodyParams : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
if (schema.properties) {
|
const resp = await axios(axiosConfig);
|
||||||
for (const [key, prop] of Object.entries<any>(schema.properties)) {
|
returnData.push({ json: resp.data });
|
||||||
const cleaned = cleanProperty(prop);
|
|
||||||
if (cleaned) properties[key] = cleaned;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (schema.type === "array" && schema.items) {
|
} catch (err: any) {
|
||||||
properties.items = cleanProperty(schema.items) || { type: "string" };
|
if (this.continueOnFail && this.continueOnFail()) {
|
||||||
}
|
returnData.push({
|
||||||
|
json: {
|
||||||
return {
|
error: true,
|
||||||
type: "object",
|
message: err.message,
|
||||||
properties,
|
response: err.response?.data,
|
||||||
required,
|
},
|
||||||
additionalProperties: false,
|
});
|
||||||
};
|
continue;
|
||||||
}
|
}
|
||||||
|
throw err;
|
||||||
/**
|
}
|
||||||
* ============================
|
|
||||||
* Clean Individual Schema Property
|
|
||||||
* ============================
|
|
||||||
*/
|
|
||||||
function cleanProperty(prop: any): any | null {
|
|
||||||
if (!prop || typeof prop !== "object") return { type: "string" };
|
|
||||||
|
|
||||||
const out: any = { type: prop.type || "string" };
|
|
||||||
|
|
||||||
Object.assign(out, extractSchemaDetails(prop));
|
|
||||||
|
|
||||||
if (prop.properties) {
|
|
||||||
out.properties = {};
|
|
||||||
for (const [k, v] of Object.entries<any>(prop.properties)) {
|
|
||||||
const cleaned = cleanProperty(v);
|
|
||||||
if (cleaned) out.properties[k] = cleaned;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(prop.required)) {
|
return [returnData];
|
||||||
out.required = prop.required.filter((r: any) => typeof r === "string");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prop.items) {
|
|
||||||
out.items = cleanProperty(prop.items);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(prop.oneOf)) out.oneOf = prop.oneOf.map(cleanProperty);
|
|
||||||
if (Array.isArray(prop.anyOf)) out.anyOf = prop.anyOf.map(cleanProperty);
|
|
||||||
if (Array.isArray(prop.allOf)) out.allOf = prop.allOf.map(cleanProperty);
|
|
||||||
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ============================
|
|
||||||
* Extract Allowed Schema Fields
|
|
||||||
* ============================
|
|
||||||
*/
|
|
||||||
function extractSchemaDetails(schema: any) {
|
|
||||||
const allowed = [
|
|
||||||
"description",
|
|
||||||
"examples",
|
|
||||||
"example",
|
|
||||||
"default",
|
|
||||||
"enum",
|
|
||||||
"pattern",
|
|
||||||
"minLength",
|
|
||||||
"maxLength",
|
|
||||||
"minimum",
|
|
||||||
"maximum",
|
|
||||||
"format",
|
|
||||||
"multipleOf",
|
|
||||||
"exclusiveMinimum",
|
|
||||||
"exclusiveMaximum",
|
|
||||||
];
|
|
||||||
|
|
||||||
const out: any = {};
|
|
||||||
for (const f of allowed) {
|
|
||||||
if (schema[f] !== undefined) out[f] = schema[f];
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ============================
|
|
||||||
* Clean tool name safely
|
|
||||||
* ============================
|
|
||||||
*/
|
|
||||||
function cleanToolName(value: string): string {
|
|
||||||
if (!value) return "unnamed_tool";
|
|
||||||
|
|
||||||
return value
|
|
||||||
.replace(/[{}]/g, "")
|
|
||||||
.replace(/[^a-zA-Z0-9_]/g, "_")
|
|
||||||
.replace(/_+/g, "_")
|
|
||||||
.replace(/^_|_$/g, "")
|
|
||||||
.toLowerCase() || "unnamed_tool";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ============================
|
|
||||||
* Public: getMcpTools
|
|
||||||
* ============================
|
|
||||||
*/
|
|
||||||
export async function getMcpTools(url: string, filterTag: string): Promise<McpTool[]> {
|
|
||||||
try {
|
|
||||||
console.log(`Fetching OpenAPI spec: ${url}`);
|
|
||||||
|
|
||||||
const res = await fetch(url);
|
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
||||||
|
|
||||||
const json = await res.json();
|
|
||||||
const tools = convertOpenApiToMcpTools(json, filterTag);
|
|
||||||
|
|
||||||
console.log(`Generated ${tools.length} MCP tools`);
|
|
||||||
return tools;
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error fetching MCP Tools:", err);
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "n8n-nodes-openapi-node",
|
"name": "n8n-nodes-openapi-node",
|
||||||
"version": "1.0.5",
|
"version": "1.0.6",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"n8n",
|
"n8n",
|
||||||
"n8n-nodes"
|
"n8n-nodes"
|
||||||
|
|||||||
Reference in New Issue
Block a user