JE

josephkamau32/erp-lite-mcp

Developer tools
9 stars 6 forks Quality 73 Trend 25MIT

A 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_approval state.
  • The agent cannot approve its own requisition. When it creates the requisition, a secure approval_token is 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_token redacted 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.sql only runs via docker-entrypoint-initdb.d on a fresh, empty Postgres volume. If you already have a pgdata volume from an earlier run, new tables (like audit_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.py binds 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.yml explicitly sets MCP_HOST=0.0.0.0 in the container environment. This is required inside Docker because binding to 127.0.0.1 inside 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.0 directly 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:

  1. MCP Streamable HTTP Transport (/mcp):

    • Protected by MCP_API_KEY via pure ASGI middleware.
    • Accepts either Authorization: Bearer or X-MCP-API-Key: .
    • Rejects unauthenticated or invalid requests with HTTP 401.
    • Fail-closed: If MCP_API_KEY is unset in the server environment, all requests to /mcp are rejected with HTTP 500.
  2. 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 (limit default 50, max 500).
    • Protected by ADMIN_API_KEY via X-Admin-Key header.
    • Fail-closed: HTTP 500 if ADMIN_API_KEY is not set.
  3. Claude Desktop / Local Stdio Transport:

    • When run in default stdio mode (python -m src.server), communication occurs over a local operating system pipe. No MCP_API_KEY is required because stdio is not exposed to the network.

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 run relative module paths correctly. Using the absolute path to .venv\Scripts\python.exe, with PYTHONPATH set 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 (SessionLocal monkeypatched 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_requisition never returns the token to the caller; approve_pending_requisition performs 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_token is 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 uses MCP_API_KEY. Production use would expand this to identity-based OAuth/OIDC and granular role checks.
  • Transport-level authentication ✅ Implemented. Streamable HTTP transport at /mcp requires MCP_API_KEY (fail-closed, constant-time compare). Default bind changed to 127.0.0.1. (Reported by Shiqiang Chen, see SECURITY.md).
  • Audit logging ✅ Implemented. Every tool call is recorded in an append-only audit_log table with tool name, redacted arguments, result (including failures), and timestamp — accessible via GET /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.

View the full README on GitHub

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" } } } }