Skip to main content

MCP gateway adding per-tool RBAC, tenant isolation, audit export, and PII redaction to any server.

Project description

MCP Audit Gateway

CI License: MIT Python

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 · HubSpot CRM MCP Server · What production MCP actually requires

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, 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: every step, its gateway decision, and the result the client actually saw, followed by an audit-outcome summary table.
  • 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

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

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:

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

# 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:

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

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

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

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 · amin.ale.business@gmail.com.

License

MIT. See LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mcp_audit_gateway-0.1.0.tar.gz (53.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mcp_audit_gateway-0.1.0-py3-none-any.whl (35.7 kB view details)

Uploaded Python 3

File details

Details for the file mcp_audit_gateway-0.1.0.tar.gz.

File metadata

  • Download URL: mcp_audit_gateway-0.1.0.tar.gz
  • Upload date:
  • Size: 53.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.17 {"installer":{"name":"uv","version":"0.9.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for mcp_audit_gateway-0.1.0.tar.gz
Algorithm Hash digest
SHA256 71ca74590d3147d5dbd5d9e159150786ee58e46ad4c561f98cd178daba72fef9
MD5 461ee9c6756d8046c4d5c299da34ce1c
BLAKE2b-256 23f3721aab69b35c49c5cc9b4bbd69dbed32ed18ec244bc4b0d239f271a351a7

See more details on using hashes here.

File details

Details for the file mcp_audit_gateway-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: mcp_audit_gateway-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 35.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.17 {"installer":{"name":"uv","version":"0.9.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for mcp_audit_gateway-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 8479900580ae52a41ac692a2c1b78eecbee260a3bf9a14a204d8e70afbfa4151
MD5 6b2fe593e70264053f144b9ba3299df2
BLAKE2b-256 ad892a68a8c901c7786a622aa7962219f6fd6ad7c808c57827246851d931716b

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page