mcp-erp-server
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-erp-serverFind customer Maria Silva and list her unpaid invoices"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mcp-erp-server
A reference Model Context Protocol server that lets an AI assistant (Claude, ChatGPT, Claude Code, any MCP client) work with a small business's ERP safely: customers, appointments, invoices and bills.
It runs against an in-memory mock ERP with synthetic data, so you can clone it and try it in a minute. The interesting part is not the mock, it is everything around it: how to expose real business data and write operations to a language model without handing it the keys.
This is a cleaned-up, generic version of patterns I use in production MCP servers for a dental clinic (appointments, receivables, payables). No real data, names or credentials are in this repository.
What it demonstrates
Concern | How it is handled | Where |
Remote MCP transport | Stateless Streamable HTTP: a fresh |
|
Authentication | OAuth 2.1 for MCP clients: protected-resource and authorization-server metadata (RFC 9728 / RFC 8414), dynamic client registration (RFC 7591), authorization code + PKCE S256, refresh tokens. Static per-person bearer tokens also work for scripts and Claude Code |
|
Authorization | Role-scoped tool registration: the role decides which tools exist for that caller. A tool that was never registered cannot be called by mistake, or by a model that was talked into it |
|
Safe writes | Every write is two steps: a preview that changes nothing, then execution with a confirmation token that is bound to the tool, the person and a hash of the exact arguments, expires, and works once |
|
Blast-radius controls | Global kill switch ( |
|
Data minimization | Search results and profiles return masked personal data; full contact details live behind a separate tool that only some roles can reach and that is audited |
|
Audit trail | Every preview, execution, denial and sensitive read is logged as structured JSON with ids and amounts, never personal data. The logger also redacts anything that looks like a credential |
|
Testing | 40 tests: unit, tool-level through a real MCP client, and end-to-end over HTTP including the full OAuth flow |
|
Related MCP server: tessera-mcp
Quick start
git clone https://github.com/mateusmeloc/mcp-erp-server.git
cd mcp-erp-server
npm install
cp .env.example .env # demo values only; see "Configuration"
set -a; . ./.env; set +a
npm run dev # http://localhost:3000Check it is alive and that it asks for credentials:
curl -s localhost:3000/healthz
curl -si -X POST localhost:3000/mcp | head -n 3 # 401 + WWW-AuthenticateThen talk to it with the demo admin key from .env.example:
curl -s localhost:3000/mcp \
-H "Authorization: Bearer change-me-demo-admin-token-0001" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'Or point the MCP Inspector at http://localhost:3000/mcp.
npm test # 40 tests, no network needed
npm run typecheck
npm run build && npm startTools
Tool | Family | Read / write | What it does |
| system | read | Who you are connected as, your role, and whether write tools are enabled |
| customers | read | Find customers by name, id or city. Returns who they are, never contact details |
| customers | read | A profile with email, phone and document masked |
| customers-pii | read, audited | Full contact details of one customer; every call is logged with the caller's stated reason |
| scheduling | read | Appointments in a date range (max 62 days), filterable by staff and status |
| scheduling | read | Free start times for one staff member on one day (30-minute grid, working hours) |
| scheduling-write | write (preview + confirm) | Books a slot |
| scheduling-write | write (preview + confirm) | Cancels an appointment |
| finance | read | Invoices by status, customer and due-date range |
| finance | read | Open invoices aged: not yet due, 1-30, 31-60, 61+ days overdue |
| finance | read | Expected money in and out in weekly buckets; overdue amounts reported separately |
| finance | read | Bills to pay, sorted by due date |
| finance-write | write (preview + confirm) | Registers a payment |
| finance-write | write (preview + confirm) | Creates a bill |
| finance-delete | write (preview + confirm) | Permanently deletes an open bill. Its own family on purpose |
Roles
Role | Gets | Typical use |
| Every family, including ones added in the future | The owner |
| Everything except | Runs the day to day, including money |
| System, customers (masked), scheduling and its writes. No money | Reception |
| Scheduling and its writes only | An automated assistant that talks to strangers (a chat bot) |
admin is the only open-ended role. The others are closed lists, so a family added tomorrow is
invisible to them until someone grants it deliberately.
How a write works
model ──► appointment_book(customer_id, staff, start) ──► PREVIEW: "Book a <service> for Alex Rivera
(C-001) with <staff> on <start>, 30 minutes."
+ confirmation_token (nothing changed)
human sees the preview and approves
model ──► appointment_book(same args, confirm=true,
confirmation_token=…) ──► executed (token consumed, audited)The token is HMAC-signed and carries a hash of tool | person | canonical(args), an expiry and a
unique id. So the model cannot skip the preview, cannot run something different from what the
human saw (any changed argument invalidates it), cannot use someone else's token, and cannot replay
an old approval. State is revalidated at execution time, because the calendar may have changed
between preview and confirm.
The order inside runWrite is fixed, in one place, so the guardrails cannot drift apart between
tools: kill switch, rate limit, revalidate, preview or consume token, run, audit.
Connecting a client
The server exposes one endpoint, POST /mcp, plus the OAuth endpoints. Deploy it behind HTTPS
(OAuth redirects and bearer tokens must never travel in clear text) and set PUBLIC_URL to the
public address.
Claude / ChatGPT (custom connector): add
https://your-host/mcpas a remote MCP server. The client discovers the OAuth metadata, registers itself, and sends the person to the consent page, which asks for their own access key (the token fromERP_USERS). The consent page names the client and the host it will redirect to.Claude Code:
claude mcp add --transport http erp https://your-host/mcp --header "Authorization: Bearer <access key>"Scripts: send
Authorization: Bearer <access key>directly.
Configuration
Everything comes from environment variables and is validated at boot; a bad configuration stops the
process with a clear message instead of running half-configured. See .env.example.
Variable | Default | Meaning |
| required | At least 32 chars. Signs OAuth tokens and confirmation tokens |
| required | JSON array: |
|
| |
| request host | Public base URL used in the OAuth metadata |
|
| Global kill switch for every write tool |
|
| Set to |
|
| Per person, per write family |
|
| How long a preview stays confirmable |
|
| OAuth access token lifetime |
|
| OAuth refresh token lifetime (30 days) |
With NODE_ENV=production the server refuses to start if any credential contains the demo
marker change-me.
Revoking access: remove the person from ERP_USERS and restart. Roles are read from the current
configuration on every request, not from the token, so the change takes effect immediately for
OAuth tokens too. Rotating TOKEN_SECRET revokes every issued token at once.
Project layout
src/
app.ts Express app: /mcp (stateless), /healthz, error handling
config.ts Env parsing and validation (fails loudly at boot)
auth/ roles, signed tokens, identity, OAuth 2.1 server + consent page
erp/ the mock ERP (types, store with deterministic seed data, privacy masking)
mcp/ server factory, write pipeline, confirmations, audit
tools/ one file per family
test/ unit, tool-level (real MCP client) and HTTP/OAuth end-to-end testsTo use it with a real system, replace src/erp/store.ts with a client for your ERP and keep the
tool layer, roles and write pipeline as they are.
Limits you should know about
This is a reference implementation, not a turnkey product. Honest list:
Single instance. Rate limiters, OAuth authorization codes, registered clients and the single-use ledger of confirmation tokens live in memory. Behind several replicas, or across restarts, they are not shared (a restart invalidates pending previews and codes, which fails safe). Move them to Redis or a database before scaling out.
Refresh tokens are not one-time. A new pair is issued on every refresh, but the server is stateless, so the previous refresh token stays valid until it expires. Strict rotation needs a store.
Dynamic client registration is open, as DCR is by design, so it is treated as untrusted: redirect URIs must be
httpsor loopback and are matched exactly, codes are single use and bound to client + redirect URI + PKCE, and nothing is granted without a valid access key. If you need stricter control, add an allow-list of clients.Access keys are shared secrets, not an identity provider. For a team, put this behind your SSO or replace
auth/identity.ts.The data is fake. Money is USD, times are UTC, the business is a generic service shop.
License
This server cannot be deployed
Maintenance
Related MCP Connectors
- odooOAuthcom.odooconsole
Odoo ERP for AI agents: hosted OAuth endpoint, gated writes, one endpoint for every instance.
OAuth 2.1 short-link tools for AI agents with scoped tokens, approvals, audit logs, and revocation.
Hosted AI agents and approval-gated workflows on Gmail, Slack, GitHub and 1,000+ apps via OAuth.
Runtime permission, approval, and audit layer for AI agent tool execution.
Related MCP Servers
- FlicenseNot gradedqualityCmaintenanceA production-grade MCP server for a fictional digital bank, exposing tools for an AI copilot to service customers across the full risk spectrum from read-only lookups to money movement and destructive admin actions, with OAuth 2.1 security and a realistic dataset.13 npm1-
- AlicenseAqualityDmaintenanceEnables AI agents to manage a fictional B2B workspace SaaS (Tessera) with tools for ticketing, invoicing, customer management, and trial extensions, featuring a human-in-the-loop confirm pattern for safety.77 npmMIT
- AlicenseNot gradedqualityCmaintenanceEnables AI assistants to manage customer support for a fictional candle shop, including order lookup, customer file access, and safe refund processing with server-side safety rules.MIT
- FlicenseAqualityBmaintenanceEnables an AI agent to handle accounts payable tasks against a mock ERP, including reading and writing bills and vendors, checking duplicates, matching invoices, recommending approvals, and queuing payment releases, with configurable profiles that limit available tools.11-