erp-mcp
Officialby prodline-ai
README.md
# erp-mcp
An open-source [Model Context Protocol](https://modelcontextprotocol.io) server that
exposes **ERP** data — production orders, inventory, BOM, customers — to any MCP client
(Claude Desktop, Cursor, the MCP Inspector, or the Prodline analytics platform).
Backend-neutral by design: a clean adapter seam (`ERPBackend`) maps each ERP into one
product-neutral contract. It ships with a **mock backend** so you can run and test it
today with zero ERP access; planned real backends include **ERPNext**, 用友 **U8**, and
**畅捷通**.
> Part of [Prodline](https://github.com/prodline-ai). The connector is intentionally
> standalone and MIT-licensed — the proprietary value lives in Prodline's expertise
> engine, not here. We benefit from distribution.
---
## What it exposes
| Kind | Name | Description |
|---|---|---|
| Resource | `erp://orders` | All production orders |
| Resource | `erp://inventory` | Current stock levels |
| Resource | `erp://customers` | Customer master data |
| Resource | `erp://bom` | Bill of materials |
| Tool | `query_orders(status, customer, due_before, due_after)` | Filter orders |
| Tool | `get_bom(product_code)` | BOM for one product |
| Tool | `get_inventory(part_no)` | Stock for one part |
| Prompt | `overdue_orders` | Orders past due date |
| Prompt | `low_stock_alert` | Materials below reorder point |
| Prompt | `due_this_week` | Orders due in the next 7 days |
Order records carry `order_no` (the natural key), so an export drops straight into
Prodline's `production_orders` via the standard ingestion pipeline.
---
## Quick start (mock backend — no ERP needed)
```bash
pip install -e .
ERP_PRODUCT=mock erp-mcp # runs over stdio
```
**Configuration via `.env`:** both `erp-mcp` and `erp-agent` auto-load a `.env` file (found by
walking up from the working directory) on startup. Copy the template and fill it in:
```bash
cp .env.example .env # then edit .env with your ERP_PRODUCT + credentials
```
Real environment variables always win over `.env` (so an MCP client's `env` block or a shell
`export` overrides it). `.env` is gitignored; `.env.example` is the committed reference.
Inspect it interactively:
```bash
npx @modelcontextprotocol/inspector erp-mcp
```
Use it in **Claude Desktop** — add to `claude_desktop_config.json`:
```json
{
"mcpServers": {
"erp": {
"command": "erp-mcp",
"env": { "ERP_PRODUCT": "mock" }
}
}
}
```
Then ask Claude: *"Which orders are overdue?"* or *"List materials below reorder point."*
---
## Testing layers
This connector is built so the first two layers need **no ERP account at all**:
| Layer | What it proves | ERP account? |
|---|---|---|
| **L1** | Prodline's ingestion turns this server's output into `production_orders` | No — point Prodline at `ERP_PRODUCT=mock` |
| **L2** | The MCP surface is correct (resources/tools/prompts) | No — run under MCP Inspector / Claude Desktop |
| **L3** | The real ERP data path (auth + endpoints + data shapes) | Yes — see below |
Run the unit tests (L1/L2 logic, no client, no ERP):
```bash
pip install -e ".[dev]"
pytest
```
---
## Feeding Prodline — the on-prem sync agent
For automated sync into the Prodline platform, run the bundled agent (`erp-agent`)
on the same LAN. It reads the ERP locally via the connector and **pushes outbound** to
Prodline — Prodline never connects into the customer network (firewall-friendly).
```bash
PRODLINE_URL=https://app.prodline.ai \
PRODLINE_TOKEN=<per-tenant connector token> \
ERP_PRODUCT=mock \
erp-agent
```
Each run:
1. fetches the **collection plan** from Prodline (`GET /api/connector/collection-plan`) — what entities to pull, `delta` vs `snapshot`, schedule
2. for each due entity, pulls rows from the ERP (delta passes filter `updated_after=<local watermark>`)
3. **pushes** the batch outbound (`POST /api/connector/ingest`), which reconciles into Prodline (idempotent upsert by `order_no`, user-edited fields sticky)
4. advances the local watermark (`AGENT_STATE_FILE`)
Run it continuously with the built-in loop, or one-shot under an external scheduler:
```bash
erp-agent # one pass, then exit (for cron / Task Scheduler)
erp-agent --interval 1800 # loop: sync every 30 min until stopped (or AGENT_INTERVAL_SECONDS=1800)
```
A `delta` cadence keeps data fresh cheaply (orders / work orders / quality inspections pull only
rows changed since the last watermark); the weekly `snapshot` pass reconciles edits. Inventory / BOM /
routings are snapshot pulls (current-state / low-churn). The agent is a thin generic pump — *what's
available* comes from the connector, *what to collect* comes from the server-pushed plan — so it
rarely needs updating.
### Protocol & compatibility
This repo and the Prodline platform are separate — the only coupling is the
`/api/connector/*` HTTP contract, so it's **versioned** (`src/erp_mcp/protocol.py`,
`PROTOCOL_VERSION`) and checked on every sync via the `X-Connector-Protocol` header:
- The agent sends its version and **refuses to sync** against a server on an incompatible
MAJOR (pushing mismatched row shapes would silently corrupt data). The server likewise
answers **`426 Upgrade Required`** to an incompatible agent.
- Same MAJOR ⇒ compatible; a MINOR bump is a backward-compatible addition (new optional
field, new entity). Bump MAJOR only for a breaking shape change.
If you change a request/response shape, bump `PROTOCOL_VERSION` here **and**
`CONNECTOR_PROTOCOL_VERSION` on the platform in lockstep. `tests/test_protocol.py` (here)
and the platform's `test_connector_contract.py` pin both ends.
---
## Going live (L3) — implementing a real backend
The server only ever calls the `ERPBackend` interface (`src/erp_mcp/backends/base.py`).
Adding a real product = implementing that interface.
### ERPNext / Frappe (`ERP_PRODUCT=erpnext`) — implemented ✅
- Read-only via the Frappe REST API. In ERPNext: **user → API Access → Generate Keys**, then set
`ERPNEXT_URL` / `ERPNEXT_API_KEY` / `ERPNEXT_API_SECRET`.
- Mappings: Sales Order (+ Sales Order Item) → orders; BOM (+ BOM Item) → BOM; Bin (+ Item) →
inventory; Customer → customers. **Manufacturing depth (Phase B):** Work Order → work orders;
BOM Operation → routing steps; Quality Inspection → quality inspections (→ Prodline's B19 tables).
See `src/erp_mcp/backends/erpnext.py`.
- Stand up a dev instance with [frappe_docker](https://github.com/frappe/frappe_docker) and load the
manufacturing demo data to test end-to-end. Mapping is unit-tested (mocked) in
`tests/test_erpnext_backend.py` — no live instance needed for CI.
### 用友 U8 (`ERP_PRODUCT=u8`) — stub
- **Free developer test accounts** during development — you can build and integration-test
without a paying customer or a purchased OpenAPI module (the customer buys that only for
production: ~¥3,000/yr operating mode).
- Docs: <https://open.yonyouup.com/> · Support: <https://u8open.yonyou.com/>
- Implement `src/erp_mcp/backends/u8.py`.
### 畅捷通 好会计 / T+ (`ERP_PRODUCT=chanjet`)
- 好会计 offers a **free trial** — a real instance you can self-serve test against. Best fit
for SME manufacturing.
- Docs: <https://open.chanjet.com/> · T+ OpenAPI: <https://open.chanjet.com/docs/file/apiFile/tcloud>
- Implement `src/erp_mcp/backends/chanjet.py`.
> **Heads-up:** Chinese open platforms commonly require enterprise real-name verification
> (企业实名认证) to register a developer app, even for sandbox keys. Have your company
> identity ready.
Each backend method maps one ERP API call → a normalized model in
`src/erp_mcp/models.py`. The MCP surface and all downstream consumers stay unchanged.
```
src/erp_mcp/
├── server.py # MCP resources / tools / prompts (never changes per-product)
├── models.py # normalized Order / BomLine / InventoryItem / Customer
└── backends/
├── base.py # ERPBackend protocol — the adapter seam
├── __init__.py # get_backend() — selects by ERP_PRODUCT
├── mock.py # in-memory fixtures (L1/L2)
├── u8.py # 用友 U8 (L3 — implement me)
└── chanjet.py # 畅捷通 (L3 — implement me)
```
## License
MIT
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues