Jake Shore f3c4cd817b Add all MCP servers + factory infra to MCPEngine — 2026-02-06
=== NEW SERVERS ADDED (7) ===
- servers/closebot — 119 tools, 14 modules, 4,656 lines TS (Stage 7)
- servers/google-console — Google Search Console MCP (Stage 7)
- servers/meta-ads — Meta/Facebook Ads MCP (Stage 8)
- servers/twilio — Twilio communications MCP (Stage 8)
- servers/competitor-research — Competitive intel MCP (Stage 6)
- servers/n8n-apps — n8n workflow MCP apps (Stage 6)
- servers/reonomy — Commercial real estate MCP (Stage 1)

=== FACTORY INFRASTRUCTURE ADDED ===
- infra/factory-tools — mcp-jest, mcp-validator, mcp-add, MCP Inspector
  - 60 test configs, 702 auto-generated test cases
  - All 30 servers score 100/100 protocol compliance
- infra/command-center — Pipeline state, operator playbook, dashboard config
- infra/factory-reviews — Automated eval reports

=== DOCS ADDED ===
- docs/MCP-FACTORY.md — Factory overview
- docs/reports/ — 5 pipeline evaluation reports
- docs/research/ — Browser MCP research

=== RULES ESTABLISHED ===
- CONTRIBUTING.md — All MCP work MUST go in this repo
- README.md — Full inventory of 37 servers + infra docs
- .gitignore — Updated for Python venvs

TOTAL: 37 MCP servers + full factory pipeline in one repo.
This is now the single source of truth for all MCP work.
2026-02-06 06:32:29 -05:00

262 lines
15 KiB
TypeScript

/**
* n8n MCP Apps Server
* Registers all n8n MCP App tools + HTML resources for Claude Desktop / VS Code / Goose
* Each app maps to one or more n8n-mcp endpoints with a rich interactive UI.
*/
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
import { readFileSync, existsSync } from "fs";
import { resolve } from "path";
import { z } from "zod";
const N8N_API_URL = process.env.N8N_API_URL || "https://auto.localbosses.org";
const N8N_API_KEY = process.env.N8N_API_KEY || "";
const server = new McpServer({
name: "n8n-apps",
version: "1.0.0",
});
// Helper: load built HTML app
function loadAppHtml(appName: string): string {
const path = resolve(`dist/app-ui/${appName}.html`);
if (existsSync(path)) return readFileSync(path, "utf-8");
return `<html><body><h1>App not built yet</h1><p>Run: npm run build</p></body></html>`;
}
// Helper: call n8n API
async function n8nApi(path: string, options: RequestInit = {}): Promise<any> {
const res = await fetch(`${N8N_API_URL}/api/v1${path}`, {
...options,
headers: {
"X-N8N-API-KEY": N8N_API_KEY,
"Content-Type": "application/json",
...options.headers,
},
});
return res.json();
}
// ═══════════════════════════════════════════════════════════
// IMPORTANT: Each tool below opens a SEPARATE UI app.
// The model should ONLY call ONE of these tools per user request.
// Never call multiple n8n UI tools in parallel.
// ═══════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════
// APP 1: Workflow Dashboard
// ═══════════════════════════════════════════════
registerAppResource(server, "n8n Workflow Dashboard", "ui://n8n/workflow-dashboard", {}, async () => ({
contents: [{ uri: "ui://n8n/workflow-dashboard", mimeType: RESOURCE_MIME_TYPE, text: loadAppHtml("workflow-dashboard") }],
}));
registerAppTool(server, "n8n_workflow_dashboard", {
description: "Display all n8n workflows in an interactive dashboard with status, node counts, and quick actions. ONLY call when the user specifically asks to see, list, or manage their workflows. Do NOT call alongside other n8n UI tools.",
inputSchema: {
active: z.boolean().optional().describe("Filter by active status"),
limit: z.number().optional().describe("Max workflows to show (default 50)"),
},
_meta: { ui: { resourceUri: "ui://n8n/workflow-dashboard" } },
}, async (args) => {
const params = new URLSearchParams();
if (args.active !== undefined) params.set("active", String(args.active));
params.set("limit", String(args.limit || 100));
const data = await n8nApi(`/workflows?${params}`);
return { content: [{ type: "text" as const, text: JSON.stringify(data) }] };
});
// ═══════════════════════════════════════════════
// APP 2: Workflow Builder
// ═══════════════════════════════════════════════
registerAppResource(server, "n8n Workflow Builder", "ui://n8n/workflow-builder", {}, async () => ({
contents: [{ uri: "ui://n8n/workflow-builder", mimeType: RESOURCE_MIME_TYPE, text: loadAppHtml("workflow-builder") }],
}));
registerAppTool(server, "n8n_workflow_builder", {
description: "Open the interactive visual workflow builder to create a NEW n8n workflow. ONLY call when the user specifically asks to create or build a new workflow. Do NOT call alongside other n8n UI tools.",
inputSchema: {
name: z.string().optional().describe("Workflow name"),
template: z.string().optional().describe("Start from a template description"),
},
_meta: { ui: { resourceUri: "ui://n8n/workflow-builder" } },
}, async (args) => ({
content: [{ type: "text" as const, text: JSON.stringify({ action: "new_workflow", name: args.name || "New Workflow", template: args.template || null, apiUrl: N8N_API_URL }) }],
}));
// ═══════════════════════════════════════════════
// APP 3: Workflow Detail / Editor
// ═══════════════════════════════════════════════
registerAppResource(server, "n8n Workflow Detail", "ui://n8n/workflow-detail", {}, async () => ({
contents: [{ uri: "ui://n8n/workflow-detail", mimeType: RESOURCE_MIME_TYPE, text: loadAppHtml("workflow-detail") }],
}));
registerAppTool(server, "n8n_workflow_detail", {
description: "View and edit a specific n8n workflow by ID. Shows visual node graph, connections, settings, version history, and validation. ONLY call when the user asks to inspect, view details of, or edit a specific workflow. Requires a workflow ID. Do NOT call alongside other n8n UI tools.",
inputSchema: {
id: z.string().describe("Workflow ID (required)"),
mode: z.enum(["full", "details", "structure", "minimal"]).optional().describe("Detail level"),
},
_meta: { ui: { resourceUri: "ui://n8n/workflow-detail" } },
}, async (args) => {
const data = await n8nApi(`/workflows/${args.id}`);
return { content: [{ type: "text" as const, text: JSON.stringify(data) }] };
});
// ═══════════════════════════════════════════════
// APP 4: Execution Dashboard
// ═══════════════════════════════════════════════
registerAppResource(server, "n8n Execution Dashboard", "ui://n8n/execution-dashboard", {}, async () => ({
contents: [{ uri: "ui://n8n/execution-dashboard", mimeType: RESOURCE_MIME_TYPE, text: loadAppHtml("execution-dashboard") }],
}));
registerAppTool(server, "n8n_execution_dashboard", {
description: "View workflow execution history with status, timing, and results. ONLY call when the user specifically asks about executions, execution history, or run results. Do NOT call alongside other n8n UI tools.",
inputSchema: {
workflowId: z.string().optional().describe("Filter by workflow ID"),
status: z.enum(["success", "error", "running", "waiting"]).optional(),
limit: z.number().optional().describe("Max executions to show"),
},
_meta: { ui: { resourceUri: "ui://n8n/execution-dashboard" } },
}, async (args) => {
const params = new URLSearchParams();
if (args.workflowId) params.set("workflowId", args.workflowId);
if (args.status) params.set("status", args.status);
params.set("limit", String(args.limit || 50));
const data = await n8nApi(`/executions?${params}`);
return { content: [{ type: "text" as const, text: JSON.stringify(data) }] };
});
// ═══════════════════════════════════════════════
// APP 5: Workflow Tester
// ═══════════════════════════════════════════════
registerAppResource(server, "n8n Workflow Tester", "ui://n8n/workflow-tester", {}, async () => ({
contents: [{ uri: "ui://n8n/workflow-tester", mimeType: RESOURCE_MIME_TYPE, text: loadAppHtml("workflow-tester") }],
}));
registerAppTool(server, "n8n_workflow_tester", {
description: "Test and trigger a specific n8n workflow interactively. Supports webhook, form, and chat triggers with real-time response. ONLY call when the user specifically asks to test, trigger, or try a workflow. Requires a workflow ID. Do NOT call alongside other n8n UI tools.",
inputSchema: {
workflowId: z.string().describe("Workflow ID to test (required)"),
triggerType: z.enum(["webhook", "form", "chat"]).optional(),
data: z.any().optional().describe("Test payload data"),
},
_meta: { ui: { resourceUri: "ui://n8n/workflow-tester" } },
}, async (args) => {
const workflow = await n8nApi(`/workflows/${args.workflowId}`);
return { content: [{ type: "text" as const, text: JSON.stringify({ workflow, triggerType: args.triggerType || "webhook", testData: args.data || {} }) }] };
});
// ═══════════════════════════════════════════════
// APP 6: Node Explorer
// ═══════════════════════════════════════════════
registerAppResource(server, "n8n Node Explorer", "ui://n8n/node-explorer", {}, async () => ({
contents: [{ uri: "ui://n8n/node-explorer", mimeType: RESOURCE_MIME_TYPE, text: loadAppHtml("node-explorer") }],
}));
registerAppTool(server, "n8n_node_explorer", {
description: "Explore and search n8n's 1,084+ available nodes. Browse by category, search by keyword, view documentation. ONLY call when the user specifically asks to browse nodes, find a node, or explore what nodes are available. Do NOT call alongside other n8n UI tools.",
inputSchema: {
query: z.string().optional().describe("Search query for nodes"),
category: z.string().optional().describe("Filter by category"),
},
_meta: { ui: { resourceUri: "ui://n8n/node-explorer" } },
}, async (args) => ({
content: [{ type: "text" as const, text: JSON.stringify({ action: "explore", query: args.query || "", category: args.category || "all" }) }],
}));
// ═══════════════════════════════════════════════
// APP 7: Template Gallery
// ═══════════════════════════════════════════════
registerAppResource(server, "n8n Template Gallery", "ui://n8n/template-gallery", {}, async () => ({
contents: [{ uri: "ui://n8n/template-gallery", mimeType: RESOURCE_MIME_TYPE, text: loadAppHtml("template-gallery") }],
}));
registerAppTool(server, "n8n_template_gallery", {
description: "Browse and deploy from 2,700+ n8n workflow templates. Search by keyword, task type, or services used. ONLY call when the user specifically asks to browse templates, find a template, or deploy a pre-built workflow. Do NOT call alongside other n8n UI tools.",
inputSchema: {
query: z.string().optional().describe("Search templates"),
task: z.string().optional().describe("Filter by task type"),
},
_meta: { ui: { resourceUri: "ui://n8n/template-gallery" } },
}, async (args) => ({
content: [{ type: "text" as const, text: JSON.stringify({ action: "browse", query: args.query || "", task: args.task || "" }) }],
}));
// ═══════════════════════════════════════════════
// APP 8: Health Monitor
// ═══════════════════════════════════════════════
registerAppResource(server, "n8n Health Monitor", "ui://n8n/health-monitor", {}, async () => ({
contents: [{ uri: "ui://n8n/health-monitor", mimeType: RESOURCE_MIME_TYPE, text: loadAppHtml("health-monitor") }],
}));
registerAppTool(server, "n8n_health_monitor", {
description: "Monitor n8n instance health, API connectivity, and tool availability. ONLY call when the user specifically asks about health, connectivity, status of the n8n instance, or to run diagnostics. Do NOT call alongside other n8n UI tools.",
inputSchema: {
mode: z.enum(["status", "diagnostic"]).optional().describe("Check mode"),
},
_meta: { ui: { resourceUri: "ui://n8n/health-monitor" } },
}, async (args) => {
let health: any = { connected: false };
try {
const users = await n8nApi("/users");
health = { connected: true, users: users.data?.length || 0, apiUrl: N8N_API_URL };
} catch (e: any) {
health = { connected: false, error: e.message };
}
return { content: [{ type: "text" as const, text: JSON.stringify({ mode: args.mode || "status", health, timestamp: new Date().toISOString() }) }] };
});
// ═══════════════════════════════════════════════
// App-only tools (hidden from model, callable from UI)
// ═══════════════════════════════════════════════
registerAppTool(server, "n8n_toggle_workflow", {
description: "Activate or deactivate a workflow",
inputSchema: { id: z.string(), active: z.boolean() },
_meta: { ui: { resourceUri: "ui://n8n/workflow-dashboard", visibility: ["app"] } },
}, async (args) => {
const endpoint = args.active ? "activate" : "deactivate";
const data = await n8nApi(`/workflows/${args.id}/${endpoint}`, { method: "POST" });
return { content: [{ type: "text" as const, text: JSON.stringify(data) }] };
});
registerAppTool(server, "n8n_delete_workflow_action", {
description: "Delete a workflow",
inputSchema: { id: z.string() },
_meta: { ui: { resourceUri: "ui://n8n/workflow-dashboard", visibility: ["app"] } },
}, async (args) => {
const data = await n8nApi(`/workflows/${args.id}`, { method: "DELETE" });
return { content: [{ type: "text" as const, text: JSON.stringify(data) }] };
});
registerAppTool(server, "n8n_create_workflow_action", {
description: "Create a new workflow from the builder",
inputSchema: {
name: z.string(),
nodes: z.string().describe("JSON string of nodes array"),
connections: z.string().describe("JSON string of connections object"),
},
_meta: { ui: { resourceUri: "ui://n8n/workflow-builder", visibility: ["app"] } },
}, async (args) => {
const body = {
name: args.name,
nodes: JSON.parse(args.nodes),
connections: JSON.parse(args.connections),
settings: { executionOrder: "v1" },
};
const data = await n8nApi("/workflows", { method: "POST", body: JSON.stringify(body) });
return { content: [{ type: "text" as const, text: JSON.stringify(data) }] };
});
// ═══════════════════════════════════════════════
// Start server
// ═══════════════════════════════════════════════
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("n8n MCP Apps server running on stdio");
}
main().catch(console.error);