# MCP Audit Gateway [![CI](https://github.com/amin-ale/mcp-audit-gateway/actions/workflows/ci.yml/badge.svg)](https://github.com/amin-ale/mcp-audit-gateway/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-informational.svg)](LICENSE) [![Python](https://img.shields.io/badge/python-3.11%2B-blue.svg)](pyproject.toml) 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 `.` 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
stdio] U2[Tenant B MCP server
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 `.` (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).