Metadata-Version: 2.4
Name: mcp-audit-gateway
Version: 0.1.0
Summary: MCP gateway adding per-tool RBAC, tenant isolation, audit export, and PII redaction to any server.
Author-email: Amin Ale <amin.ale.business@gmail.com>
License: MIT
License-File: LICENSE
Keywords: agent-governance,audit,gateway,governance,mcp,mcp-audit,model-context-protocol,multi-tenant,rbac
Requires-Python: >=3.11
Requires-Dist: pyyaml==6.0.2
Provides-Extra: dev
Requires-Dist: pytest==8.3.4; extra == 'dev'
Description-Content-Type: text/markdown

# 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-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).
