MCP gateway: small-team edition
One docker run that fronts one or more MCP servers, enforces a role to tool
allowlist, and logs every tool call as JSON. Configured by a single file. No
Redis, no admin portal, no tenant API keys.
Table of contents
When to use this
Use the MCP scanner to check a server before you connect it; use this gateway to govern it while it runs. This edition is for a small team that wants an allowlist and an audit trail without deploying the multi-tenant platform gateway.
Quick start
Write a gateway.yaml:
team: acme
log: { format: json, path: "-" }
routes:
- route: files
transport: stdio
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "/data"]
isolation_ack: true
rbac:
roles:
reader: { allowed_tools: ["read_file", "list_directory"] }
agents:
coding-agent: reader
Run it:
docker run -p 8080:8080 \
-v $PWD/gateway.yaml:/etc/mcp-gateway/gateway.yaml \
shield-mcp-gateway
# or: docker compose -f docker-compose.gateway.yml up
Point your agent at http://localhost:8080/gateway/files/mcp, sending its
identity as headers:
X-Agent-Key: coding-agent
X-User-Role: reader
A call to read_file passes; a call to delete_file is blocked and logged.
What it enforces
- Allowlist. Each agent maps to a role; each role lists
allowed_tools(empty means all) anddenied_tools. A call outside the allowlist is blocked before it reaches the upstream. Identity comes from the connection headers, never from tool arguments. - Logging. Every decision is one JSON line to stdout (or a file): tool, agent, allowed, action, reason, risk. Ship it to your log stack.
- Description scanning. With
scan_descriptions: true, poisoned tool descriptions are annotated ontools/list.
Enforcement modes
The default image is slim and runs on CPU: the allowlist and logging need no model. Model-grade output DLP and prompt-injection detection are opt-in by pointing a route at a Shield backend:
enforcement_backend: http
shield_url: "https://shield.example.com"
shield_tenant_key: "..."
Important: non-bypassability
The gateway only governs traffic that flows through it. Your upstream MCP server
MUST accept connections only from the gateway (localhost, firewall, or mTLS).
A route with isolation_ack: false logs a loud warning, because an upstream an
agent can reach directly is not actually governed.
Configuration reference
| field | meaning |
|---|---|
team |
a label for this deployment |
log.format / log.path |
json/text; "-" for stdout or a file path |
routes[].route |
the path segment: /gateway/<route>/mcp |
routes[].transport |
stdio (command/args/env) or sse/http (url/headers) |
routes[].scan_descriptions |
annotate poisoned tool descriptions |
routes[].isolation_ack |
you confirm the upstream only accepts the gateway |
routes[].enforcement_backend |
inprocess (default) or http (Shield backend) |
rbac.roles |
role to allowed_tools / denied_tools |
rbac.agents |
X-Agent-Key to role |
Set SHIELD_GATEWAY_FAIL_OPEN=1 to allow a call when enforcement itself errors
(default is fail-closed).
Source: core/mcp/gateway_lite.py.
Spec: docs/spec-mcp-gateway-lite.md.