
josephkamau32/erp-lite-mcp
Developer toolsA Model Context Protocol (MCP) server exposing ERP-style data (sales orders, inventory, purchase requisitions) to AI agents — with a real human-in-the-loop approval gate on write actions.
Overview
An enterprise-ready Model Context Protocol (MCP) server that exposes ERP functionalities to AI agents. Built as a portfolio project to demonstrate AI/ML engineering maturity, this project features a realistic data schema, a genuinely enforced human-in-the-loop approval workflow for write actions, and a full compliance-style audit trail. As enterprise AI adoption accelerates, providing LLMs with direct read/write access to ERPs is becoming essential. However, autonomous agents should not execute consequential operations (like creating purchase orders or altering system configurations) without human oversight. This server demonstrates a robust "human-in-the-loop" pattern: - The AI agent can query , check , and identify using its read-only tools. - When an agent decides to replenish stock, it can only propose a in a pending_approval state. - When it creates the requisition, a secure approval_token is generated and saved to the database, but is never returned to the agent.
README
ERP-lite MCP Server
An enterprise-ready Model Context Protocol (MCP) server that exposes ERP functionalities to AI agents. Built as a portfolio project to demonstrate AI/ML engineering maturity, this project features a realistic data schema, a genuinely enforced human-in-the-loop approval workflow for write actions, and a full compliance-style audit trail.
Overview
As enterprise AI adoption accelerates, providing LLMs with direct read/write access to ERPs is becoming essential. However, autonomous agents should not execute consequential operations (like creating purchase orders or altering system configurations) without human oversight.
This server demonstrates a robust “human-in-the-loop” pattern:
- The AI agent can query open sales orders, check inventory levels, and identify low-stock items using its read-only tools.
- When an agent decides to replenish stock, it can only propose a purchase requisition in a
pending_approvalstate. - The agent cannot approve its own requisition. When it creates the requisition, a secure
approval_tokenis generated and saved to the database, but is never returned to the agent. - A human administrator can view pending requisitions and their tokens via a dedicated, authenticated REST endpoint (
GET /admin/pending-requisitions) that sits entirely outside the MCP tool surface — no agent can reach it. - The human retrieves the token through that endpoint and supplies it back to approve the requisition, closing the loop with a real access-control check (constant-time token comparison), not just a naming convention.
- Every tool call — successful or failed — is written to an append-only audit log, with sensitive values like
approval_tokenredacted before persistence.
Architecture
graph TD
Client[Claude Desktop / Custom Client] -- "MCP (stdio or Streamable HTTP with Bearer Token)" --> FastMCP[FastMCP Server]
FastMCP -- "SQLAlchemy" --> DB[(PostgreSQL Database)]
DB --> Seed[Seed Data]
Admin[Human Admin] -- "X-Admin-Key (REST, outside MCP surface)" --> FastMCP
Demo
https://github.com/user-attachments/assets/8584d882-ecb2-43f2-bd31-7a095bedd25c
Checking low-stock inventory → agent proposing a purchase requisition → retrieving the approval token via the admin endpoint → approving the requisition → resulting audit log entry.
Quick Start (Docker)
First, create your .env file and set secure keys for both the MCP transport and the Admin API:
cp .env.example .env
# Edit .env and set MCP_API_KEY and ADMIN_API_KEY to secure, independent values
Then start the containers:
docker compose up
This spins up:
- A PostgreSQL database, health-checked, pre-seeded with realistic enterprise data for sales orders, inventory, requisitions, and an empty audit log table.
- The MCP server, exposing the Streamable HTTP transport on port 8000, protected with
MCP_API_KEY.
⚠️ Upgrading from a previous version?
seed_data.sqlonly runs viadocker-entrypoint-initdb.don a fresh, empty Postgres volume. If you already have apgdatavolume from an earlier run, new tables (likeaudit_log) won’t be created automatically. To pick up schema changes:docker compose down -v docker compose up --build
Network Binding & Docker vs. Host Execution
- Local Host Execution (Safe Default): By default,
server.pybinds to loopback (127.0.0.1), ensuring the server is not reachable from other machines on your local network unless explicitly configured. - Docker Execution (
MCP_HOST=0.0.0.0):docker-compose.ymlexplicitly setsMCP_HOST=0.0.0.0in the container environment. This is required inside Docker because binding to127.0.0.1inside a container isolates it entirely within the container’s private network namespace, making it unreachable via Docker’s port mapping (8000:8000). - ⚠️ Warning: Never expose
MCP_HOST=0.0.0.0directly to public networks or untrusted LANs without verifying that transport authentication (MCP_API_KEY) or an authenticating reverse proxy is active.
Tools Exposed (MCP)
| Tool | Type | Description |
|---|---|---|
get_open_orders(status="open", limit=20) |
Read | Retrieves sales orders by status. |
check_inventory(material_id) |
Read | Checks inventory level and computes whether it’s below the reorder point. |
get_low_stock_items() |
Read | Identifies all inventory items below their reorder threshold. |
create_requisition(material_id, quantity, requested_by) |
Write | Creates a purchase requisition in pending_approval state; silently generates and stores an approval_token. |
approve_pending_requisition(requisition_id, approved_by, approval_token) |
Write | Approves a pending requisition — only succeeds with the correct token, sourced from the human-only admin endpoint below. |
All tool calls, successful or failed, are recorded in the append-only audit log. Sensitive arguments (e.g. approval_token) are redacted before being persisted.
Authentication & Access Boundaries
The server enforces strict, independent access controls across surfaces:
-
MCP Streamable HTTP Transport (
/mcp):- Protected by
MCP_API_KEYvia pure ASGI middleware. - Accepts either
Authorization: BearerorX-MCP-API-Key:. - Rejects unauthenticated or invalid requests with HTTP 401.
- Fail-closed: If
MCP_API_KEYis unset in the server environment, all requests to/mcpare rejected with HTTP 500.
- Protected by
-
Admin REST Endpoints (outside MCP tool surface):
GET /admin/pending-requisitions— lists pending requisitions with approval tokens.GET /admin/audit-log?limit=50— returns the most recent audit log entries, newest first (limitdefault 50, max 500).- Protected by
ADMIN_API_KEYviaX-Admin-Keyheader. - Fail-closed: HTTP 500 if
ADMIN_API_KEYis not set.
-
Claude Desktop / Local Stdio Transport:
- When run in default stdio mode (
python -m src.server), communication occurs over a local operating system pipe. NoMCP_API_KEYis required because stdio is not exposed to the network.
- When run in default stdio mode (
Testing Locally
Custom Python client (Streamable HTTP with Bearer Auth)
Set your MCP_API_KEY environment variable or pass --api-key:
# Using environment variable
export MCP_API_KEY="your_secret_mcp_api_key_here"
python client.py
# Or via CLI argument
python client.py --api-key "your_secret_mcp_api_key_here"
Claude Desktop (stdio transport)
Because stdio is a direct local process pipe, it does not require network credentials:
{
"mcpServers": {
"erp-lite": {
"command": "/absolute/path/to/erp-lite-mcp/.venv/Scripts/python.exe",
"args": ["-m", "src.server"],
"env": {
"PYTHONUNBUFFERED": "1",
"PYTHONIOENCODING": "utf-8",
"PYTHONPATH": "/absolute/path/to/erp-lite-mcp"
}
}
}
}
Windows note: Claude Desktop’s sandboxing frequently fails to resolve
uv runrelative module paths correctly. Using the absolute path to.venv\Scripts\python.exe, withPYTHONPATHset explicitly, is the reliable configuration.
Running Unit Tests
uv run pytest
Covers tool logic, the full requisition lifecycle (create → pending → wrong-token rejection → correct-token approval), and audit logging (including token redaction and failed-attempt capture).
All tests run against an in-memory SQLite database (
SessionLocalmonkeypatched in fixtures) — no Postgres or Docker required. CI (GitHub Actions) uses the same approach, so no database service is configured in the workflow.
CI
Every push and pull request to main runs the full pytest suite via GitHub Actions.
Design Decisions Worth Knowing
- The approval gate is an access-control mechanism, not a naming convention.
create_requisitionnever returns the token to the caller;approve_pending_requisitionperforms a constant-time comparison (secrets.compare_digest) against the stored value, so there’s no timing side-channel and no path by which the same agent session can complete both halves of the workflow on its own. - The admin surface is intentionally separate from the MCP tool surface. Tokens and audit history are retrievable only via authenticated REST routes an agent has no tool access to — the trust boundary is structural, not just a prompt-level instruction telling the agent not to self-approve.
- Audit logging is fire-and-forget but not silent. A logging failure never blocks a real tool response, but is written to server stderr, so an audit pipeline failure is observable in ops rather than invisible.
Future Enhancements
- Token security: the
approval_tokenis currently stored as plaintext so the admin endpoint can serve it directly. A production version would deliver it via a side channel (email/Slack) at creation time and store only a salted hash, never exposing plaintext through any API. - Authentication & RBAC: the admin routes currently use a shared-secret
ADMIN_API_KEY, while the MCP transport usesMCP_API_KEY. Production use would expand this to identity-based OAuth/OIDC and granular role checks. Transport-level authentication✅ Implemented. Streamable HTTP transport at/mcprequiresMCP_API_KEY(fail-closed, constant-time compare). Default bind changed to127.0.0.1. (Reported by Shiqiang Chen, see SECURITY.md).Audit logging✅ Implemented. Every tool call is recorded in an append-onlyaudit_logtable with tool name, redacted arguments, result (including failures), and timestamp — accessible viaGET /admin/audit-log.- Policy search resource: expose procurement policy documents to the agent as an MCP Resource with semantic search, so the agent can check policy context before proposing a requisition.
Security
Please see SECURITY.md for vulnerability reporting guidelines and details on past security advisories (including GH-001 responsible disclosure credit to Shiqiang Chen).
License
This project is licensed under the MIT License - see the LICENSE file for details.
Install
This server does not publish a one-line install command.
Open the repository installation guide New to MCP? Start with the mcpskill.org guide →Configuration
{
"mcpServers": {
"erp-lite": {
"command": "/absolute/path/to/erp-lite-mcp/.venv/Scripts/python.exe",
"args": ["-m", "src.server"],
"env": {
"PYTHONUNBUFFERED": "1",
"PYTHONIOENCODING": "utf-8",
"PYTHONPATH": "/absolute/path/to/erp-lite-mcp"
}
}
}
}