erp-mcp-server-demo
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
erp-mcp-server-demo
A read-only MCP server that lets a small-business owner ask Claude about sales, stock, cash and receivables, with per-key auth, hard tenant isolation, rate limits and an audit trail.
Sanitized demo of a production feature I built for a real multi-tenant ERP (Peru: PEN/USD, 18% IGV). All data here is fake, the code is rewritten from scratch.

TypeScript, Node 20, official @modelcontextprotocol/sdk, Streamable HTTP (stateless, JSON responses) plus a stdio entry point, SQLite, Zod, Vitest, fast-check. 74 tests.
Architecture
flowchart LR
C[MCP client<br/>Claude Code] -->|HTTPS POST /mcp<br/>Bearer zx_...| H[HTTP]
H --> IP{Per-IP limit<br/>120/min}
IP -->|429 + Retry-After| X1[reject]
IP --> K{API key auth<br/>SHA-256 lookup}
K -->|401 + WWW-Authenticate| X2[reject]
K --> KL{Per-key limit<br/>60/min, by hash}
KL -->|429 + Retry-After| X3[reject]
KL --> T[Tenant context<br/>from key record only]
T --> W[Tool whitelist<br/>strict JSON Schema validation]
W --> A[(Audit line)]
W --> D[(Data: every query<br/>filtered by tenant_id)]Related MCP server: Toast MCP Server
Security design
Decision | Why |
Personal API key | A database leak must not leak usable credentials. The key is random with ~238 bits of entropy, so a fast hash is enough (no need for bcrypt). |
Max 5 active keys per user; revoked or expired keys get | Bounds the blast radius of forgotten keys. The client can't tell why a key failed (malformed, unknown, revoked, expired all look the same). |
Tenant comes only from the key record, never from headers or tool arguments | The classic multi-tenant bug is trusting |
Per-IP limit (120/min) is checked before key validation, then a per-key limit (60/min) keyed by the key hash. | Invented keys can't hammer the key store (a test shows the throttled requests never reach it), and the raw key never sits in a memory map or log. |
Fixed whitelist of 7 read-only tools, inputs validated against strict schemas before anything runs. No write tools at all | The model can only do what is listed. The JSON Schema advertised in |
One audit line per tool call: key prefix, org, tool, duration, ok/error | Answers "who asked what, when" without storing secrets. It never contains the key or the arguments (tested), and unknown tool names are not echoed into logs. |
Money in integer cents, IGV split as | No float rounding bugs. Property tests (fast-check) check the invariants over random amounts. |
Body parsed only after auth, 100 KB limit, loopback bind, no stack traces to clients | Anonymous callers can't make the server do work. |
Tools
Tool | What it answers |
| Documents, total, IGV base/tax, average ticket, by payment method |
| Best sellers by revenue |
| What is running out |
| Opening, cash sales, in/out, expected cash in drawer |
| Outstanding vs overdue, oldest overdue age |
| Open invoices, oldest first |
| Price and stock by name or SKU |
Every result carries its currency and money as { cents, display } (e.g. { "cents": 123450, "display": "S/ 1,234.50" }).
Quickstart
Requires Node 20+.
npm install
npm run seed # fake data for two tenants
npm run keys:create -- --tenant cafe-arequipa --name my-laptop # prints the key ONCE
npm start # http://127.0.0.1:3333/mcpTenants: cafe-arequipa (coffee shop, PEN) and compu-lima (computer store, USD).
Connect Claude Code:
claude mcp add --transport http erp-demo http://localhost:3333/mcp --header "Authorization: Bearer <key>"Things to ask:
"What were my sales last week, and how much of it was IGV?"
"Which products are running low on stock?"
"What are my 5 best sellers this month?"
"How much cash should be in the drawer right now?"
"Who owes me money and what is overdue? Who is the oldest?"
"How much does the 1kg bag of beans cost and how many do I have?"
Try the isolation: create a key for compu-lima too and notice it can never see the coffee shop's data (and vice versa).
stdio (no HTTP) is also supported: ERP_API_KEY=<key> npm run start:stdio. Audit lines go to stderr so they never corrupt the protocol stream.
Config: PORT (default 3333), ERP_DB_PATH (default data/erp.db), TRUST_PROXY (hop count, only behind a proxy you control).
Tests
npm test # 74 tests, ~5 s
npm run typecheck # tsc --noEmitFile | Covers |
| Key format, hash-only storage, 5-key cap, revoked/expired, 401 + |
| Tenant A never sees tenant B across all tools, spoofed |
| 120/min per IP before key lookup, 60/min per key, |
| Invalid inputs per tool, unknown tools, SQL-looking input, audit lines never hold key or arguments |
| fast-check properties: |
| Results cross-checked against independent SQL, pagination, and an end-to-end run with the official MCP client |
CI: .github/workflows/ci.yml runs typecheck and tests on Node 20 and 22.
Honest limitations and Roadmap
claude.ai and ChatGPT connectors need OAuth. They don't let you paste a static
Authorizationheader, so this key-based design works with Claude Code, Claude Desktop (via config) and other header-capable clients today. OAuth 2.1 (authorization server, dynamic client registration, scoped tokens) is the main roadmap item.Rate limiting is in-memory and per process. Behind several instances you would move the counters to Redis.
Fixed-window counters allow short bursts at window edges; a sliding window or token bucket would be smoother.
Demo stock numbers are static and not decremented by the fake sales.
Roadmap: OAuth, key rotation endpoint, per-tool scopes, Redis rate limiter, structured log shipping.
License
MIT, see LICENSE.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
This server cannot be deployed
Maintenance
Related MCP Connectors
Query 40 databases from Claude, ChatGPT, or Cursor — on any device. Read-only, encrypted, audited.
Safe, read-only Postgres and MySQL access for AI agents. Audit log + column-level controls.
GA4, Google Ads and Search Console in Claude. Read-only OAuth, multi-account for agencies.
Read-only Google Ads, GA4 and Search Console data for Claude, with no Google Cloud setup.
Related MCP Servers
- AlicenseNot gradedqualityNot gradedmaintenanceProvides structured, read-mostly access to small-business back-office data including customers, invoices, and account notes, allowing Claude to query overdue invoices, revenue summaries, and more.MIT
- AlicenseNot gradedqualityCmaintenanceEnables AI assistants to query live Toast POS data and generate sales, labor, and cash reports while answering restaurant operations questions, all in a read-only manner.25 npmMIT
- AlicenseAqualityCmaintenanceEnables Claude to answer questions about a small business's own data by providing predefined sales, customer, refund, and inventory tools, plus a guarded read-only SQL query tool with audit logging.6MIT
- AlicenseAqualityCmaintenanceEnables Claude to answer business questions against a company's retail database — sales summaries, top customers, inventory alerts, customer profiles, and plain-English queries — through read-only SQL tools plus a schema resource and reporting prompt. Access is governed by OAuth 2.1 roles and scopes, column-level PII masking, a SQL guard, rate limits, and audit logging.5MIT