mcp-servers/mcp-gateway/erpnext-mcp/src/index.ts

148 lines
5.7 KiB
TypeScript

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<void> {
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<void> {
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);
});
}