Zero-Trust MCP Gateway — a deny-by-default firewall/proxy for the Model Context Protocol.
mcpgate sits between an AI agent and an MCP server. Every gated tools/call, resources/read, prompts/get, and reverse-channel sampling/createMessage is evaluated against a YAML policy before it reaches the other side. Unknown or denied gated calls are blocked and logged; non-sensitive protocol traffic such as initialization still passes through.
The fastest way to understand mcpgate is the showcase flow in docs/SHOWCASE.md:
Watch the 60-second narrated showcase video.
- An agent tries to read a safe file and mcpgate allows it.
- The same agent tries to write a file and mcpgate parks the call for human approval.
- A dangerous delete is denied by policy before it reaches the MCP server.
- Prompt-injection or exfiltration text is flagged in the signed audit trail and can be blocked with
heuristics.block_on_warn.
- Deny-by-default YAML policy for MCP tools, resources, prompts, and reverse-channel sampling.
- Write-ahead SQLite audit: calls are logged before forwarding, and audit failure denies the call.
- Local browser dashboard for pending approvals and live audit events, including approval-source filtering.
- HMAC-verifiable audit chain export for incident review, including approval source metadata.
- Deterministic prompt-injection/tool-poisoning scanner with optional block-on-warn mode.
- Stdio and HTTP server transports with optional HTTP egress allowlisting.
- One active selected MCP server per gateway process for explicit client routing and audit attribution.
mcpgate is open source under the MIT License. Contributions are welcome; start with CONTRIBUTING.md, follow the Code of Conduct, and report vulnerabilities through the private disclosure process in SECURITY.md.
AI Agent (e.g. Claude)
│ JSON-RPC over stdio
▼
┌──────────┐
│ mcpgate │──── enforce policy ────► allow / deny / ask
│ proxy │ │
└──────────┘ │ allow
│ JSON-RPC over stdio ▼
▼ ┌─────────────────┐
MCP Server │ SQLite audit DB │
(filesystem, git, …) └─────────────────┘
- The agent's stdin/stdout is piped through mcpgate instead of directly to the MCP server.
- mcpgate connects to the configured MCP server over stdio or HTTP.
- For every gated method (
tools/call,resources/read,prompts/get, and reverse-channelsampling/createMessage) the policy engine runs. - The verdict (
ALLOW/DENY/ASK) is written to a SQLite audit log before the call is forwarded. - A small HTTP server on
127.0.0.1:18789exposes the browser dashboard,/health,/approve,/pending,/audit, and/events.
- Go 1.25+ (module path:
github.com/maksym-mishchenko/mcpgate) - An MCP server binary (e.g.
mcp-filesystem)
Download a release archive from https://github.com/maksym-mishchenko/mcpgate/releases, verify it with checksums.txt and GitHub artifact attestations, extract the mcpgate binary for your OS/architecture, and put it on your PATH.
Or install with Go:
go install github.com/maksym-mishchenko/mcpgate/cmd/mcpgate@latest# mcpgate.yaml
version: 1
mode: enforce
default: "false"
servers:
filesystem:
command: ["/usr/local/bin/mcp-filesystem", "--root", "/home/user/safe"]
tools:
read_file:
allow: "true"
constraints:
path:
within: ["/home/user/safe"]
write_file:
# ask prompts do not evaluate constraints; approvers must inspect
# the proposed path before allowing the call.
allow: ask
delete_file:
allow: "false"
resources:
allow: "true"
# Injection / tool-poisoning heuristics (v1.1).
heuristics:
enabled: true # WARN-only detection (default)
block_on_warn: false # set true to deny on a match and withhold poisoned content# Set a high-entropy token for the web API (required)
openssl rand -hex 32 > .mcpgate-token
# Run — with a single configured server, mcpgate starts its command from the policy
mcpgate --config mcpgate.yaml --token-file .mcpgate-tokenmcpgate prints a token-safe dashboard URL with a placeholder, for example http://127.0.0.1:18789/#token=<token-from-token-source>, so startup logs and screenshots do not expose credentials. Replace the placeholder with the token from .mcpgate-token for local browser access. Use the same token as a Bearer token for API calls.
Configure your AI client to use mcpgate's stdio instead of the MCP server directly. For example in Claude Desktop's mcp.json:
{
"mcpServers": {
"filesystem": {
"command": "mcpgate",
"args": ["--config", "/path/to/mcpgate.yaml", "--token-file", "/path/to/.mcpgate-token"]
}
}
}version: 1 # must be 1
mode: enforce # "enforce" (block violations) | "observe" (log only, allow all)
default: "false" # default verdict for unmatched calls: "true" | "false" | "ask"
servers:
<server-name>: # choose this name with --server when multiple servers are configured
command: [] # stdio server command; omit when using url
url: "" # HTTP JSON-RPC endpoint; omit when using command
egress_allow: [] # optional hostname allowlist for HTTP transport
tools:
<tool-name>:
allow: "true" | "false" | ask
constraints: # optional; only evaluated when allow is "true"
path:
within: ["/allowed/prefix"] # path must be under one of these roots
resolve_within: ["/allowed/prefix"] # optional symlink-aware check for existing paths
equals: "/exact/path" # path must equal this exactly
one_of: ["/a", "/b"] # path must be one of these values
matches: "regex" # path must match this anchored regex
fields:
mode:
one_of: ["read", "search"]
limit:
min: 1
max: 100
include_hidden:
bool: false
query:
matches: "[a-z0-9 _-]+"
resources:
allow: "true" | "false" | ask
prompts:
allow: true | false
sampling:
allow: true | false
heuristics:
enabled: true # default when omitted
block_on_warn: false # opt in to deny and withhold on deterministic scanner matchesModes:
| Mode | Behaviour |
|---|---|
enforce |
DENY all calls not explicitly allow: "true". ask parks the call for human approval and auto-denies on timeout. |
observe |
All calls pass through. Useful for discovering what an agent actually calls. |
Allow values:
| Value | Verdict |
|---|---|
"true" |
Allow (after constraint check) |
"false" |
Deny immediately |
ask |
Interactive approval through the local browser UI; timeout resolves as deny; constraints are not evaluated for ask prompts |
Constraints:
| Constraint | Applies to | Behavior |
|---|---|---|
path.within |
arguments.path |
String-level absolute path containment check; no symlink resolution; useful for planned writes or new paths |
path.resolve_within |
arguments.path |
Opt-in EvalSymlinks containment check for existing paths; fails closed if the path or root cannot be resolved |
path.equals / path.one_of / path.matches |
arguments.path |
Exact, enum, or anchored RE2 checks |
fields.<name>.equals / one_of / matches |
JSON string argument | Exact, enum, or anchored RE2 checks |
fields.<name>.min / max |
JSON number or numeric string argument | Denies on parse/range failure |
fields.<name>.bool |
JSON boolean or boolean string argument | Denies on mismatch |
For constraint-evaluated allow: "true" rules, missing constrained fields deny the call. Invalid regexes, unparseable numbers, unparseable booleans, unresolved symlinks, and missing path values for path-constrained allow rules fail closed. ask prompts do not evaluate constraints; approvers must inspect proposed paths manually.
TOCTOU boundary: Path checks run before the call is forwarded. The child MCP server performs actual filesystem I/O later, so high-risk deployments should combine mcpgate policy with the MCP server's own root restrictions, read-only mounts where possible, containers, or OS-level permissions.
mcpgate [flags] [-- <server-command> [server-args...]]
| Flag | Default | Description |
|---|---|---|
--config |
mcpgate.yaml |
Path to policy YAML file |
--token |
$MCPGATE_TOKEN |
Bearer token for web API authentication |
--token-file |
$MCPGATE_TOKEN_FILE |
File containing the web API bearer token; preferred over command-line token values |
--audit-key |
$MCPGATE_AUDIT_KEY_FILE |
32-byte HMAC key file for signing runtime audit rows |
--addr |
127.0.0.1:18789 |
Web server listen address |
--approval-timeout |
30s |
How long an ask call waits before auto-deny |
--server-timeout |
60s |
How long mcpgate waits for MCP server responses before failing closed |
--server |
"" |
Server name from policy config to run; required when config has multiple servers |
If the policy config defines one server, mcpgate starts that server's command or url. If it defines multiple servers, pass --server <name> to choose one deterministically. The double-dash -- separator is only needed for the fallback mode where the server command is supplied on the CLI instead of in policy config.
mcpgate intentionally runs one active MCP server per process. For multiple MCP servers, configure one client entry and one mcpgate process per server; share a policy file if that makes operations simpler, but select each server explicitly with --server.
Policy decisions are hot-reloaded from --config when the policy file modification time changes. Reload failures keep the last valid policy in memory rather than replacing it with a broken config. The selected MCP server transport is startup-only; changing servers.<name>.command, url, or egress_allow requires restarting mcpgate.
mcpgate export --db mcpgate.db --out audit-review.jsonl
mcpgate verify --file audit-review.jsonl
mcpgate discover --file audit-review.jsonl --out draft-policy.yamlUse mcpgate keygen audit.key and run with --audit-key audit.key to sign runtime audit rows. Keyed verification requires contiguous sequence numbers and a valid HMAC signature on every row except the bootstrap seq=1 GENESIS row. discover converts a verified observe-mode audit export into a conservative mode: enforce draft policy. It includes only warning-free ALLOW rows, keeps default: "false", and uses placeholder server commands so operators must review transport settings and add path/field constraints before use. See docs/AUDIT_REVIEW.md.
All endpoints require authentication. Prefer passing the token as a Bearer header; the local browser UI accepts #token= and keeps the token out of the initial HTTP request URL. Requests from non-localhost Host headers are rejected (anti-DNS-rebinding).
Generate and store real dashboard tokens outside the repository. See docs/OPERATIONAL_SECRETS.md for storage and rotation guidance.
Returns safe, non-secret runtime status when the gateway is running.
curl -H "Authorization: Bearer $MCPGATE_TOKEN" http://127.0.0.1:18789/healthExample response:
{
"status": "ok",
"version": "1.4.2",
"server": "filesystem",
"policy": {
"path": "mcpgate.yaml",
"mode": "enforce",
"reload": "hot-last-known-good",
"heuristics_enabled": true,
"heuristics_block_on_warn": false
},
"audit": {
"history": true,
"hmac": true
},
"runtime": {
"pending": 0
}
}Returns currently parked ask calls, so a browser can reconnect without losing pending approvals.
curl -H "Authorization: Bearer $MCPGATE_TOKEN" http://127.0.0.1:18789/pendingReturns the latest 100 audit entries from SQLite, newest first. Approval-related rows include approval_source so operators can distinguish policy decisions, human UI decisions, approval timeouts, and heuristic blocks.
curl -H "Authorization: Bearer $MCPGATE_TOKEN" http://127.0.0.1:18789/auditFor long-running deployments, export and verify audit logs before archival or rotation. See docs/AUDIT_RETENTION.md.
Resolve a pending ask approval.
curl -X POST -H "Authorization: Bearer $MCPGATE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"key":"filesystem:42","verdict":"allow"}' \
http://127.0.0.1:18789/approve| Field | Values |
|---|---|
key |
"<server-name>:<request-id>" |
verdict |
"allow" or "deny" |
Server-Sent Events stream of audit events. Connect with any SSE client.
curl -N -H "Authorization: Bearer $MCPGATE_TOKEN" http://127.0.0.1:18789/eventsEvents are broadcast as event: <name>\ndata: <json>\n\n.
- Fail-closed: Any audit write failure causes the call to be denied. mcpgate never allows a call if it cannot record it.
- Token authentication: All web API endpoints require a matching Bearer token. There is no guest mode.
- Anti-DNS-rebinding: The
Hostheader is checked on every request. Onlylocalhostand127.0.0.1are accepted, regardless of the token. - Deny by default: Unmatched calls return
DENYin enforce mode. You must explicitly opt in to allow a tool. - Interactive approval:
askcalls are parked for approval in the local UI and denied automatically on timeout; audit rows record whether the final decision came from policy, a human, timeout, or heuristic blocking. - Prompt-poisoning detection: Deterministic scanner warnings are signed into the audit chain and can be escalated with
heuristics.block_on_warn. - Process isolation: stdio MCP servers are spawned with process-group isolation so
SIGTERM/SIGKILLreaches the whole subprocess tree, not just the direct child. - One active server per process: multiplexing is explicit at the MCP client level; each mcpgate process fronts one selected server for simpler routing and audit attribution.
cmd/mcpgate/ — CLI entry point, flag parsing, wiring
internal/
policy/ — YAML config types, policy engine (pure function)
proxy/ — core single-server message loop, verdict dispatch
audit/ — SQLite write-ahead audit log
approval/ — coordinator for pending human approvals
child/ — spawn/stop child MCP server process
codec/ — newline-delimited JSON-RPC reader/writer
transport/ — Transport interface (stdio and HTTP implementations)
web/ — HTTP server (/health, /approve, /pending, /audit, /events)
jsonrpc/ — minimal JSON-RPC message type
scanner/ — deterministic injection/exfiltration signatures
MIT

