Skip to main content
Glama
amin-ale

MCP Audit Gateway

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.

Related MCP server: protect-mcp

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.

A
license - permissive license
-
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
1Releases (12mo)
Commit activity

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/amin-ale/mcp-audit-gateway'

If you have feedback or need assistance with the MCP directory API, please join our Discord server