mcp-ops-server
A reference MCP server that gives an LLM agent safe write access to a business database, managing orders, production state, and FIFO stock with derived state and idempotent imports.
README
mcp-ops-server
A reference implementation of an MCP server that gives an LLM agent real, safe write access to a business database — orders arriving from sales channels, production state per item, and FIFO material stock.
It is deliberately small (about 1,000 lines including tests) and deliberately opinionated. Every design decision below came from running a system shaped like this one in a real operation, where a wrong tool call costs money rather than a red test.
pip install -r requirements.txt
python -m ops # runnable demo, no MCP client needed
pytest # 29 tests
What this demonstrates
Most "AI integration" projects wire a chatbot on top of a REST API and stop. The hard part is not calling the model — it is designing a tool surface that an autonomous agent cannot corrupt, over data that stays correct when the same file gets imported for the fourth time.
Four rules carry most of that weight:
1. State is derived, never assigned
An order's status is computed from its items every time an item moves:
| Derived | meaning |
|---|---|
open |
every item still todo |
in_progress |
some item started |
ready |
every item done |
Only genuine human decisions — packing, shipped, cancelled — are
written directly, and derivation refuses to overwrite them. There is no
code path where an agent sets ready on an order whose items are untouched,
because there is no code path where anything sets ready at all.
2. Re-importing a file must never destroy work
Channel exports overlap: today's file contains yesterday's orders. Ingestion
upserts on (channel, external_id), and item rows are replaced wholesale —
the export is the source of truth for what was ordered. But production
state is snapshotted per SKU first and re-applied afterwards.
Without that snapshot, the 6am import silently resets everything finished the day before. When quantity drops between exports, the most advanced units are the ones kept: throwing away finished work is worse than throwing away pending work.
def test_reimport_preserves_production_state(conn):
... # tests/test_ops.py
3. Header data is not row data
Marketplace exports repeat the order total and the fee on every item line. Summing them multiplies revenue by the item count — the most common and most expensive bug in this category of importer. Fees are read once per order group, and there is a test that fails if that ever regresses.
An actual fee, once known, never regresses to an estimate.
4. Balances are sums, not counters
Stock has no stored balance. Receipts create FIFO lots; consumption walks them
oldest-first and returns what the material really cost. The balance is
SUM(in) - SUM(out) over an append-only movement log, so it cannot drift from
its own history. Insufficient stock raises rather than going negative or
consuming partially.
The tool surface
Eleven tools. The interesting part is not the count — it is that the docstrings are longer than the functions.
| Tool | Purpose |
|---|---|
board_list |
open orders with items, most urgent deadline first |
item_set_state |
move one item todo → doing → done, optionally drawing material |
order_advance |
apply a manual status, refusing transitions that skip production |
ingest_csv |
idempotent import of a channel export |
stock_status / stock_receive / stock_consume |
derived balance, FIFO lots, real cost |
material_upsert / sku_upsert |
catalogue |
pending_recipes |
SKUs seen in orders with no recipe yet, most-sold first |
audit_tail |
who changed what, when |
Why the docstrings are long
A tool's docstring is not a comment. It is the entire interface contract the model sees at call time. The model cannot read the source, cannot inspect the schema, and cannot ask a clarifying question before choosing arguments. Every ambiguity left in a docstring becomes a wrong tool call in production.
What actually works:
- say what the tool is for, not only what it does;
- name the units and the exact allowed values;
- state what it refuses and why;
- point at the tool to call instead when this one is the wrong choice.
Compare a typical generated docstring with the one in mcp_server.py:
# Typical — technically accurate, operationally useless
def stock_receive(material_code: str, qty: float, unit_cost: float) -> dict:
"""Receive stock for a material."""
# What the model actually needs
def stock_receive(material_code: str, qty: float, unit_cost: float) -> dict:
"""Register a purchase as a new FIFO lot.
Args:
material_code: must already exist — create it with `material_upsert`.
qty: amount received, in the material's own unit (g, ml, unit).
unit_cost: price per that same unit, not per package. If you bought a
1kg spool for 90.00 and the unit is grams, unit_cost is 0.09.
"""
The second version is why an agent stops recording a 1kg spool as costing 90.00 per gram.
Safety rails
- Destructive and terminal transitions are validated, not trusted:
ready → packing → shippedis the only path to shipped. - Every mutation is written to
audit_logwith its actor (agent,human,ingest), so agent actions can always be separated from human ones after the fact. - A missing recipe never blocks production; it only leaves costing incomplete
and puts the SKU at the top of
pending_recipes. Catalogue maintenance is a queue you drain by frequency, not a hundred forms you fill before starting.
Architecture
ops/
├── db.py schema + forward-only idempotent migrations
├── ingest.py CSV parsing: dirty money, tabs, locale dates, grouping
├── services.py derived state, idempotent upsert, FIFO, audit
├── mcp_server.py the tool surface (docstrings are the contract)
└── __main__.py runnable demo
tests/test_ops.py 29 tests, one per way this goes wrong
Dependencies: mcp and pytest. Storage is SQLite through the standard
library — no ORM, because at this size the schema is the documentation.
migrate() runs on every connection and is safe to call repeatedly: a fresh
database and one from three versions ago converge to the same shape. Table
reshaping happens in a _pre_migrate hook that runs before the schema
script, because creating an index on a column that does not exist yet is the
classic way to brick a startup on real data.
Registering it with an MCP client
claude mcp add ops -- python -m ops.mcp_server
Built against mcp >= 2.0, where the server class is MCPServer. On mcp 1.x
the equivalent import is from mcp.server.fastmcp import FastMCP.
What is deliberately absent
- No web UI. The tools are the product; a UI would be the least interesting 400 lines here.
- No auth or multi-tenancy. Single operator by design. Adding tenancy would mean a tenant filter on every query — a real concern, but a different demonstration.
- No LLM calls anywhere in this codebase. Arithmetic is deterministic and belongs in code. The model's job is to decide what to do and to explain the result — not to add up a stock balance.
Provenance
Extracted from the design of a private system I built and run daily for my own manufacturing operation — a 35-tool MCP server that ingests marketplace order exports, schedules production against shipping deadlines, and tracks material consumption. This repository reimplements that architecture on a neutral domain with synthetic data; it shares no code, data or business logic with the original.
Built by Layson Santos — 13 years of Java and distributed systems, now building systems where the LLM is part of the architecture rather than a chatbot bolted on top.
MIT licensed.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.