diff --git a/erpnext-mcp/src/index.ts b/erpnext-mcp/src/index.ts new file mode 100644 index 0000000..26c2770 --- /dev/null +++ b/erpnext-mcp/src/index.ts @@ -0,0 +1,148 @@ +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); + }); +}