ERP-lite MCP Server
This server exposes ERP read-only queries plus a human-in-the-loop purchase-requisition workflow with audit logging.
Query sales orders by status (
get_open_orders).Check inventory levels and reorder status (
check_inventory).List all inventory items below their reorder point (
get_low_stock_items).Create purchase requisitions that start in
pending_approvalstate (create_requisition).Approve pending requisitions (
approve_pending_requisition) — per the README this requires a human-held approval token, though the provided schema lists onlyrequisition_idandapproved_by.View pending requisitions with approval tokens and retrieve audit-log entries via admin-only REST endpoints (
X-Admin-Key).
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., "@ERP-lite MCP ServerCheck inventory levels for material M-100"
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.
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.
Related MCP server: PyerP MCP Server
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)" --> FastMCPDemo
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 valuesThen start the containers:
docker compose upThis 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 |
| Read | Retrieves sales orders by status. |
| Read | Checks inventory level and computes whether it's below the reorder point. |
| Read | Identifies all inventory items below their reorder threshold. |
| Write | Creates a purchase requisition in |
| 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: Bearer <MCP_API_KEY>orX-MCP-API-Key: <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.
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.
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 pytestCovers 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.
Available Tools
5 toolsapprove_pending_requisitionB
Approve a pending purchase requisition. This MUST be triggered by a human.
| Name | Required | Description | Default |
|---|---|---|---|
| approved_by | Yes | ||
| requisition_id | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| status | Yes | |
| quantity | Yes | |
| created_at | Yes | |
| approved_at | No | |
| material_id | Yes | |
| requested_by | Yes | |
| requisition_id | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the full burden for transparency. It only discloses the human-trigger requirement, but does not mention side effects, irreversibility, permission requirements, or what the approval action entails. This is a significant gap for a mutation tool.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is extremely concise with two sentences. The first states the purpose clearly, and the second emphasizes a critical human-in-the-loop constraint. Every word earns its place.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
This is an approval action with side effects, yet the description provides minimal context. It lacks prerequisites, postconditions, and any explanation of the approval workflow. The existence of an output schema may cover return values, but the overall description is insufficient for an AI agent to safely and correctly invoke the tool in a given context.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, and the description provides no explanations for 'requisition_id' or 'approved_by'. The parameter names are somewhat self-explanatory, but the tool does not compensate for the lack of schema documentation, leaving meaning entirely to inference.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the action ('Approve a pending purchase requisition') with a specific verb and resource. It distinguishes itself from siblings like create_requisition and get_open_orders, and the extra note about human triggering adds clarity.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description provides a clear usage constraint: it MUST be triggered by a human, implying it should not be used in automated flows. However, it does not explicitly name alternatives or conditions under which this tool is preferred over others.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
check_inventoryC
Check the inventory level for a specific material ID.
| Name | Required | Description | Default |
|---|---|---|---|
| material_id | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| warehouse | Yes | |
| description | Yes | |
| material_id | Yes | |
| reorder_point | Yes | |
| quantity_on_hand | Yes | |
| below_reorder_point | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden for behavioral disclosure. It states a simple check but does not describe whether the operation is read-only, what response format to expect (though an output schema exists), how invalid material IDs are handled, or any authentication requirements. The description adds little beyond the tool name.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is a single, concise sentence that directly states the tool's purpose with no redundant or irrelevant information. It is appropriately sized for a simple one-parameter tool.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a simple tool with one parameter and an output schema, the description is minimally adequate. However, it lacks differentiation from sibling tools and provides no situational context, making it incomplete for an agent trying to select among similar inventory-related tools.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The description mentions 'specific material ID' but provides no additional meaning about the parameter, such as format, source, or constraints. Since schema description coverage is 0%, the description was expected to compensate, but it only restates the parameter name without clarifying its semantics.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool checks inventory level for a specific material ID, which is a specific verb+resource. It inherently differentiates from list-style siblings like get_low_stock_items and get_open_orders, but does not explicitly name alternatives or scope limitations.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
No guidance is provided on when to use this tool versus the sibling tools. It does not mention any exclusions, prerequisites, or alternative tools, leaving the agent to infer its appropriate context from the name alone.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
create_requisitionA
Create a new purchase requisition (pending approval). The agent CANNOT approve its own requisition.
| Name | Required | Description | Default |
|---|---|---|---|
| quantity | Yes | ||
| material_id | Yes | ||
| requested_by | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| status | Yes | |
| quantity | Yes | |
| created_at | Yes | |
| approved_at | No | |
| material_id | Yes | |
| requested_by | Yes | |
| requisition_id | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the burden of behavioral disclosure. It discloses the key constraint that the agent cannot approve its own requisition, which is non-obvious and valuable. However, it does not mention other potential side effects such as validation behavior or permission requirements.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is two sentences long and immediately states the core purpose. The second sentence adds crucial behavioral context without unnecessary words. No filler or redundancy.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The tool has a simple create operation with 3 required parameters and an output schema, so return value details are likely covered. The description provides the essential workflow context (pending approval) and the critical self-approval restriction. It does not address error handling or preconditions, but given the presence of an output schema and sibling context, this is sufficient for a minimal viable description.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The schema provides no descriptions for the parameters, and the description adds none either. The agent must rely solely on parameter names (material_id, quantity, requested_by), leading to ambiguity about required formats, constraints (e.g., positive quantity), or source of values. The description does not compensate for the low schema coverage.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the action ('Create') and the resource ('purchase requisition'), with a parenthetical clarifying its initial state ('pending approval'). This uniquely distinguishes it from sibling tools like approve_pending_requisition, which perform a different action on the same entity.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description implicitly indicates this tool is for creating requisitions but does not explicitly state when to use it over alternatives. The note about not being able to approve its own requisition implies a workflow dependency but offers no explicit usage guidance or exclusions.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_low_stock_itemsA
List all inventory items where the quantity on hand is below the reorder point.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the full burden. It clearly states the tool is a listing operation (read-only) and defines the filter. It doesn't cover edge cases like empty results or pagination, but for a simple list tool, this is sufficient.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is a single sentence that is direct and front-loaded with the primary action and target. Every word is necessary, with no filler or redundancy.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool has no parameters and an output schema exists, the description fully specifies the tool's behavior. The list of all low-stock items is unambiguous, and no further context is required.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The tool has zero parameters, so the schema is trivially complete. The description adds no parameter details because there are none to explain, aligning with the baseline score of 4 for no-parameter tools.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description uses a specific verb ('List') and identifies the exact resource (inventory items) and condition (quantity on hand below reorder point). This clearly distinguishes it from sibling tools like get_open_orders and check_inventory.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description clearly implies the use case: when you need all items with low stock. It does not explicitly name alternatives or exclusions, but the context (sibling tools) makes the distinction clear enough, earning a 4 rather than a 5.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_open_ordersC
Retrieve a list of sales orders by status.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | ||
| status | No | open |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description must disclose behavior but only states the basic retrieval action. It fails to mention pagination, default status behavior, or any side-effect or safety implications. A read operation is inferred, but this is not disclosed.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is a single, front-loaded sentence with no fluff. It is concise, though slightly under-specified, which is acceptable for a simple tool.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Despite having an output schema, the description lacks critical context such as parameter semantics, default behavior, and relationship to sibling tools. It is a minimal viable description but leaves the agent to infer how to invoke it correctly, especially regarding 'status' values.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description must compensate. It only mentions 'status' but does not explain valid values or default, and it omits 'limit' entirely. The schema provides defaults but no semantic meaning.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool retrieves a list of sales orders by status, using a specific verb and resource. It distinguishes from all sibling tools which deal with inventory or requisitions, not sales orders.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description gives no guidance on when to use this tool versus alternatives, nor does it mention exclusions or prerequisites. The only implied usage is that it's for sales orders, but no explicit context is provided.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
5 tool updates
v0.1.0- First observed
approve_pending_requisition - First observed
check_inventory - First observed
create_requisition - First observed
get_low_stock_items - First observed
get_open_orders
TDQS
Scored across 5 tools
Each tool targets a distinct resource and action: reading sales orders, checking inventory, listing low stock, creating requisitions, and approving requisitions. There is no overlap or ambiguity between tool purposes.
All names follow a clear verb_noun pattern in snake_case (get_open_orders, check_inventory, get_low_stock_items, create_requisition, approve_pending_requisition). The mix of 'get' and 'check' is minor and still consistent as retrieval verbs.
Five tools is well-scoped for an ERP-lite server covering sales, inventory, and purchasing. Each tool fills a distinct role, and the count is neither too thin nor bloated.
The tool surface has significant gaps: there is no way to view order details, update or close orders, adjust inventory, list pending requisitions, or create purchase orders after approval. The workflow ends abruptly after requisition approval.
Maintenance
Related MCP Connectors
MCP server unifying ERPs, CRMs, APIs and knowledge base for Claude, ChatGPT and Gemini.
Let AI agents query data and act across all your business apps via MCP.
MCP server connecting AI agents to 100+ apps (Gmail, Slack, Notion, GitHub) via one-click OAuth.
Unified MCP Server is a remote MCP connector for AI agents and vertical AI products that provides access to 22,000+ authorized SaaS tools across 400+ integrations and 24 categories directly inside LLMs (Claude, GPT, Gemini, Cohere). Tools operate only on explicitly authorized customer connections, enabling agents to safely read and write against live third-party systems.
Related MCP Servers
- AlicenseBqualityDmaintenanceAn MCP server that enables AI assistants to interact with Odoo ERP apps like Inventory, CRM, Sales, and Manufacturing. It allows users to read, create, and manage Odoo records and workflows using natural language commands.256 npm1ISC
- FlicenseNot gradedqualityNot gradedmaintenanceAn MCP server that enables LLM agents to interact with PyerP ERP systems via a REST API. It allows users to search, read, create, and update ERP records such as inventory, clients, and users using natural language.-
- AlicenseNot gradedqualityNot gradedmaintenanceAn enterprise-grade MCP server that enables AI agents to execute SAP RFC functions and read business data securely through the Model Context Protocol.MIT
- FlicenseNot gradedqualityBmaintenanceMCP server that enables AI platforms to search products, customers, and warehouses, and prepare and submit sales orders to a fixed ERP endpoint with per-session bearer authentication.-