@11ai/mcp-gate
v0.2.1
Published
Fail-closed MCP proxy that gates agent tool calls with Execution Governance.
Maintainers
Readme
@11ai/mcp-gate
A firewall for MCP tool calls. Add one line to your config — no code changes.
mcp-gate is a fail-closed stdio proxy that sits between any MCP client
(Claude Desktop, Claude Code, Cursor, anything) and any MCP server. Every
tools/call is checked against your policy before it's forwarded. Denied
calls never reach the server — the client gets a JSON-RPC error and a
signed receipt records the attempt.
No API key. No network. No telemetry.
One-line install
Wrap any MCP server by changing its command in your client config:
Before
{
"command": "node",
"args": ["their-server.js"]
}After
{
"command": "npx",
"args": ["-y", "@11ai/mcp-gate", "--policy", "eg-policy.yaml", "--", "node", "their-server.js"]
}That's it. initialize, tools/list, resources, and notifications pass
through untouched. Only tools/call is gated.
Why you want this
MCP servers run with your credentials and your filesystem. A prompt-injected agent can call any tool the server exposes — exfiltrate secrets, POST data to attacker URLs, delete files. Reviewing logs afterward doesn't undo it.
mcp-gate decides before the call runs:
- Deny by policy — block outbound calls carrying secret material, writes outside allowed paths, dangerous shell commands, whatever your policy says.
- Fail-closed — engine error, timeout, malformed policy? The call is denied. There is no fail-open path.
- Signed receipts — every allow and every deny is Ed25519-signed,
SHA3-512 hashed, and chained. Verify the file offline with
eg-verify, no access to the machine required.
What a denial looks like
The client receives a JSON-RPC error instead of a tool result, and the receipt log records:
{
"tool": "http.post",
"decision": "deny",
"reason": "exfiltration: outbound call carrying secret material",
"policyVersion": "starter-1",
"sig": "…"
}Policy
Start from the canonical starter policy and edit YAML — allow/deny rules per tool, argument matching, path and URL constraints. Full schema: docs/POLICY.md.
CLI reference
mcp-gate --policy <file> [options] -- <server-command> [args...]| Flag | Purpose |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| --policy <path> | Policy file. Required unless EG_CONTROL_PLANE_URL is set. Malformed policy = every call denied. |
| --receipts <path> | Append signed receipts here (default ./eg-receipts.jsonl). |
| --key <path> | Ed25519 seed for a stable signing key. Without it a key is generated per run and receipts are not verifiable across restarts. |
| --timeout <ms> | Policy evaluation timeout. A timeout is a deny. |
| --name <name> | Tool namespace prefix. Defaults to a name derived from the wrapped command. See the warning below — this affects policy matching. |
| --validate | Check the policy and exit. Starts no server, writes no receipts. Safe in CI. |
| -h, --help | Show usage and exit 0. |
| -- | Everything after is the wrapped server command, verbatim. |
Set
--keybefore you rely on the receipts. Without it, a new signing key is generated per run and receipts cannot be verified across restarts. Each run's receipts still verify against that run's own key, so nothing looks broken — the failure only appears later, when you try to verify an older file and no longer have the key it was signed with. The gate warns on startup when it generates an ephemeral key.
--nameis part of what your policy matches on. Tool calls are evaluated as<name>.<tool>, so a rule written fortheir-server.http_poststops matching if the prefix changes. The default is derived from the wrapped command, which means editing the command in your client config can change the prefix as a side effect — and a rule that no longer matches is a rule that no longer denies. Set--nameexplicitly and the prefix stops depending on how the server happens to be launched.
Validate a policy before you depend on it:
npx @11ai/mcp-gate --validate --policy eg-policy.yamlExits 0 with the policy version, or 1 naming the fault. This runs the engine's
own parser rather than a schema check, so it catches what a schema cannot: a
rule naming an actionClass that was never declared, and an argsPattern that
is not a compilable regex. Both produce a policy that loads as valid YAML and
then denies every call, which looks identical to a very strict policy.
Exit behavior: if the wrapped server exits, the gate exits with the same code. If the gate cannot start (bad policy, missing binary), it exits nonzero and no server starts — fail-closed extends to process lifecycle.
Works with
- Claude Desktop / Claude Code — wrap any server in
claude_desktop_config.jsonor.mcp.json - Cursor, Windsurf, any MCP client — anything that launches stdio MCP servers
- Any MCP server — filesystem, GitHub, databases, browsers; the gate is server-agnostic
Versioning
Semver, tracks @11ai/execution-governance minors. The receipt format is
versioned independently; old receipt files stay verifiable.
The @11ai packages
| Package | What it is |
| ---------------------------------------------------------------------------------------- | ------------------------------------------------ |
| @11ai/execution-governance | SDK: gate any function call, not just MCP. |
| @11ai/mcp-gate | This package. |
| @11ai/identity-oidc (coming) | OIDC-verified principals bound to every receipt. |
| execution-governance on PyPI (coming) | Python SDK, cross-verifiable receipts. |
Part of Execution Governance
Built on
@11ai/execution-governance
— the SDK for gating any function call (not just MCP) with the same policy
engine and receipt chain. Try the prompt-injection demo:
git clone https://github.com/11-11AI/execution-governance && cd execution-governance
npm install && npm run demoLicense
Apache-2.0. Fully functional locally — no account, no hosted dependency. See LICENSE. LICENSING.md sets out, per component, what is open permanently and what is commercial: every part needed to verify a receipt is Apache-2.0 and stays that way.
