import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import express from "express"; import { ERPNextClient } from "./services/erpnext-client.js"; import { registerCoreTools } from "./tools/core.js"; import { registerAccountingTools } from "./tools/accounting.js"; import { registerSellingTools } from "./tools/selling.js"; import { registerBuyingTools } from "./tools/buying.js"; import { registerStockTools } from "./tools/stock.js"; import { registerManufacturingTools, registerHRTools, registerCRMTools, registerProjectTools, registerSupportTools, registerSetupTools, } from "./tools/domains.js"; import { registerFrappeTools } from "./tools/frappe.js"; // ── Configuration ─────────────────────────────────────── function getConfig() { const baseUrl = process.env.ERPNEXT_URL || process.env.ERPNEXT_BASE_URL || ""; const apiKey = process.env.ERPNEXT_API_KEY || ""; const apiSecret = process.env.ERPNEXT_API_SECRET || ""; if (!baseUrl || !apiKey || !apiSecret) { console.error( "Missing required environment variables:\n" + " ERPNEXT_URL - ERPNext instance URL (e.g. https://erp.example.com)\n" + " ERPNEXT_API_KEY - API Key from ERPNext user settings\n" + " ERPNEXT_API_SECRET - API Secret from ERPNext user settings" ); process.exit(1); } return { baseUrl, apiKey, apiSecret }; } // ── Server Setup ──────────────────────────────────────── function createServer(): { server: McpServer; client: ERPNextClient } { const config = getConfig(); const client = new ERPNextClient(config); const server = new McpServer({ name: "erpnext-mcp-server", version: "1.0.0", }); // Register all tool categories registerCoreTools(server, client); // 1-15: Generic CRUD, search, reports, meta, bulk ops registerAccountingTools(server, client); // 16-22: GL balance, exchange rate, journal entries, payments registerSellingTools(server, client); // 23-30: Quotations, sales orders, invoices, delivery notes registerBuyingTools(server, client); // 31-36: Purchase orders, receipts, RFQ, supplier quotations registerStockTools(server, client); // 37-50: Stock balance, entries, batches, barcodes, bins registerManufacturingTools(server, client);// 51-53: Work orders, BOMs registerHRTools(server, client); // 54-57: Employees, departments, org chart registerCRMTools(server, client); // 58-60: Leads, opportunities, pipeline registerProjectTools(server, client); // 61-63: Projects, tasks registerSupportTools(server, client); // 64-66: Issues, SLA registerSetupTools(server, client); // 67-70: Company, addresses, holidays, T&C registerFrappeTools(server, client); // 71-100+: Get/set value, assignments, comments, tags, // leaderboards, notifications, todos, activity, // linked docs, print formats, warehouse tree, // returns, material request ops, rename, etc. console.error( `ERPNext MCP Server initialized\n` + ` Instance: ${config.baseUrl}\n` + ` Tools registered: 100+` ); return { server, client }; } // ── Transport: stdio ──────────────────────────────────── async function runStdio(): Promise { const { server } = createServer(); const transport = new StdioServerTransport(); await server.connect(transport); console.error("ERPNext MCP Server running on stdio"); } // ── Transport: Streamable HTTP ────────────────────────── async function runHTTP(): Promise { const { server } = createServer(); const app = express(); app.use(express.json()); // Health check app.get("/health", (_req, res) => { res.json({ status: "ok", server: "erpnext-mcp-server", version: "1.0.0" }); }); // MCP endpoint app.post("/mcp", async (req, res) => { const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true, }); res.on("close", () => transport.close()); await server.connect(transport); await transport.handleRequest(req, res, req.body); }); // Handle GET and DELETE for MCP protocol compliance app.get("/mcp", async (_req, res) => { res.writeHead(405).end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Method not allowed. Use POST for MCP requests." }, id: null, })); }); app.delete("/mcp", async (_req, res) => { res.writeHead(405).end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Method not allowed." }, id: null, })); }); const port = parseInt(process.env.PORT || "32802", 10); app.listen(port, () => { console.error(`ERPNext MCP Server running on http://0.0.0.0:${port}/mcp`); }); } // ── Entrypoint ────────────────────────────────────────── const transport = process.env.TRANSPORT || "stdio"; if (transport === "http") { runHTTP().catch((error) => { console.error("Server error:", error); process.exit(1); }); } else { runStdio().catch((error) => { console.error("Server error:", error); process.exit(1); }); }