tambahannnya
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -32,3 +32,7 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
|||||||
|
|
||||||
# Finder (MacOS) folder config
|
# Finder (MacOS) folder config
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
|
||||||
|
# hasil build
|
||||||
|
n8n-nodes-openapi-mcp-server
|
||||||
323
bak.txt
Normal file
323
bak.txt
Normal file
@@ -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<string, any[]>();
|
||||||
|
|
||||||
|
// ======================================================
|
||||||
|
// Load OpenAPI → MCP Tools
|
||||||
|
// ======================================================
|
||||||
|
async function loadTools(openapiUrl: string, filterTag: string, forceRefresh = false): Promise<any[]> {
|
||||||
|
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<string, any> = {},
|
||||||
|
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<JSONRPCResponse> {
|
||||||
|
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<INodePropertyOptions[]> {
|
||||||
|
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<IWebhookResponseData> {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ const lodash_1 = __importDefault(require("lodash"));
|
|||||||
* Convert OpenAPI 3.x JSON spec into MCP-compatible tool definitions (without run()).
|
* Convert OpenAPI 3.x JSON spec into MCP-compatible tool definitions (without run()).
|
||||||
* Hanya menyertakan endpoint yang memiliki tag berisi "mcp".
|
* Hanya menyertakan endpoint yang memiliki tag berisi "mcp".
|
||||||
*/
|
*/
|
||||||
function convertOpenApiToMcpTools(openApiJson) {
|
function convertOpenApiToMcpTools(openApiJson, filterTag) {
|
||||||
var _a, _b, _c;
|
var _a, _b, _c;
|
||||||
const tools = [];
|
const tools = [];
|
||||||
const paths = openApiJson.paths || {};
|
const paths = openApiJson.paths || {};
|
||||||
@@ -21,7 +21,7 @@ function convertOpenApiToMcpTools(openApiJson) {
|
|||||||
for (const [method, operation] of Object.entries(methods)) {
|
for (const [method, operation] of Object.entries(methods)) {
|
||||||
const tags = Array.isArray(operation.tags) ? operation.tags : [];
|
const tags = Array.isArray(operation.tags) ? operation.tags : [];
|
||||||
// ✅ exclude semua yang tidak punya tag atau tag-nya tidak mengandung "mcp"
|
// ✅ 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;
|
continue;
|
||||||
const rawName = lodash_1.default.snakeCase(operation.operationId || `${method}_${path}`) || "unnamed_tool";
|
const rawName = lodash_1.default.snakeCase(operation.operationId || `${method}_${path}`) || "unnamed_tool";
|
||||||
const name = cleanToolName(rawName);
|
const name = cleanToolName(rawName);
|
||||||
@@ -71,9 +71,9 @@ function cleanToolName(name) {
|
|||||||
/**
|
/**
|
||||||
* Ambil OpenAPI JSON dari endpoint dan konversi ke tools MCP
|
* 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 data = await fetch(url);
|
||||||
const openApiJson = await data.json();
|
const openApiJson = await data.json();
|
||||||
const tools = convertOpenApiToMcpTools(openApiJson);
|
const tools = convertOpenApiToMcpTools(openApiJson, filterTag);
|
||||||
return tools;
|
return tools;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,28 @@
|
|||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.OpenapiMcpServer = void 0;
|
exports.OpenapiMcpServer = void 0;
|
||||||
const mcp_tool_convert_1 = require("../lib/mcp_tool_convert");
|
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
|
// Load OpenAPI → MCP Tools
|
||||||
// ======================================================
|
// ======================================================
|
||||||
async function loadTools(openapiUrl) {
|
async function loadTools(openapiUrl, filterTag, forceRefresh = false) {
|
||||||
tools = await (0, mcp_tool_convert_1.getMcpTools)(openapiUrl);
|
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
|
// 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;
|
const { id, method, params, credentials } = request;
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case "initialize":
|
case "initialize":
|
||||||
@@ -58,12 +74,22 @@ async function handleMCPRequest(request) {
|
|||||||
jsonrpc: "2.0",
|
jsonrpc: "2.0",
|
||||||
id,
|
id,
|
||||||
result: {
|
result: {
|
||||||
tools: tools.map((t) => ({
|
tools: tools.map((t) => {
|
||||||
name: t.name,
|
var _a;
|
||||||
description: t.description,
|
const inputSchema = typeof t.inputSchema === "object" && ((_a = t.inputSchema) === null || _a === void 0 ? void 0 : _a.type) === "object"
|
||||||
inputSchema: t.inputSchema,
|
? t.inputSchema
|
||||||
"x-props": t["x-props"],
|
: {
|
||||||
})),
|
type: "object",
|
||||||
|
properties: {},
|
||||||
|
required: [],
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
name: t.name,
|
||||||
|
description: t.description || "No description provided",
|
||||||
|
inputSchema,
|
||||||
|
"x-props": t["x-props"],
|
||||||
|
};
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
case "tools/call": {
|
case "tools/call": {
|
||||||
@@ -122,15 +148,12 @@ class OpenapiMcpServer {
|
|||||||
group: ['trigger'],
|
group: ['trigger'],
|
||||||
version: 1,
|
version: 1,
|
||||||
description: 'Runs an MCP Server inside n8n',
|
description: 'Runs an MCP Server inside n8n',
|
||||||
icon: 'fa:server',
|
icon: 'file:icon.svg',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'OpenAPI MCP Server'
|
name: 'OpenAPI MCP Server'
|
||||||
},
|
},
|
||||||
credentials: [
|
credentials: [
|
||||||
{
|
{ name: "openapiMcpServerCredentials", required: true },
|
||||||
name: "openapiMcpServerCredentials",
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
inputs: [],
|
inputs: [],
|
||||||
outputs: ['main'],
|
outputs: ['main'],
|
||||||
@@ -156,26 +179,66 @@ class OpenapiMcpServer {
|
|||||||
default: "",
|
default: "",
|
||||||
placeholder: "https://example.com/openapi.json",
|
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
|
// WEBHOOK HANDLER
|
||||||
// ==================================================
|
// ==================================================
|
||||||
async webhook() {
|
async webhook() {
|
||||||
const openapiUrl = this.getNodeParameter("openapiUrl", 0);
|
const openapiUrl = this.getNodeParameter("openapiUrl", 0);
|
||||||
if (!tools.length) {
|
const filterTag = this.getNodeParameter("defaultFilter", 0);
|
||||||
await loadTools(openapiUrl);
|
// 🟢 selalu refresh (agar node terbaru)
|
||||||
}
|
const tools = await loadTools(openapiUrl, filterTag, true);
|
||||||
const creds = await this.getCredentials("openapiMcpServerCredentials");
|
const creds = await this.getCredentials("openapiMcpServerCredentials");
|
||||||
const body = this.getBodyData();
|
const body = this.getBodyData();
|
||||||
if (Array.isArray(body)) {
|
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 {
|
return {
|
||||||
webhookResponse: await Promise.all(responses),
|
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 {
|
return {
|
||||||
webhookResponse: single,
|
webhookResponse: single,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "n8n-nodes-openapi-mcp-server",
|
"name": "n8n-nodes-openapi-mcp-server",
|
||||||
"version": "1.1.2",
|
"version": "1.1.15",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"n8n",
|
"n8n",
|
||||||
"n8n-nodes"
|
"n8n-nodes"
|
||||||
|
|||||||
@@ -3,17 +3,40 @@ import {
|
|||||||
INodeTypeDescription,
|
INodeTypeDescription,
|
||||||
IWebhookFunctions,
|
IWebhookFunctions,
|
||||||
IWebhookResponseData,
|
IWebhookResponseData,
|
||||||
|
ILoadOptionsFunctions,
|
||||||
|
INodePropertyOptions,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import { getMcpTools } from "../lib/mcp_tool_convert";
|
import { getMcpTools } from "../lib/mcp_tool_convert";
|
||||||
|
|
||||||
let tools: any[] = []; // ✅ cache global tools
|
// ======================================================
|
||||||
|
// Cache tools per URL
|
||||||
|
// ======================================================
|
||||||
|
const toolsCache = new Map<string, any[]>();
|
||||||
|
|
||||||
// ======================================================
|
// ======================================================
|
||||||
// Load OpenAPI → MCP Tools
|
// Load OpenAPI → MCP Tools
|
||||||
// ======================================================
|
// ======================================================
|
||||||
async function loadTools(openapiUrl: string, filterTag: string) {
|
async function loadTools(openapiUrl: string, filterTag: string, forceRefresh = false): Promise<any[]> {
|
||||||
tools = await getMcpTools(openapiUrl, filterTag);
|
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;
|
id: string | number;
|
||||||
method: string;
|
method: string;
|
||||||
params?: any;
|
params?: any;
|
||||||
credentials?: any; // ✅ tambahan (inject credential)
|
credentials?: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
type JSONRPCResponse = {
|
type JSONRPCResponse = {
|
||||||
@@ -81,10 +104,11 @@ async function executeTool(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ======================================================
|
// ======================================================
|
||||||
// JSON-RPC Handler
|
// JSON-RPC Handler (per node, per request)
|
||||||
// ======================================================
|
// ======================================================
|
||||||
async function handleMCPRequest(
|
async function handleMCPRequest(
|
||||||
request: JSONRPCRequest
|
request: JSONRPCRequest,
|
||||||
|
tools: any[]
|
||||||
): Promise<JSONRPCResponse> {
|
): Promise<JSONRPCResponse> {
|
||||||
const { id, method, params, credentials } = request;
|
const { id, method, params, credentials } = request;
|
||||||
|
|
||||||
@@ -105,12 +129,23 @@ async function handleMCPRequest(
|
|||||||
jsonrpc: "2.0",
|
jsonrpc: "2.0",
|
||||||
id,
|
id,
|
||||||
result: {
|
result: {
|
||||||
tools: tools.map((t) => ({
|
tools: tools.map((t) => {
|
||||||
name: t.name,
|
const inputSchema =
|
||||||
description: t.description,
|
typeof t.inputSchema === "object" && t.inputSchema?.type === "object"
|
||||||
inputSchema: t.inputSchema,
|
? t.inputSchema
|
||||||
"x-props": t["x-props"],
|
: {
|
||||||
})),
|
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: {
|
defaults: {
|
||||||
name: 'OpenAPI MCP Server'
|
name: 'OpenAPI MCP Server'
|
||||||
},
|
},
|
||||||
|
|
||||||
credentials: [
|
credentials: [
|
||||||
{
|
{ name: "openapiMcpServerCredentials", required: true },
|
||||||
name: "openapiMcpServerCredentials",
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
|
|
||||||
inputs: [],
|
inputs: [],
|
||||||
outputs: ['main'],
|
outputs: ['main'],
|
||||||
|
|
||||||
webhooks: [
|
webhooks: [
|
||||||
{
|
{
|
||||||
name: 'default',
|
name: 'default',
|
||||||
@@ -203,7 +232,6 @@ export class OpenapiMcpServer implements INodeType {
|
|||||||
path: '={{$parameter["path"]}}',
|
path: '={{$parameter["path"]}}',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
properties: [
|
properties: [
|
||||||
{
|
{
|
||||||
displayName: "Path",
|
displayName: "Path",
|
||||||
@@ -224,10 +252,47 @@ export class OpenapiMcpServer implements INodeType {
|
|||||||
type: "string",
|
type: "string",
|
||||||
default: "",
|
default: "",
|
||||||
placeholder: "mcp | tag",
|
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<INodePropertyOptions[]> {
|
||||||
|
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
|
// WEBHOOK HANDLER
|
||||||
// ==================================================
|
// ==================================================
|
||||||
@@ -235,9 +300,8 @@ export class OpenapiMcpServer implements INodeType {
|
|||||||
const openapiUrl = this.getNodeParameter("openapiUrl", 0) as string;
|
const openapiUrl = this.getNodeParameter("openapiUrl", 0) as string;
|
||||||
const filterTag = this.getNodeParameter("defaultFilter", 0) as string;
|
const filterTag = this.getNodeParameter("defaultFilter", 0) as string;
|
||||||
|
|
||||||
if (!tools.length) {
|
// 🟢 selalu refresh (agar node terbaru)
|
||||||
await loadTools(openapiUrl, filterTag);
|
const tools = await loadTools(openapiUrl, filterTag, true);
|
||||||
}
|
|
||||||
|
|
||||||
const creds = await this.getCredentials("openapiMcpServerCredentials") as {
|
const creds = await this.getCredentials("openapiMcpServerCredentials") as {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
@@ -248,17 +312,17 @@ export class OpenapiMcpServer implements INodeType {
|
|||||||
|
|
||||||
if (Array.isArray(body)) {
|
if (Array.isArray(body)) {
|
||||||
const responses = body.map((r) =>
|
const responses = body.map((r) =>
|
||||||
handleMCPRequest({ ...r, credentials: creds })
|
handleMCPRequest({ ...r, credentials: creds }, tools)
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
webhookResponse: await Promise.all(responses),
|
webhookResponse: await Promise.all(responses),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const single = await handleMCPRequest({
|
const single = await handleMCPRequest(
|
||||||
...(body as JSONRPCRequest),
|
{ ...(body as JSONRPCRequest), credentials: creds },
|
||||||
credentials: creds,
|
tools
|
||||||
});
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
webhookResponse: single,
|
webhookResponse: single,
|
||||||
|
|||||||
Reference in New Issue
Block a user