Skip to main content
Glama
prodline-ai

erp-mcp

Official
by 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