MCP Audit Gateway
by amin-ale
README.md
# MCP Audit Gateway
[](https://github.com/amin-ale/mcp-audit-gateway/actions/workflows/ci.yml)
[](LICENSE)
[](pyproject.toml)
<!-- mcp-name: io.github.amin-ale/mcp-audit-gateway -->
MCP gateway that adds per-tool RBAC and an exportable audit log to any MCP server, plus per-tenant isolation, PII redaction on tool results, per-tenant rate limits, and tenant-level allow/deny policies. It sits between an MCP client (Claude Desktop, an agent runtime, your own app) and one or more upstream MCP servers. The concrete case it was built for: put per-tool RBAC and an exportable audit log in front of a QuickBooks or HubSpot MCP server, so every `tools/call` against the books flows through one enforceable, logged, redacted chokepoint.
Related: [QuickBooks Online MCP Server](https://github.com/amin-ale/mcp-quickbooks) ·
[HubSpot CRM MCP Server](https://github.com/amin-ale/hubspot-mcp-server) ·
[What production MCP actually requires](https://amin-ale.github.io/portfolio-site/what-production-mcp-actually-requires.html)
Point it at any MCP server, describe your tenants and roles in YAML, and the policy is one file you can read in a review. HMAC request signing and both stdio and streamable-HTTP transports are included. MIT, no paid tier.
## Why this exists
A bare MCP server exposes every tool to every caller with no identity, no quota, no record, and no redaction. That is fine on a laptop and unshippable in a multi-tenant product. This gateway adds that control plane without touching the upstream server. The companion write-up, [docs/what-production-mcp-actually-requires.md](docs/what-production-mcp-actually-requires.md), walks each feature past a concrete failure story.
## Tools
The gateway exposes no tools of its own. It proxies `tools/list` and `tools/call` through to the upstream MCP servers configured for the calling tenant, namespacing each proxied tool as `<upstream>.<tool>` and applying RBAC filtering, rate limiting, and PII redaction on the way through. Started with no upstreams configured, `initialize` and `tools/list` still succeed and the tool list comes back empty.
Its own surface is the CLI control plane:
- `validate`: check a policy file (roles, principals, tenants, upstreams, detectors) before it serves any traffic.
- `serve --transport stdio`: run the gateway over stdio for a local MCP client such as Claude Desktop.
- `serve --transport http`: run the gateway over streamable HTTP for remote or agent clients.
- `audit export --format csv`: export the audit log as CSV for a spreadsheet or a SIEM ingest.
- `audit export --format json`: export the same records as JSON.
## Proof: the two-tenant demo
The evidence here is a demo you run yourself, not a site I ask you to trust. `demo/run_demo.py` stands up the gateway in front of two tenants, **acme** (a stdio upstream subprocess) and **globex** (a streamable-HTTP upstream), and drives twelve scenarios that exercise RBAC, tenant kill-switches, PII redaction, cross-tenant isolation, HMAC signing, unknown-tool rejection, and per-tenant rate limiting against the bundled toy MCP server. There is no hosted instance to keep alive and no external service to reach; everything runs on loopback and finishes in seconds.
The committed output is the artifact:
- [docs/demo-transcript.md](docs/demo-transcript.md): every step, its gateway decision, and the result the client actually saw, followed by an audit-outcome summary table.
- [docs/demo-audit.csv](docs/demo-audit.csv): the full machine-readable audit trail the same run produced.
Both files are regenerated verbatim by `python demo/run_demo.py`, so the transcript and its audit table always reconcile with a run you can reproduce locally.
## Architecture
```mermaid
flowchart LR
subgraph Clients
C1[Tenant A client]
C2[Tenant B client]
end
subgraph Gateway [mcp-audit-gateway]
direction TB
AUTH[Authenticate principal] --> SIG[Verify HMAC signature]
SIG --> RBAC[RBAC and allow/deny policy]
RBAC --> RL[Per-tenant rate limit]
RL --> ROUTE[Resolve tenant upstream]
ROUTE --> RED[PII redaction on result]
RED --> AUD[(Audit log JSONL)]
end
subgraph Upstreams
U1[Tenant A MCP server<br/>stdio]
U2[Tenant B MCP server<br/>streamable HTTP]
end
C1 -->|signed JSON-RPC| AUTH
C2 -->|signed JSON-RPC| AUTH
ROUTE --> U1
ROUTE --> U2
U1 --> RED
U2 --> RED
RED -->|redacted result| C1
```
Every request is authenticated to a principal, which pins it to exactly one tenant and role. A tenant can only ever reach its own upstreams, credentials, and state. The pipeline short-circuits at the first failing gate and records the decision.
## Features
| Feature | What it does |
| --- | --- |
| Per-tool RBAC | Role-to-tool allow/deny lists with glob patterns; deny always wins. |
| Per-tenant isolation | Each tenant gets its own upstream processes/endpoints, credentials, tool catalog, rate-limit bucket, and state. Cross-tenant tool names are invisible. |
| Audit logging | Append-only JSONL of who called which tool, with which (redacted) arguments, result status, latency, and redaction counts. Exportable to CSV and JSON. |
| PII redaction | Configurable detectors (email, phone, SSN, Canadian SIN, credit card) applied to tool results before they leave the gateway. `mask`, `hash`, or `partial` modes. |
| Rate limiting | Per-tenant token bucket (`requests_per_minute` + `burst`). |
| Allow/deny policies | Tenant-level tool kill-switch that overrides roles: a governance layer above RBAC. |
| Request signing | HMAC-SHA256 over the request body with a timestamp freshness window to stop replay. |
| Transports | stdio and streamable-HTTP on both the client-facing and upstream-facing sides. |
## Quickstart
```bash
uv venv --python 3.12
uv pip install -e ".[dev]"
# Validate the bundled two-tenant demo config
python -m mcp_gateway validate --config config/demo.yaml
# Run the full scripted demo (starts a stdio upstream and an HTTP upstream,
# proves tenant isolation, and writes docs/demo-transcript.md)
python demo/run_demo.py
```
Installed as a package, the same commands run through the `mcp-audit-gateway` console script.
### Running it bare
The gateway starts with no config file at all, which is what an MCP client or a registry crawler sees on a first `tools/list` probe:
```bash
mcp-audit-gateway serve --transport stdio
```
That serves one local principal against zero upstreams: `initialize` and `tools/list` succeed, the tool list is empty, nothing is written to disk. Point it at a policy file to make it useful, either with `--config` or by setting `MCP_AUDIT_GATEWAY_CONFIG`.
### Running the gateway
```bash
# Streamable HTTP, for remote/agent clients (reads host/port from the config)
python -m mcp_gateway serve --config config/demo.yaml --transport http
# stdio, for a local client such as Claude Desktop (pins the session to a principal)
python -m mcp_gateway serve --config config/demo.yaml --transport stdio --principal acme-admin
```
`--principal` is only required when the config defines more than one; with a single principal the stdio session uses it.
The demo's `globex` tenant proxies an HTTP upstream expected at `http://127.0.0.1:9100/mcp`. To serve the gateway against `demo.yaml` directly, start that upstream first:
```bash
python -m mcp_gateway.toy_upstream --transport http --host 127.0.0.1 --port 9100 --dataset globex
```
`demo/run_demo.py` handles this wiring automatically on an ephemeral port, so it is the fastest way to see everything work.
Stdio upstream commands that start with `python` or `python3` are launched under the gateway's own interpreter, so a `python -m ...` upstream works whether or not `python` is on the caller's `PATH`. Any other command is executed verbatim.
### Exporting the audit log
```bash
python -m mcp_gateway audit export --input config/audit-log.jsonl --format csv --output audit.csv
python -m mcp_gateway audit export --input config/audit-log.jsonl --format json
```
## Config reference
```yaml
gateway:
name: mcp-audit-gateway-demo
http: { host: 127.0.0.1, port: 8080 }
security:
require_signature: true # enforce HMAC signing on incoming requests
signature_max_age_seconds: 300 # replay window
redaction:
enabled: true # redact tool RESULTS before returning them
mode: mask # mask | hash | partial
detectors: [email, phone, ssn, sin, credit_card]
redact_arguments: true # also redact arguments written to the audit log
audit:
enabled: true
path: audit-log.jsonl # relative to the config file's directory
argument_logging: redacted # redacted | full | keys_only | none
roles: # role -> tool allow/deny (glob patterns, deny wins)
admin: { allow_tools: ["*"] }
analyst: { allow_tools: ["billing.get_*", "billing.list_*"], deny_tools: ["billing.delete_*"] }
principals: # a credential = one identity = tenant + role + signing secret
- { id: acme-admin, tenant: acme, role: admin, secret: "demo-only-secret" }
tenants:
acme:
deny_tools: ["billing.delete_invoice"] # optional tenant-level kill switch
upstreams:
- { name: billing, transport: stdio, command: ["python", "-m", "mcp_gateway.toy_upstream", "--dataset", "acme"] }
rate_limit: { requests_per_minute: 60, burst: 10 }
```
Tools are namespaced as `<upstream>.<tool>` (for example `billing.get_invoice`), so RBAC and policy patterns are stable across tenants and collisions between upstreams are impossible.
## Running the tests
```bash
uv run pytest
```
The suite is fully offline. The "external" MCP servers it talks to are the bundled toy upstream, exercised for real over both stdio (subprocess) and streamable HTTP (loopback): no network, no mocks-of-mocks, and it finishes in seconds.
## Security and scope notes
- Static YAML secrets are for local demos only. In production, load principal secrets from a secret manager and prefer per-request short-lived credentials; see the write-up.
- HMAC signing protects the client-to-gateway hop. It is not a substitute for TLS or network controls.
- Redaction is regex-based best-effort defense-in-depth, not a certified DLP guarantee. Treat it as one layer.
- The audit log is a point-in-time record of what the gateway observed. It is not a compliance attestation.
- Only proxy and inspect MCP servers you own or are authorized to operate.
## Hire me
I make AI-era and money-critical backends production-safe: auth, multi-tenancy, governance, and the boring correctness that keeps you out of incidents. Available for MCP, billing, and hardening work. Portfolio and contact: [https://amin-ale.github.io/portfolio-site](https://amin-ale.github.io/portfolio-site) · [amin.ale.business@gmail.com](mailto:amin.ale.business@gmail.com).
## License
MIT. See [LICENSE](LICENSE).
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues