secure-inventory-mcp
README.md
# secure-inventory-mcp
A small, **read-only** MCP server that lets an AI assistant (Claude Desktop, etc.)
answer inventory and order-status questions for a small shop — built to show
least-privilege MCP design, not just "it works."
> Demo data is fictional. This is a reference implementation, not a certified
> or guaranteed-secure product. Review it against your own threat model.
## Tools
| Tool | What it does | Guardrails | Adapters |
|---|---|---|---|
| `get_product` | One product by SKU | SKU must match `AA-0000` | sqlite, csv |
| `search_products` | Name search | 2–40 chars, letters/digits/space/`-`/`/` only, ≤50 rows | sqlite, csv |
| `low_stock` | Items at/below a threshold | 0–1000 | sqlite, csv |
| `order_status` | Status + item count of one order | Never returns customer name/email | sqlite |
There is deliberately **no** raw-query tool and **no** write tool.
## Data sources (adapters)
The server is split so a new data source is about **one file**:
```
server.py picks the adapter (INVENTORY_ADAPTER), runs STDIO only
core.py shared controls: input types, row cap, sanitizing,
untrusted_data wrapper, audit log, masked errors
adapters/__init__.py allowlist registry (env var can't import arbitrary code)
adapters/sqlite_inventory.py SQLite, read-only (default)
adapters/csv_inventory.py CSV file, read-only
adapters/_template.py copy this to add Google Sheets, REST API, Postgres...
```
| Adapter | `INVENTORY_ADAPTER` | Config env var | Extra protections |
|---|---|---|---|
| SQLite | `sqlite` (default) | `INVENTORY_DB` | `mode=ro` + `PRAGMA query_only`; parameterized SQL |
| CSV | `csv` | `INVENTORY_CSV` | Column allowlist (other columns like costs or supplier emails are dropped on load); 5 MB / 50,000-row cap; spreadsheet formulas (`= + - @`) defused; malformed files fail closed |
**Adding a source:** copy `adapters/_template.py`, fill in `_fetch_rows()` using a
read-only credential from an env var, add it to `ADAPTERS` in
`adapters/__init__.py`, and copy the CSV tests in `tests/test_adapters.py`.
The template's docstring lists which controls `core.py` gives you for free
(C1, C6–C9) and which the adapter must handle (C2, C3, C5, C10).
## Security controls
| # | Control | Where |
|---|---|---|
| C1 | STDIO only — no network port | `server.py` |
| C2 | Data source opened read-only | each adapter (`_connect()`, `_load()`) |
| C3 | Fixed, parameterized queries / filters only | each adapter |
| C4 | Strict input schemas checked before code runs | shared types in `core.py` |
| C5 | Data minimization (allowlisted fields only, no customer PII) | each adapter |
| C6 | Hard row cap (50) | `core.wrap()` |
| C7 | Output sanitized + wrapped as `untrusted_data` (stored prompt-injection defense) | `core.clean()`, `core.wrap()` |
| C8 | Every call audited to JSONL, including rejected calls | `core.AuditMiddleware` |
| C9 | Internal error details masked | `core.build_server()` |
| C10 | Config by env vars, no secrets in code | `INVENTORY_ADAPTER`, `INVENTORY_DB`, `INVENTORY_CSV`, `AUDIT_LOG` |
Each control has tests that try to break it. `tests/test_security.py` (SQLite):
SQL injection, LIKE wildcards, oversize input, row-cap bypass, stored prompt
injection with a bidi-override character, PII leakage, writes, path leakage
in errors, audit gaps. `tests/test_adapters.py` (CSV + registry): the same
attacks plus hidden-column leakage, spreadsheet-formula injection, file
tampering, malformed/oversized/binary files leaking paths or row content,
unknown-adapter names, and the template failing closed.
For a new client job, start with `CLIENT-INTAKE.md`.
## Run it
```bash
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/python seed_db.py
.venv/bin/python -m pytest -q
```
## Connect to Claude Desktop
Add to `claude_desktop_config.json` (use absolute paths):
```json
{
"mcpServers": {
"secure-inventory": {
"command": "/ABSOLUTE/PATH/secure-inventory-mcp/.venv/bin/python",
"args": ["/ABSOLUTE/PATH/secure-inventory-mcp/server.py"],
"env": {
"INVENTORY_DB": "/ABSOLUTE/PATH/shop.db",
"AUDIT_LOG": "/ABSOLUTE/PATH/audit.jsonl"
}
}
}
}
```
For a CSV file instead, use this `env` block:
```json
"env": {
"INVENTORY_ADAPTER": "csv",
"INVENTORY_CSV": "/ABSOLUTE/PATH/inventory.csv",
"AUDIT_LOG": "/ABSOLUTE/PATH/audit.jsonl"
}
```
The CSV needs the columns `sku,name,description,price,qty` (extra columns
are allowed and ignored). See `sample_inventory.csv`.
## Adapting it for a real business
The pattern carries over to any data source (Shopify, Square, a Postgres DB,
a Google Sheet): write one adapter from `adapters/_template.py`, give it its
own **read-only** credential scoped to the smallest set of data, expose a few
narrow tools instead of a generic query tool, and keep the tests. See
`SECURITY-CHECKLIST.md`.
## References
- OWASP GenAI Security Project — *A Practical Guide for Secure MCP Server Development*: https://genai.owasp.org/resource/a-practical-guide-for-secure-mcp-server-development/
- FastMCP: https://gofastmcp.com
## License
MIT (see `LICENSE`).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues