Commit c2a55970 authored by Vũ Hoàng Anh's avatar Vũ Hoàng Anh

Initial commit: n8n MCP Server for Cloudflare Workers

parents
Pipeline #3364 failed with stages
node_modules/
dist/
.wrangler/
deploy_error*.txt
cloudflared_log.txt
.dev.vars
Bnode.exe : Γû▓ [WARNING]
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "n8n-mcp-server",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "wrangler dev",
"start": "wrangler dev",
"deploy": "wrangler deploy",
"cf-typegen": "wrangler types"
},
"dependencies": {
"agents": "^0.5.0",
"zod": "^3.25.76"
},
"devDependencies": {
"typescript": "5.9.3",
"wrangler": "^4.67.0"
}
}
import { createMcpHandler } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
// Type for environment bindings
interface Env {
N8N_API_URL: string;
}
// Helper to call n8n REST API
async function n8nFetch(
baseUrl: string,
apiKey: string,
path: string,
method: string = "GET",
body?: unknown
): Promise<unknown> {
const url = `${baseUrl.replace(/\/+$/, "")}/api/v1${path}`;
const res = await fetch(url, {
method,
headers: {
"X-N8N-API-KEY": apiKey,
"Content-Type": "application/json",
},
body: body ? JSON.stringify(body) : undefined,
});
if (!res.ok) {
const errText = await res.text();
throw new Error(`n8n API error ${res.status}: ${errText}`);
}
return res.json();
}
// Create MCP server with n8n tools — new instance per request
function createServer(baseUrl: string, apiKey: string) {
const server = new McpServer({
name: "n8n MCP Server",
version: "1.0.0",
});
// ─── List Workflows ───
server.tool(
"list_workflows",
"List all n8n workflows. Optionally filter by active status.",
{
active: z.boolean().optional().describe("Filter by active status (true/false). Omit to list all."),
},
async ({ active }) => {
const params = active !== undefined ? `?active=${active}` : "";
const data = await n8nFetch(baseUrl, apiKey, `/workflows${params}`);
return {
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
};
}
);
// ─── Get Workflow ───
server.tool(
"get_workflow",
"Get detailed information about a specific workflow by ID.",
{
id: z.string().describe("The workflow ID"),
},
async ({ id }) => {
const data = await n8nFetch(baseUrl, apiKey, `/workflows/${id}`);
return {
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
};
}
);
// ─── Activate Workflow ───
server.tool(
"activate_workflow",
"Activate (enable) a workflow so it can be triggered.",
{
id: z.string().describe("The workflow ID to activate"),
},
async ({ id }) => {
const data = await n8nFetch(baseUrl, apiKey, `/workflows/${id}/activate`, "POST");
return {
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
};
}
);
// ─── Deactivate Workflow ───
server.tool(
"deactivate_workflow",
"Deactivate (disable) a workflow.",
{
id: z.string().describe("The workflow ID to deactivate"),
},
async ({ id }) => {
const data = await n8nFetch(baseUrl, apiKey, `/workflows/${id}/deactivate`, "POST");
return {
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
};
}
);
// ─── Execute Workflow ───
server.tool(
"execute_workflow",
"Execute (run) a workflow by ID. Optionally pass input data.",
{
id: z.string().describe("The workflow ID to execute"),
data: z.record(z.unknown()).optional().describe("Optional input data to pass to the workflow"),
},
async ({ id, data: inputData }) => {
const body = inputData ? { data: inputData } : undefined;
const result = await n8nFetch(baseUrl, apiKey, `/workflows/${id}/execute`, "POST", body);
return {
content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
};
}
);
// ─── List Executions ───
server.tool(
"list_executions",
"List recent workflow executions. Optionally filter by workflow ID or status.",
{
workflowId: z.string().optional().describe("Filter by workflow ID"),
status: z.enum(["success", "error", "waiting"]).optional().describe("Filter by execution status"),
limit: z.number().optional().default(20).describe("Max results to return (default: 20)"),
},
async ({ workflowId, status, limit }) => {
const params = new URLSearchParams();
if (workflowId) params.set("workflowId", workflowId);
if (status) params.set("status", status);
if (limit) params.set("limit", String(limit));
const qs = params.toString() ? `?${params.toString()}` : "";
const data = await n8nFetch(baseUrl, apiKey, `/executions${qs}`);
return {
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
};
}
);
return server;
}
// Worker fetch handler — per-user API key via URL path
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
const url = new URL(request.url);
const path = url.pathname;
// Health check
if ((path === "/" || path === "/health") && request.method === "GET") {
return new Response(
JSON.stringify({
status: "ok",
service: "n8n-mcp-server",
usage: "Connect via /mcp/<your-n8n-api-key>",
n8n_url: env.N8N_API_URL,
}),
{ headers: { "Content-Type": "application/json" } }
);
}
// Debug endpoint: /debug/<api_key> — test key extraction + n8n API
const debugMatch = path.match(/^\/debug\/(.+)$/);
if (debugMatch && request.method === "GET") {
const apiKey = decodeURIComponent(debugMatch[1]);
try {
const data = await n8nFetch(env.N8N_API_URL, apiKey, "/workflows?limit=1");
return new Response(
JSON.stringify({ status: "ok", key_length: apiKey.length, key_preview: apiKey.substring(0, 20) + "...", n8n_response: data }, null, 2),
{ headers: { "Content-Type": "application/json" } }
);
} catch (e: any) {
return new Response(
JSON.stringify({ status: "error", key_length: apiKey.length, key_preview: apiKey.substring(0, 20) + "...", key_full: apiKey, error: e.message }, null, 2),
{ status: 500, headers: { "Content-Type": "application/json" } }
);
}
}
// Extract API key from URL: /mcp/<api_key> or /mcp/<api_key>/...
const mcpMatch = path.match(/^\/mcp\/([^\/]+)(\/.*)?$/);
if (mcpMatch) {
const apiKey = decodeURIComponent(mcpMatch[1]);
// Rewrite the URL to /mcp for the MCP handler
const rewrittenUrl = new URL(request.url);
rewrittenUrl.pathname = "/mcp" + (mcpMatch[2] || "");
const rewrittenRequest = new Request(rewrittenUrl.toString(), request);
const server = createServer(env.N8N_API_URL, apiKey);
return createMcpHandler(server)(rewrittenRequest, env, ctx);
}
// Bare /mcp without key → error message
if (path === "/mcp") {
return new Response(
JSON.stringify({
error: "API key required",
usage: "Use /mcp/<your-n8n-api-key> as the MCP Server URL",
example: "https://n8n-mcp-server.canifa-mcp.workers.dev/mcp/your-api-key-here",
}),
{ status: 401, headers: { "Content-Type": "application/json" } }
);
}
return new Response("Not found", { status: 404 });
},
} satisfies ExportedHandler<Env>;
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": [
"ESNext"
],
"types": [
"@cloudflare/workers-types"
],
"jsx": "react-jsx",
"noEmit": true,
"isolatedModules": true,
"strict": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules"
]
}
\ No newline at end of file
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "n8n-mcp-server",
"main": "src/index.ts",
"compatibility_date": "2025-03-10",
"compatibility_flags": [
"nodejs_compat"
],
"observability": {
"enabled": true
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment