mcp-server-prod
by jigonyoo
README.md
# mcp-server-prod
[](https://github.com/jigonyoo/mcp-server-prod/actions/workflows/ci.yml)
**A hardened MCP server — because MCP tool arguments are written by an LLM, from untrusted user text.** Exposes read-only "orders" tools to an AI agent, and treats every argument as hostile: validated, parameterized, and served over a read-only connection.
> Runs offline. `docker compose up` runs the security battery with **no API key and no network** — the standard library only. (The MCP server itself needs `pip install mcp`.)
---
## The problem
An MCP tool signature like `search_orders(status: str)` looks safe. But `status` doesn't come from your code — it comes from a model, interpreting whatever a user typed. That's a network boundary wearing a function signature. The naive server everyone writes first concatenates that string into SQL and calls it a day:
```python
sql = f"SELECT * FROM orders WHERE status = '{status}'" # ← LLM-controlled
```
One `status = "paid' OR '1'='1"` and the agent just dumped your whole table.
## Quickstart
```bash
docker compose up # security battery: naive vs prod scorecard (offline)
make demo # same, without Docker
make test # the security contract tests (pytest)
make serve # run the actual MCP server over stdio (needs: pip install mcp)
```
## The scorecard (before → after)
The same five hostile tool-calls, run against the naive server and this one:
| attack | naive | prod |
|---|:--:|:--:|
| filter bypass — `status = paid' OR '1'='1` | 🔴 LEAK | 🟢 SAFE |
| table exfil — `customer = zzz' OR '1'='1` | 🔴 LEAK | 🟢 SAFE |
| numeric injection — `min_total = "0 OR 1=1"` | 🔴 LEAK | 🟢 SAFE |
| id injection — `get_order("1 OR 1=1")` | 🔴 LEAK | 🟢 SAFE |
| write reaches the DB (read-only?) | 🔴 LEAK | 🟢 SAFE |
| **safely handled** | **0 / 5** | **5 / 5** |
Meanwhile legitimate queries are unaffected — `search_orders(status="paid")` returns the 12 paid orders, `revenue_summary("paid")` returns `{count: 12, total: 25451.24}`. Hardening didn't cost functionality.
## The three defenses
1. **Validate every argument** (`validation.py`) — `status` must be an enum member, `order_id` must be an integer, `limit` is clamped to 100. An agent asking for a million rows gets 100; a bad enum is a structured error, not a silent mis-query.
2. **Parameterize every query** (`tools.py`) — user values are *bound*, never concatenated. `customer = "zzz' OR '1'='1"` becomes a literal name that matches nobody, not a bypass.
3. **Open the database read-only** — `file:...?mode=ro`. Defense in depth: even a query that slips through cannot mutate data. The write test raises at the driver.
## Wire it into a desktop MCP client
```json
{
"mcpServers": {
"orders": { "command": "python", "args": ["-m", "mcp_server_prod.server"] }
}
}
```
Drop that into Claude Desktop's MCP config and the `search_orders` / `get_order` / `revenue_summary` tools appear to the agent. (A short screen recording of that flow makes a good demo GIF — the server code and config here are what it records.)
## How it's verified
Security is a **contract test suite**, not a claim: the production tools must reject or safely handle each hostile input, *and* the naive implementation must demonstrably leak (so the threat is real, not hypothetical). CI also builds the server against the official `mcp` SDK. All checks run offline; the battery scorecard is computed from real behavior.
```
9 passed # pytest
0/5 → 5/5 # security battery
mcp server ok # builds with the official SDK
```
## 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 authЗ, per-tool authorization,
and audit logging (authn/authz) — explicitly out of scope here and called out, not faked.
· The "naive vs prod" contrast is illustrative; it doesn't enumerate every injection class
(no second-order, stored, or ORM-specific vectors).
· Parameterization + read-only defends the data layer; it is not a full threat model
(rate limiting, secrets handling, and prompt-injection of the agent itself are separate).
```
## Layout
```
mcp_server_prod/ db.py · validation.py · tools.py (prod) · naive.py · server.py · demo.py
tests/ test_security.py # the security contract
Dockerfile · docker-compose.yml · .github/workflows/ci.yml
```
MIT © 2026 Jigon Yoo
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues