build
This commit is contained in:
@@ -18,12 +18,11 @@ const toolsCache = new Map<string, CachedTools>();
|
|||||||
|
|
||||||
// ======================================================
|
// ======================================================
|
||||||
// Load OpenAPI → MCP Tools
|
// Load OpenAPI → MCP Tools
|
||||||
// (preserves original function name loadTools)
|
// - preserves function name loadTools (do not rename)
|
||||||
// NOTE: filterTag now supports string | string[] (multi-select)
|
// - adds TTL, forceRefresh handling, and robust error handling
|
||||||
// ======================================================
|
// ======================================================
|
||||||
async function loadTools(openapiUrl: string, filterTag: string | string[] = "", forceRefresh = false): Promise<any[]> {
|
async function loadTools(openapiUrl: string, filterTag: string, forceRefresh = false): Promise<any[]> {
|
||||||
const normalizedFilterKey = Array.isArray(filterTag) ? filterTag.join("|") : (filterTag ?? "");
|
const cacheKey = `${openapiUrl}::${filterTag || ""}`;
|
||||||
const cacheKey = `${openapiUrl}::${normalizedFilterKey}`;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const cached = toolsCache.get(cacheKey);
|
const cached = toolsCache.get(cacheKey);
|
||||||
@@ -31,9 +30,8 @@ async function loadTools(openapiUrl: string, filterTag: string | string[] = "",
|
|||||||
return cached.tools;
|
return cached.tools;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[MCP] 🔄 Refreshing tools from ${openapiUrl} with filter '${normalizedFilterKey}' ...`);
|
console.log(`[MCP] 🔄 Refreshing tools from ${openapiUrl} ...`);
|
||||||
// Pass through filterTag in original shape (string | string[]) to getMcpTools.
|
const fetched = await getMcpTools(openapiUrl, filterTag);
|
||||||
const fetched = await getMcpTools(openapiUrl, filterTag as any);
|
|
||||||
|
|
||||||
console.log(`[MCP] ✅ Loaded ${fetched.length} tools`);
|
console.log(`[MCP] ✅ Loaded ${fetched.length} tools`);
|
||||||
if (fetched.length > 0) {
|
if (fetched.length > 0) {
|
||||||
@@ -44,6 +42,7 @@ async function loadTools(openapiUrl: string, filterTag: string | string[] = "",
|
|||||||
return fetched;
|
return fetched;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[MCP] Failed to load tools from ${openapiUrl}:`, err);
|
console.error(`[MCP] Failed to load tools from ${openapiUrl}:`, err);
|
||||||
|
// On failure, if cache exists return stale to avoid complete outage
|
||||||
const stale = toolsCache.get(cacheKey);
|
const stale = toolsCache.get(cacheKey);
|
||||||
if (stale) {
|
if (stale) {
|
||||||
console.warn(`[MCP] Returning stale cached tools for ${cacheKey}`);
|
console.warn(`[MCP] Returning stale cached tools for ${cacheKey}`);
|
||||||
@@ -73,7 +72,9 @@ type JSONRPCResponse = {
|
|||||||
|
|
||||||
// ======================================================
|
// ======================================================
|
||||||
// EXECUTE TOOL — SUPPORT PATH, QUERY, HEADER, BODY, COOKIE
|
// EXECUTE TOOL — SUPPORT PATH, QUERY, HEADER, BODY, COOKIE
|
||||||
// (preserves function name executeTool)
|
// - preserves function name executeTool
|
||||||
|
// - fixes cookie accumulation, query-array handling, path param safety,
|
||||||
|
// requestBody handling based on x.parameters + synthetic body param
|
||||||
// ======================================================
|
// ======================================================
|
||||||
async function executeTool(
|
async function executeTool(
|
||||||
tool: any,
|
tool: any,
|
||||||
@@ -91,42 +92,61 @@ async function executeTool(
|
|||||||
|
|
||||||
const query: Record<string, any> = {};
|
const query: Record<string, any> = {};
|
||||||
const headers: Record<string, any> = {
|
const headers: Record<string, any> = {
|
||||||
|
// default content-type; may be overridden by header params or request
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Support multiple cookies: accumulate into array, then join
|
||||||
const cookies: string[] = [];
|
const cookies: string[] = [];
|
||||||
|
|
||||||
let bodyPayload: any = undefined;
|
let bodyPayload: any = undefined;
|
||||||
|
|
||||||
|
// x.parameters may have been produced by converter.
|
||||||
|
// Expected shape: [{ name, in, schema?, required? }]
|
||||||
if (Array.isArray(x.parameters)) {
|
if (Array.isArray(x.parameters)) {
|
||||||
for (const p of x.parameters) {
|
for (const p of x.parameters) {
|
||||||
const name = p.name;
|
const name = p.name;
|
||||||
|
// allow alias e.g. body parameter named "__body" or "body"
|
||||||
const value = args?.[name];
|
const value = args?.[name];
|
||||||
|
|
||||||
|
// If param not provided, skip (unless required, leave to tool to validate later)
|
||||||
if (value === undefined) continue;
|
if (value === undefined) continue;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
switch (p.in) {
|
switch (p.in) {
|
||||||
case "path":
|
case "path":
|
||||||
|
// Safely replace only if placeholder exists
|
||||||
if (path.includes(`{${name}}`)) {
|
if (path.includes(`{${name}}`)) {
|
||||||
path = path.replace(new RegExp(`{${name}}`, "g"), encodeURIComponent(String(value)));
|
path = path.replace(new RegExp(`{${name}}`, "g"), encodeURIComponent(String(value)));
|
||||||
} else {
|
} else {
|
||||||
|
// If path doesn't contain placeholder, append as query fallback
|
||||||
query[name] = value;
|
query[name] = value;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "query":
|
case "query":
|
||||||
|
// handle array correctly: produce repeated keys for URLSearchParams
|
||||||
|
// Store as-is and handle later when building QS
|
||||||
query[name] = value;
|
query[name] = value;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "header":
|
case "header":
|
||||||
headers[name] = value;
|
headers[name] = value;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "cookie":
|
case "cookie":
|
||||||
cookies.push(`${name}=${value}`);
|
cookies.push(`${name}=${value}`);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "body":
|
case "body":
|
||||||
case "requestBody":
|
case "requestBody":
|
||||||
|
// prefer explicit body param; overwrite if multiple present
|
||||||
bodyPayload = value;
|
bodyPayload = value;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
// unknown param location — put into body as fallback
|
||||||
bodyPayload = bodyPayload ?? {};
|
bodyPayload = bodyPayload ?? {};
|
||||||
bodyPayload[name] = value;
|
bodyPayload[name] = value;
|
||||||
break;
|
break;
|
||||||
@@ -136,6 +156,7 @@ async function executeTool(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// fallback → semua args dianggap body
|
||||||
bodyPayload = args;
|
bodyPayload = args;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,10 +164,15 @@ async function executeTool(
|
|||||||
headers["Cookie"] = cookies.join("; ");
|
headers["Cookie"] = cookies.join("; ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ======================================================
|
||||||
|
// Build Final URL
|
||||||
|
// ======================================================
|
||||||
|
// Ensure baseUrl doesn't end with duplicate slashes
|
||||||
const normalizedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
const normalizedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
||||||
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
||||||
let url = `${normalizedBase}${normalizedPath}`;
|
let url = `${normalizedBase}${normalizedPath}`;
|
||||||
|
|
||||||
|
// Build query string with repeated keys if array provided
|
||||||
const qsParts: string[] = [];
|
const qsParts: string[] = [];
|
||||||
for (const [k, v] of Object.entries(query)) {
|
for (const [k, v] of Object.entries(query)) {
|
||||||
if (v === undefined || v === null) continue;
|
if (v === undefined || v === null) continue;
|
||||||
@@ -155,6 +181,7 @@ async function executeTool(
|
|||||||
qsParts.push(`${encodeURIComponent(k)}=${encodeURIComponent(String(item))}`);
|
qsParts.push(`${encodeURIComponent(k)}=${encodeURIComponent(String(item))}`);
|
||||||
}
|
}
|
||||||
} else if (typeof v === "object") {
|
} else if (typeof v === "object") {
|
||||||
|
// JSON-encode objects as value
|
||||||
qsParts.push(`${encodeURIComponent(k)}=${encodeURIComponent(JSON.stringify(v))}`);
|
qsParts.push(`${encodeURIComponent(k)}=${encodeURIComponent(JSON.stringify(v))}`);
|
||||||
} else {
|
} else {
|
||||||
qsParts.push(`${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`);
|
qsParts.push(`${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`);
|
||||||
@@ -162,20 +189,28 @@ async function executeTool(
|
|||||||
}
|
}
|
||||||
if (qsParts.length) url += `?${qsParts.join("&")}`;
|
if (qsParts.length) url += `?${qsParts.join("&")}`;
|
||||||
|
|
||||||
|
// ======================================================
|
||||||
|
// Build Request Options
|
||||||
|
// ======================================================
|
||||||
const opts: RequestInit & { headers: Record<string, any> } = { method, headers };
|
const opts: RequestInit & { headers: Record<string, any> } = { method, headers };
|
||||||
|
// If content-type is form data, adjust accordingly (converter could mark)
|
||||||
const contentType = headers["Content-Type"]?.toLowerCase() ?? "";
|
const contentType = headers["Content-Type"]?.toLowerCase() ?? "";
|
||||||
|
|
||||||
if (["POST", "PUT", "PATCH", "DELETE"].includes(method) && bodyPayload !== undefined) {
|
if (["POST", "PUT", "PATCH", "DELETE"].includes(method) && bodyPayload !== undefined) {
|
||||||
|
// If requestBody is already a FormData-like or flagged in x (converter support),
|
||||||
|
// caller could pass a special object { __formdata: true, entries: [...] } — support minimal
|
||||||
if (bodyPayload && bodyPayload.__formdata === true && Array.isArray(bodyPayload.entries)) {
|
if (bodyPayload && bodyPayload.__formdata === true && Array.isArray(bodyPayload.entries)) {
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
for (const [k, v] of bodyPayload.entries) {
|
for (const [k, v] of bodyPayload.entries) {
|
||||||
form.append(k, v);
|
form.append(k, v);
|
||||||
}
|
}
|
||||||
|
// Let fetch set Content-Type with boundary
|
||||||
delete opts.headers["Content-Type"];
|
delete opts.headers["Content-Type"];
|
||||||
opts.body = (form as any) as BodyInit;
|
opts.body = (form as any) as BodyInit;
|
||||||
} else if (contentType.includes("application/x-www-form-urlencoded")) {
|
} else if (contentType.includes("application/x-www-form-urlencoded")) {
|
||||||
opts.body = new URLSearchParams(bodyPayload).toString();
|
opts.body = new URLSearchParams(bodyPayload).toString();
|
||||||
} else {
|
} else {
|
||||||
|
// default JSON
|
||||||
opts.body = JSON.stringify(bodyPayload);
|
opts.body = JSON.stringify(bodyPayload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -196,13 +231,14 @@ async function executeTool(
|
|||||||
url,
|
url,
|
||||||
path,
|
path,
|
||||||
data,
|
data,
|
||||||
headers: res.headers,
|
headers: res.headers, // keep for diagnostics
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================================================
|
// ======================================================
|
||||||
// JSON-RPC Handler
|
// JSON-RPC Handler
|
||||||
// (preserves function name handleMCPRequest)
|
// - preserves handleMCPRequest name
|
||||||
|
// - improved error reporting, robust content conversion, batch safety
|
||||||
// ======================================================
|
// ======================================================
|
||||||
async function handleMCPRequest(
|
async function handleMCPRequest(
|
||||||
request: JSONRPCRequest,
|
request: JSONRPCRequest,
|
||||||
@@ -210,6 +246,7 @@ async function handleMCPRequest(
|
|||||||
): Promise<JSONRPCResponse> {
|
): Promise<JSONRPCResponse> {
|
||||||
const { id, method, params, credentials } = request;
|
const { id, method, params, credentials } = request;
|
||||||
|
|
||||||
|
// helper to create consistent error responses with optional debug data
|
||||||
const makeError = (code: number, message: string, data?: any) => ({
|
const makeError = (code: number, message: string, data?: any) => ({
|
||||||
jsonrpc: "2.0",
|
jsonrpc: "2.0",
|
||||||
id,
|
id,
|
||||||
@@ -261,13 +298,17 @@ async function handleMCPRequest(
|
|||||||
return makeError(-32601, `Tool '${toolName}' not found`) as JSONRPCResponse;
|
return makeError(-32601, `Tool '${toolName}' not found`) as JSONRPCResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Converter MCP content yang valid
|
||||||
function convertToMcpContent(data: any) {
|
function convertToMcpContent(data: any) {
|
||||||
|
// String → text
|
||||||
if (typeof data === "string") {
|
if (typeof data === "string") {
|
||||||
return {
|
return {
|
||||||
type: "text",
|
type: "text",
|
||||||
text: data,
|
text: data,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Image (dengan __mcp_type)
|
||||||
if (data?.__mcp_type === "image" && data.base64) {
|
if (data?.__mcp_type === "image" && data.base64) {
|
||||||
return {
|
return {
|
||||||
type: "image",
|
type: "image",
|
||||||
@@ -275,6 +316,8 @@ async function handleMCPRequest(
|
|||||||
mimeType: data.mimeType || "image/png",
|
mimeType: data.mimeType || "image/png",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Audio
|
||||||
if (data?.__mcp_type === "audio" && data.base64) {
|
if (data?.__mcp_type === "audio" && data.base64) {
|
||||||
return {
|
return {
|
||||||
type: "audio",
|
type: "audio",
|
||||||
@@ -283,6 +326,7 @@ async function handleMCPRequest(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Semua lainnya → text (untuk mencegah error Zod union)
|
||||||
return {
|
return {
|
||||||
type: "text",
|
type: "text",
|
||||||
text: (() => {
|
text: (() => {
|
||||||
@@ -295,6 +339,7 @@ async function handleMCPRequest(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const baseUrl = credentials?.baseUrl;
|
const baseUrl = credentials?.baseUrl;
|
||||||
const token = credentials?.token;
|
const token = credentials?.token;
|
||||||
@@ -316,6 +361,7 @@ async function handleMCPRequest(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
// return error with message and minimal debug info (avoid leaking secrets)
|
||||||
const debug = { message: err?.message, stack: err?.stack?.split("\n").slice(0, 5) };
|
const debug = { message: err?.message, stack: err?.stack?.split("\n").slice(0, 5) };
|
||||||
|
|
||||||
return makeError(-32603, err?.message || "Internal error", debug) as JSONRPCResponse;
|
return makeError(-32603, err?.message || "Internal error", debug) as JSONRPCResponse;
|
||||||
@@ -332,7 +378,9 @@ async function handleMCPRequest(
|
|||||||
|
|
||||||
// ======================================================
|
// ======================================================
|
||||||
// MCP TRIGGER NODE
|
// MCP TRIGGER NODE
|
||||||
// (preserves class name OpenapiMcpServer)
|
// - preserves class name OpenapiMcpServer
|
||||||
|
// - avoids forcing refresh on every webhook call (uses cache by default)
|
||||||
|
// - safer batch handling (Promise.allSettled) to return array of results
|
||||||
// ======================================================
|
// ======================================================
|
||||||
export class OpenapiMcpServer implements INodeType {
|
export class OpenapiMcpServer implements INodeType {
|
||||||
description: INodeTypeDescription = {
|
description: INodeTypeDescription = {
|
||||||
@@ -370,23 +418,13 @@ export class OpenapiMcpServer implements INodeType {
|
|||||||
default: "",
|
default: "",
|
||||||
placeholder: "https://example.com/openapi.json",
|
placeholder: "https://example.com/openapi.json",
|
||||||
},
|
},
|
||||||
|
|
||||||
// ======================================================
|
|
||||||
// ⬇⬇⬇ UPDATED: Default Filter sekarang multi-select (multiOptions)
|
|
||||||
// ======================================================
|
|
||||||
{
|
{
|
||||||
displayName: 'Default Filter',
|
displayName: "Default Filter",
|
||||||
name: 'defaultFilter',
|
name: "defaultFilter",
|
||||||
type: 'multiOptions', // <-- multi-select
|
type: "string",
|
||||||
typeOptions: {
|
default: "",
|
||||||
loadOptionsMethod: 'loadAvailableTags',
|
placeholder: "mcp | tag",
|
||||||
refreshOnOpen: true,
|
|
||||||
},
|
|
||||||
default: [], // empty means no tag filtering (or 'All' in loader)
|
|
||||||
description: 'Filter berdasarkan tag dari OpenAPI (multi-select supported)',
|
|
||||||
},
|
},
|
||||||
// ======================================================
|
|
||||||
|
|
||||||
{
|
{
|
||||||
displayName: 'Available Tools (auto-refresh)',
|
displayName: 'Available Tools (auto-refresh)',
|
||||||
name: 'toolList',
|
name: 'toolList',
|
||||||
@@ -396,7 +434,7 @@ export class OpenapiMcpServer implements INodeType {
|
|||||||
refreshOnOpen: true,
|
refreshOnOpen: true,
|
||||||
},
|
},
|
||||||
default: 'all',
|
default: 'all',
|
||||||
description: 'Daftar tools yang berhasil dimuat dari OpenAPI (tergantung Default Filter)',
|
description: 'Daftar tools yang berhasil dimuat dari OpenAPI',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -406,57 +444,16 @@ export class OpenapiMcpServer implements INodeType {
|
|||||||
// ==================================================
|
// ==================================================
|
||||||
methods = {
|
methods = {
|
||||||
loadOptions: {
|
loadOptions: {
|
||||||
// ========================================================
|
|
||||||
// ⬇⬇⬇ NEW: dropdown tag loader (unchanged)
|
|
||||||
// ========================================================
|
|
||||||
async loadAvailableTags(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
|
||||||
const openapiUrl = this.getNodeParameter("openapiUrl", 0) as string;
|
|
||||||
|
|
||||||
if (!openapiUrl) {
|
|
||||||
return [{ name: "❌ No OpenAPI URL provided", value: "all" }];
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch(openapiUrl);
|
|
||||||
const json = await res.json();
|
|
||||||
|
|
||||||
const tags: string[] =
|
|
||||||
json?.tags?.map((t: any) => t.name) ??
|
|
||||||
Object.values(json.paths || {})
|
|
||||||
.flatMap((p: any) =>
|
|
||||||
Object.values(p).flatMap((m: any) => m.tags || [])
|
|
||||||
);
|
|
||||||
|
|
||||||
const unique = Array.from(new Set(tags));
|
|
||||||
|
|
||||||
// include an "All" option; users can still select none (empty array) which we'll treat as "all"
|
|
||||||
return [
|
|
||||||
{ name: "All", value: "all" },
|
|
||||||
...unique.map((t) => ({
|
|
||||||
name: t,
|
|
||||||
value: t,
|
|
||||||
})),
|
|
||||||
];
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed loading tags:", err);
|
|
||||||
return [{ name: "All", value: "all" }];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// ========================================================
|
|
||||||
|
|
||||||
// ========================================================
|
|
||||||
// ⬇⬇⬇ UPDATED: refreshToolList now reads multi-select defaultFilter (string | string[])
|
|
||||||
// ========================================================
|
|
||||||
async refreshToolList(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
async refreshToolList(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||||
const openapiUrl = this.getNodeParameter("openapiUrl", 0) as string;
|
const openapiUrl = this.getNodeParameter("openapiUrl", 0) as string;
|
||||||
const filterTag = this.getNodeParameter("defaultFilter", 0) as string | string[]; // may be array
|
const filterTag = this.getNodeParameter("defaultFilter", 0) as string;
|
||||||
|
|
||||||
if (!openapiUrl) {
|
if (!openapiUrl) {
|
||||||
return [{ name: "❌ No OpenAPI URL provided", value: "" }];
|
return [{ name: "❌ No OpenAPI URL provided", value: "" }];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass the filterTag in its native shape to loadTools
|
// force refresh when user opens selector explicitly
|
||||||
const tools = await loadTools(openapiUrl, filterTag as any, true);
|
const tools = await loadTools(openapiUrl, filterTag, true);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{ name: "All Tools", value: "all" },
|
{ name: "All Tools", value: "all" },
|
||||||
@@ -467,7 +464,6 @@ export class OpenapiMcpServer implements INodeType {
|
|||||||
})),
|
})),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
// ========================================================
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -476,9 +472,10 @@ export class OpenapiMcpServer implements INodeType {
|
|||||||
// ==================================================
|
// ==================================================
|
||||||
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
|
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
|
||||||
const openapiUrl = this.getNodeParameter("openapiUrl", 0) as string;
|
const openapiUrl = this.getNodeParameter("openapiUrl", 0) as string;
|
||||||
const filterTag = this.getNodeParameter("defaultFilter", 0) as string | string[]; // multi-select support
|
const filterTag = this.getNodeParameter("defaultFilter", 0) as string;
|
||||||
|
|
||||||
const tools = await loadTools(openapiUrl, filterTag as any, false);
|
// Use cached tools by default — non-blocking and faster
|
||||||
|
const tools = await loadTools(openapiUrl, filterTag, false);
|
||||||
|
|
||||||
const creds = await this.getCredentials("openapiMcpServerCredentials") as {
|
const creds = await this.getCredentials("openapiMcpServerCredentials") as {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
@@ -491,12 +488,14 @@ export class OpenapiMcpServer implements INodeType {
|
|||||||
|
|
||||||
const body = this.getBodyData();
|
const body = this.getBodyData();
|
||||||
|
|
||||||
|
// Batch handling: use Promise.allSettled and return array of results
|
||||||
if (Array.isArray(body)) {
|
if (Array.isArray(body)) {
|
||||||
const promises = body.map((r) =>
|
const promises = body.map((r) =>
|
||||||
handleMCPRequest({ ...r, credentials: creds }, tools)
|
handleMCPRequest({ ...r, credentials: creds }, tools)
|
||||||
);
|
);
|
||||||
const settled = await Promise.allSettled(promises);
|
const settled = await Promise.allSettled(promises);
|
||||||
|
|
||||||
|
// Normalize to either results or errors in MCP shape
|
||||||
const responses = settled.map((s) => {
|
const responses = settled.map((s) => {
|
||||||
if (s.status === "fulfilled") return s.value;
|
if (s.status === "fulfilled") return s.value;
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "n8n-nodes-openapi-mcp-server",
|
"name": "n8n-nodes-openapi-mcp-server",
|
||||||
"version": "1.1.32",
|
"version": "1.1.33",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"n8n",
|
"n8n",
|
||||||
"n8n-nodes"
|
"n8n-nodes"
|
||||||
|
|||||||
Reference in New Issue
Block a user