Skip to main content
Glama
Medhaj-ops

mcp-safe-inventory-demo

by Medhaj-ops
README.md
# Safe MCP patterns for agents that mutate business state

A minimal MCP server showing three patterns for giving AI agents safe
write-access to real business systems: **phase-gating**,
**validation-before-mutation**, and **structured audit logging**.

This is a demo, not a product. The domain (a toy inventory + purchase
order system) exists only to give the patterns something concrete to
apply to — the patterns themselves are the point, and they're
domain-agnostic.

## Why this exists

AI agents are increasingly given write access to real systems — orders,
inventory, CRM records, forecasts. The common failure mode isn't that
the underlying LLM is unreliable in some abstract sense; it's that
implementations often trust the model to "do the right thing" with no
structural guardrail behind it. When an agent hallucinates a state, gets
manipulated by a prompt injection, or just gets the order of operations
wrong, the result is a silent, incorrect write to a system that a human
now has to notice, diagnose, and fix after the fact.

The three patterns below aren't novel research — they're standard
engineering discipline for anything touching production state,
applied specifically to agent tool calls.

## The three patterns

**1. Phase gating.** A mutating operation (`submit_purchase_order`)
cannot succeed unless a corresponding read/preview operation
(`draft_purchase_order`) happened first, in the same session. This is
enforced in code — a hard error, not a prompt instruction the model can
ignore or be talked out of. The error message tells the caller exactly
what to do next, which is what lets an agent self-correct instead of
just failing.

**2. Validation-before-mutation.** Every check — does the SKU exist, is
the quantity sane, does this exceed a sane order threshold — runs
against a *pure* representation of the proposed change, before anything
is written. Validation never has side effects. And critically: all
checks run regardless of earlier failures, so a caller sees every
problem at once instead of fixing one, resubmitting, and hitting the
next.

**3. Structured audit logging.** Every tool call is logged — including
blocked and rejected ones, not just successful mutations. An audit trail
that's silent about denied attempts is missing exactly the events most
worth reviewing later: what did the agent try that got stopped, and why.

## The demo

- [`examples/happy_path.md`](examples/happy_path.md) — draft → review →
  submit, with real captured output
- [`examples/blocked_paths.md`](examples/blocked_paths.md) — five ways
  the safety layer actually stops a bad call, also with real output

## Try it yourself

```bash
pip install -r requirements.txt
pytest tests/ -v          # 17 tests, exercises every pattern above
python server.py          # runs the MCP server over stdio
```

The tests are the actual proof, not the prose above. If you want to
verify a claim in this README, the corresponding test is a better
source of truth than my description of it.

## Structure

```
server.py              # MCP tool definitions — thin, delegates everywhere
safety/
  phases.py             # session state + the phase gate itself
  validation.py         # pure validation functions
  audit.py               # structured logging, including failures
domain/
  inventory.py           # toy in-memory "database"
  purchase_orders.py    # draft/commit data + transformations
tests/                   # one file per pattern, ~17 tests total
examples/                 # real captured walkthroughs
```

`safety/` and `domain/` don't import from each other in the direction
you'd expect a "business logic" and "guardrails" split to invert: the
domain layer has no idea sessions or approval exist. The gate lives
entirely outside it, in `safety/phases.py`, which decides whether
`domain.purchase_orders.commit_draft()` is ever reached at all. That
separation is deliberate — it's what makes it possible to reason about
the safety properties without also reasoning about inventory logic at
the same time.

## What this is not

Not production code. No real database — inventory is a Python dict.
No authentication. Session state is in-memory and single-process. This
exists to make the safety patterns inspectable and testable in
isolation, not to be a system anyone should deploy.

## Background

I designed and built production MCP servers (Go, Kubernetes) during my
internship at Eli Lilly, including phase-gated tool access and mandatory
validation before any state-mutating deployment operation. This demo is
built fresh, in a different domain, using none of that code — it
isolates the same underlying patterns so they can be read, run, and
tested without requiring access to anything proprietary.