mcp-ops-server

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.

Category
Visit Server

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 → shipped is the only path to shipped.
  • Every mutation is written to audit_log with 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

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.

Official
Featured
TypeScript
Magic Component Platform (MCP)

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.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

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.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

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.

Official
Featured
TypeScript
Kagi MCP Server

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.

Official
Featured
Python
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

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.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured