Skip to main content
Glama
jigonyoo

mcp-server-prod

by jigonyoo

mcp-server-prod

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:

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

One status = "paid' OR '1'='1" and the agent just dumped your whole table.

Related MCP server: odoo-mcp-server

Quickstart

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-onlyfile:...?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

{
  "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

A
license - permissive license
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jigonyoo/mcp-server-prod'

If you have feedback or need assistance with the MCP directory API, please join our Discord server