Developer tooling · MCP · security

mcp-server-prod

A hardened MCP server that exposes read-only "orders" tools to an AI agent — and treats every argument as hostile, because MCP tool arguments are written by an LLM from untrusted user text. Validated, parameterized, read-only. Offline, no keys.

Public · synthetic demo
The problem

A tool signature is a network boundary wearing a function name.

search_orders(status: str) looks safe — but status doesn't come from your code, it comes from a model interpreting whatever a user typed. The naive server everyone writes first drops it straight into SQL:

sql = f"SELECT * FROM orders WHERE status = '{status}'"   # ← LLM-controlled

# status = "paid' OR '1'='1"  → the agent just dumped the whole table.
The money shot

Same five hostile tool-calls · naive server vs this one

① Naive: every argument is a way in
attacknaiveprod
filter bypass — status = paid' OR '1'='1LEAKSAFE
table exfil — customer = zzz' OR '1'='1LEAKSAFE
numeric injection — min_total = "0 OR 1=1"LEAKSAFE
id injection — get_order("1 OR 1=1")LEAKSAFE
write reaches the DB (read-only?)LEAKSAFE
0/5 → 5/5safely handled (naive → prod)
0rows leaked by prod
0functionality lost to hardening
② Three defenses — and legit queries untouched
1. VALIDATE   status ∈ {paid,pending,refunded} · order_id is int · limit clamped to 100
2. PARAMETERIZE  values are bound, never concatenated ("zzz' OR '1'='1" = a literal name)
3. READ-ONLY   connection opened file:...?mode=ro — a write raises at the driver
legit queryresult
search_orders(status="paid")12 rows (db has 12 paid)
revenue_summary("paid"){count: 12, total: 25451.24}

Hardening cost nothing on the happy path — the correct answers are still correct.

③ Verified, not asserted
9/9security contract tests pass
0/5 → 5/5battery, computed from real behavior
server builds on the official mcp SDK

CI also proves the naive version really leaks — so the threat is demonstrated, not hypothetical.

Wire it into a desktop agent

Real MCP, over stdio.

{
  "mcpServers": {
    "orders": { "command": "python", "args": ["-m", "mcp_server_prod.server"] }
  }
}

Drop that into a desktop MCP client (e.g. Claude Desktop) and search_orders / get_order / revenue_summary appear to the agent — every call passing through the same validation, parameterization, and read-only guard.

Honest limitations
· Synthetic 20-row SQLite dataset — a demonstrator of the access pattern, not a product.
· Scope is read-only retrieval. A write-capable server needs authn/authz, per-tool
  authorization, and audit logging — out of scope here and called out, not faked.
· The naive-vs-prod contrast is illustrative; it doesn't enumerate every injection class.
· Parameterization + read-only hardens the data layer; it is not a full threat model
  (rate limiting, secrets, and prompt-injection of the agent itself are separate concerns).
This is a synthetic sample demonstrating the method. Inspect the tools, the naive counter-example, and the security tests ↗