MCP Audit Gateway
MCP gateway adding per-tool RBAC, tenant isolation, audit export, and PII redaction to any server.
README
MCP Audit Gateway
<!-- 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 · 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.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。