An agent team you can read: invoice-intake-mcp.
Client systems stay private, so we published one instead. A complete, runnable agent system for supplier invoice intake, built with the rules we use on engagements.
Why we published it
Most of what we build belongs to the client, so it cannot be shown. invoice-intake-mcp exists to make the patterns visible: it is a complete, runnable agent system for one real back-office process, supplier invoice intake, built with the same rules we apply on client work. It is MIT licensed and small on purpose. The ERP behind it is a JSON file; everything above that file is the shape we use in production.
It is the reference implementation for the rules described in Designing MCP servers and tools that agents can use safely.
The process it runs
Supplier invoices arrive in an inbox. Each one has to be read, matched to a purchase order, checked against a tolerance, and either posted to the ERP or sent to a person. The interesting part is not the happy path; it is everything else: an invoice with no PO reference, a price variance above tolerance, goods that were never received, a total above the approval limit.
The system's first run states each of those outcomes explicitly rather than guessing:
plan: 4 new invoices -> extract, resolve vendor, match, stage, post-or-approve
invoice_2291.pdf: staged STG-… -> WAITING for APR-… (7,420 EUR > 5,000 limit)
invoice_2292.pdf: match=goods_not_received -> REVIEW
invoice_2293.pdf: match=variance {"variance_pct": 2.34} -> REVIEW
invoice_2294.pdf: no PO reference -> REVIEW (ask requester)
How it is built
Two servers, not one
Reads and reversible staging live in a --role read server. The single irreversible operation, posting an invoice, lives in a separate --role commit server with its own credential. Specialist agents are given only the read server; the orchestrator is the only component that connects to commit. The separation is enforced by which server an agent can reach, not by an instruction asking it to behave.
Tools model the business operation
The tools are named after what a competent employee would do: erp_match_po, erp_stage_invoice, erp_post_invoice. There is no generic PATCH /invoices for an agent to reason its way into. Schemas are strict: currencies are enumerated, records are addressed by id rather than by name, and an empty result is a valid, explicit answer rather than an error.
Typed errors
Every failure comes back as a small, closed set the agent can act on, with an explicit retry hint:
{"error": "approval_required" | "validation_failed" | "business_rule" | "not_found",
"retryable": false, ...}
This is the difference between an agent that escalates correctly and an agent that loops.
The approval gate is server-side
erp_post_invoice refuses to act unless a recorded approval exists, whatever the client believes or was told. The human decision is a separate CLI step that writes that approval. A prompt cannot talk its way past it, because the check does not live in the prompt.
An audit log an auditor can read
audit.jsonl is append-only and records the actor, the tool, the record ids and the approval reference for every call. Months later, the question "why was this invoice posted" has a written answer.
Tool results are untrusted input
The extraction tool says so in its own description, and the evaluation set includes a prompt-injection case: a document that tries to instruct the agent. Treating tool output as data rather than as instructions is a design rule, and it is tested.
The orchestrator is deterministic
The hub plans, routes and verifies, and it replays without an API key. The places where a model belongs are marked in the code: ambiguous vendor candidates, free-text remarks, the note back to the requester. The match rule, the tolerance and the approval limit are not among them. Policy stays in code where it can be reviewed.
Tests and replayable evals
pytest covers the core operations and the gate. evals/run.py replays decision cases from cases.jsonl on every change, which is what makes a prompt, model or tool change a measurable event rather than a hopeful one.
Where to find it
- Source: github.com/justdukkan/invoice-intake-mcp (MIT)
- PyPI:
uvx invoice-intake-mcp --role read|commit - MCP Registry:
com.justdukkan/invoice-intake-mcp
What it demonstrates
Read it as a worked example of the four things we do on client engagements: roles drawn before code, tools written for the business operation, integration boundaries that carry the permissions, and an evaluation set that makes maintenance possible. The same structure, pointed at a real ERP instead of a JSON file, is what a production agent team looks like.
We look at the process, design the agent team it needs, and decide together whether it is worth building.
Get in touch → Book a call