Gatehouse
by Ibrahim-ziaa
README.md
# Gatehouse
**A read only MCP gateway you can put in front of a real business system, with an audit console.** Python, official MCP SDK, FastAPI, SQLite, React, TypeScript, Tailwind. 129 tests, 84 of them security tests. Runs with one command, or in Docker.
Give Claude (or any AI assistant) safe, read only access to your CRM, with a key per assistant, least privilege scopes, rate limits, and an audit log that shows everything the AI did and everything it was refused.

## What it does
You have a system full of customer data. You want an AI assistant to answer questions from it. You do not want the AI to change anything, see everything, or do anything you cannot review later. Gatehouse sits between the two:
- **The AI cannot write.** There is no tool that writes, the database is opened read only three separate ways, and a test suite attacks it to prove the point.
- **Every assistant gets its own key.** A key decides which tools it may call, which rows it may see (for example EMEA accounts only), which fields stay masked (contact emails and phone numbers, deal amounts), and how many calls per minute it may make.
- **Everything is logged.** Time, key, tool, arguments, decision, the rule that made the decision, rows returned, fields masked, latency. Blocked calls explain themselves: *Blocked: key 'support-bot' is not scoped for list_deals.*
- **A console a non engineer can read.** Overview, audit log, keys and permissions, the tool catalogue with a Try it runner, a live attack suite, and copy and paste setup for Claude Desktop and Claude Code.
- **Real MCP, not a mock.** Built on the official `mcp` Python SDK (2.x), Streamable HTTP with bearer authentication, plus stdio. The demo's audit log is produced by real MCP client sessions against the running server.
| | |
|---|---|
|  |  |
| Calls over time, why calls were blocked, top tools and keys, latency. All computed from the audit table. | One row per key, and a tool by key permission matrix. Create and revoke work. |
|  |  |
| The same call run as three keys: unmasked, masked, and refused. | 31 attacks (writes, SQL injection, scope escapes, floods, stolen keys), run live, with evidence. |
The demo data is a fictional company (Halden Supply Co.) with 120 accounts, 344 contacts, 215 deals, 137 tickets and 417 activities, generated by a seeded script. No real companies, people or email addresses.
## Run it
Requirements: Python 3.11 or newer, Node.js 20 or newer.
```bash
make demo
```
That creates `.venv`, installs the Python and web dependencies, builds the console and starts everything on one port. Open <http://localhost:8104>.
On startup the demo creates seven access keys, then replays a scripted week of AI traffic (799 calls, 75 of them blocked) through the real `/mcp` endpoint using the SDK's own client. It takes about five seconds, and the console shows a banner while it runs. **Reset demo** in the sidebar does the same again, so the state is reproducible.
Other commands:
```bash
make test # 129 tests: security suite, MCP end to end, API, demo replay
make attack # run the live attack suite in the terminal
make serve # start without reinstalling or rebuilding
make shots # recapture the screenshots (demo must be running)
```
Docker (a multi stage `Dockerfile` and a `docker-compose.yml` are included):
```bash
docker compose up --build
```
### Connect Claude
The console's **Connect** screen builds these for you with a working demo key. For Claude Code:
```bash
claude mcp add --transport http gatehouse http://localhost:8104/mcp \
--header "Authorization: Bearer <access key>"
```
For Claude Desktop, the Connect screen gives a `claude_desktop_config.json` block (using the `mcp-remote` bridge to send the key), and a stdio variant that needs no Node.js.
### Configuration
| Variable | Default | Meaning |
|---|---|---|
| `GATEHOUSE_PORT` | `8104` | Port for the MCP endpoint, API and console |
| `GATEHOUSE_HOST` | `127.0.0.1` | Bind address |
| `GATEHOUSE_DEMO` | `1` | `1` wipes gateway state on start and replays the demo week. `0` keeps keys and the audit log between restarts and creates no demo keys |
| `GATEHOUSE_ADMIN_PASSWORD` | unset | When set, the console and `/api` require HTTP Basic auth with this password. `/mcp` always requires a bearer access key |
| `GATEHOUSE_PUBLIC_URL` | `http://localhost:8104` | The URL printed on the Connect screen |
| `GATEHOUSE_ALLOWED_HOSTS` | unset | Extra `Host` values the MCP endpoint accepts (DNS rebinding protection is on) |
| `GATEHOUSE_DATA_DIR` | `./data` | Where `crm.sqlite` and `control.sqlite` live |
| `GATEHOUSE_KEY` | unset | The access key for the stdio transport |
No API keys, no paid services, no network access needed.
## Engineering notes
### The path of one tool call
Every call, from either transport or from the console's Try it runner, goes through `Gateway.call` (`gatehouse/gateway.py`). Checks run in this order and the first failure wins:
1. **Authentication.** Bearer token, looked up by SHA-256 hash. Unknown, malformed and revoked keys get HTTP 401 before any MCP message is read. Revocation also cuts off sessions that are already open.
2. **Rate limit.** Sliding 60 second window per key. Refused calls still count, so probing is throttled too.
3. **Tool exists.** Eight published tools, nothing else. There is no `run_sql`.
4. **Tool scope.** The key's allow list. A key is also only *shown* the tools it may call.
5. **Argument validation.** Strict Pydantic models: closed schema (unknown fields rejected), enums, id patterns, length and range caps, no type coercion.
6. **Masked filters.** A key that cannot see deal amounts cannot filter by `min_amount` either, because repeated filtered calls would binary search the hidden value. Contact search never looks at email or phone for the same reason.
7. **Row scope.** Asking for another region, or for the id of a row in another region, is refused. The AI is told "not found", the administrator is told the truth.
8. **Query.** A constant SQL template on the read only connection, with the key's region scope inside the SQL.
9. **Masking.** Sensitive fields are redacted at any depth of the result, and the AI is told which fields are masked so it does not guess.
Whatever happens, one row is written to `audit_log`. Tokens are never logged, oversized arguments are truncated, and results are never stored, so the log cannot leak what a key could not see.
### Read only, in depth
- `crm.sqlite` is opened with `mode=ro`, then `PRAGMA query_only = ON`, then an SQLite authorizer that vetoes every operation except reads (this also blocks `ATTACH` and `PRAGMA` changes). The tests remove layers and show each one holds alone.
- Every SQL statement is a module level constant in `gatehouse/tools.py`. Arguments only ever travel as bound parameters. The one place SQL varies (`GROUP BY` column in `pipeline_summary`) picks between three constants using an already validated enum.
- Keys and the audit log live in a different file (`control.sqlite`). The tool connection has never opened it.
- The CRM file is `chmod 444` after seeding, and a test hashes it before and after a hostile session.
### The security suite
`tests/test_security.py` is written to be read top to bottom as a threat model: writes, SQL injection, tool scope, row scope, field masking, rate limits, authentication, the audit log. 84 cases. `tests/test_mcp_end_to_end.py` repeats the important ones over the real wire: it starts the app with uvicorn on a TCP port and connects with the SDK client over Streamable HTTP (and launches the stdio server as a subprocess). `gatehouse/attacks.py` is the 31 check suite behind the console's Security checks screen and `make attack`; one test sabotages masking to prove the suite reports failures rather than hiding them.
### Honest numbers
The console has no hard coded figures. The Overview is a set of queries over `audit_log` (`gatehouse/audit.py`); percentiles are nearest rank over the latencies of served calls. The demo audit log is written by the gateway itself while `gatehouse/demo.py` drives real MCP sessions: several keys behaving differently, a support bot that keeps asking for deals it is not scoped for, an outside integrator that probes for SQL injection and later floods the endpoint, a revoked key that keeps knocking. The only thing injected is the clock, so a week of traffic can be produced in seconds. Latency is measured inside the gateway (policy checks plus query, not network), which is why it is well under a millisecond against local SQLite.
### What this is not
The backing system is SQLite so the demo runs anywhere. Pointing the same gateway at Postgres or a SaaS API means replacing the query layer in `tools.py` and keeping a read only credential; the policy engine, keys, audit log and console do not change. The admin console has optional Basic auth, not SSO. The rate limiter is in memory, so it is per process.
### Layout
```
gatehouse/
tools.py tool catalogue, strict input models, constant SQL templates, masking
gateway.py the decision pipeline and audit writer
keys.py access keys (hashed), scopes, revocation
db.py read only CRM connection, control database
mcp_server.py MCP server: Streamable HTTP with bearer auth, and stdio
api.py, app.py admin API, static console, one process on one port
audit.py audit queries and overview statistics
demo.py demo keys and the scripted week replayed over real MCP sessions
attacks.py the live attack suite
crm_seed.py deterministic fictional CRM
web/ React 19, TypeScript, Vite, Tailwind CSS v4
tests/ security suite, MCP end to end, API, demo replay
```
MIT licensed.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues