A small MCP (Model Context Protocol) firewall that proxies JSON-RPC and enforces allow/deny policies for tools, resources, prompts, and methods. It can run as a stdio wrapper or as a streamable HTTP reverse proxy, and includes a clean local GUI for policy edits, history, templates, and live logs.
Important: policy controls intent, containment controls capability. If you allow a CLI tool, that tool can still reach external data unless you enable containment (--no-network + --allow-bin).
- Build:
go build ./cmd/mcp-firewall- Scan + install (wrap stdio servers, proxy HTTP servers):
./mcp-firewall --host-scan --host-root .
./mcp-firewall --host-install --policy policy.example.yaml --host-root . --no-network --allow-bin git- Run the HTTP proxy + GUI (for HTTP upstreams and the dashboard):
./mcp-firewall --listen 127.0.0.1:17880 --routes ~/.mcp-firewall/routes.json --path /mcp --ui 127.0.0.1:8081Open http://127.0.0.1:8081/ui.
Tip: use --host-dry-run to preview changes, and --host-uninstall --host-restore to roll back.
Build:
go build ./cmd/mcp-firewallRun as a wrapper around a real MCP server:
./mcp-firewall --policy policy.example.yaml -- <server-command> <args>Serve the GUI while running stdio:
./mcp-firewall --ui 127.0.0.1:8081 --policy policy.example.yaml -- <server-command> <args>Open http://127.0.0.1:8081/ui.
The policy editor shows diff counts, local warnings, a library modal with diff view, and history in the Advanced drawer. The dashboard includes a "Top blocked by" chart, log table filters, CSV exports, filter presets, and a replay/simulate control for blocked events.
- Observe mode: log + flag only (no blocking).
- Enforce mode: block policy violations.
- Contain mode: enforce + network sandbox + allowlisted binaries.
./mcp-firewall --mode observe --policy policy.example.yaml -- <server-command>
./mcp-firewall --mode enforce --policy policy.example.yaml -- <server-command>
./mcp-firewall --mode contain --policy policy.example.yaml --no-network --allow-bin git -- <server-command>Notes:
--mode observeis shorthand for--dry-run.--mode containenables--no-networkautomatically, but you should still set--allow-binto constrain subprocesses.
Enable policy edits from the GUI:
./mcp-firewall --ui 127.0.0.1:8081 --policy policy.example.yaml --policy-write -- <server-command>Lock down the GUI/API with a token:
./mcp-firewall --ui 127.0.0.1:8081 --api-token YOUR_TOKEN --policy policy.example.yaml -- <server-command>Adjust how many versions to keep:
./mcp-firewall --ui 127.0.0.1:8081 --policy-history 50 --policy policy.example.yaml -- <server-command>Scan common MCP host config locations (Claude Desktop, Cursor, VS Code) and workspace roots:
./mcp-firewall --host-scan --host-root . --host-root ~/ProjectsWrap discovered stdio servers (and proxy HTTP servers) with the firewall:
./mcp-firewall --host-install \\
--policy policy.example.yaml \\
--no-network \\
--allow-bin git \\
--host-root . \\
--host-http-listen 127.0.0.1:17880 \\
--host-http-path /mcpThe installer writes timestamped backups next to each config file, plus a routes file (default ~/.mcp-firewall/routes.json) for HTTP upstreams, and flips on a global toggle file (default ~/.mcp-firewall/enabled).
Preview changes with diffs only:
./mcp-firewall --host-install --host-dry-run --policy policy.example.yaml --host-root .Uninstall (unwrap in place), or restore from the latest backups:
./mcp-firewall --host-uninstall --host-root .
./mcp-firewall --host-uninstall --host-restore --host-root .Run the HTTP proxy for those routes:
./mcp-firewall --listen 127.0.0.1:17880 --routes ~/.mcp-firewall/routes.json --path /mcp --ui 127.0.0.1:8081Use an enabled file to flip enforcement on/off:
./mcp-firewall --enabled-file ~/.mcp-firewall/enabled --enable
./mcp-firewall --enabled-file ~/.mcp-firewall/enabled --disable
./mcp-firewall --enabled-file ~/.mcp-firewall/enabled --statusWhen --enabled-file is set on the wrapper, missing file = bypass, present file = enforce. The GUI status panel includes a toggle button when this is configured.
Run the firewall as a reverse proxy in front of an MCP HTTP server:
./mcp-firewall --listen 127.0.0.1:8080 \
--upstream http://127.0.0.1:9000/mcp \
--path /mcp \
--ui-path /ui \
--allow-origins http://localhost:1234 \
--policy policy.example.yamlOpen http://127.0.0.1:8080/ui.
Notes:
--allow-originsis strongly recommended for browser clients.- The firewall expects streamable HTTP and supports SSE responses.
- Use
--routes ~/.mcp-firewall/routes.jsonto run in multi-upstream mode (paths like/mcp/<id>).
Generate an allowlist from a server's exact tool/prompt/resource names:
./mcp-firewall --discover --server-framing line -- <server-command> <args> > policy.discovered.yamlUse the policy below to allow only CLI-like tools and local resources. Everything else is blocked.
methods:
# Only restrict these if needed; leaving allow empty keeps defaults permissive.
deny: []
tools:
# Allow only CLI tools. Adjust tool names to match your server.
allow:
- "cli.*"
- "shell.*"
deny: []
resources:
# Allow only local/file-ish resources.
allow_schemes:
- "file"
- "local"
deny_schemes:
- "http"
- "https"
- "smtp"
- "imap"
- "s3"
prompts:
# Allow prompts by name if you expose any.
allow: []
deny: []Hardening (block outbound network from the MCP server process):
./mcp-firewall --no-network --policy policy.example.yaml -- <server-command> <args>Notes:
--no-networkonly applies to stdio/discover modes because HTTP upstreams run elsewhere.- On macOS this uses
sandbox-exec; on Linux it attemptsfirejailorunshare. Use--no-network-best-effortto proceed if no sandbox is available. - Use
--allow-bin git,ls(repeatable) to restrict which executables the server can spawn; this is enforced withsandbox-execorfirejail.
Enable inspection to flag suspicious outputs from tools/resources/prompts:
./mcp-firewall --inspect --inspect-threshold 5 --inspect-excerpt --policy policy.example.yaml -- <server-command>Per-source thresholds (useful when tool output is noisy):
./mcp-firewall --inspect \
--inspect-threshold-tools 7 \
--inspect-threshold-resources 5 \
--inspect-threshold-prompts 3 \
--policy policy.example.yaml -- <server-command>Optional hardening:
--inspect-redactreplaces suspicious text with[redacted by mcp-firewall].--inspect-blockblocks responses that cross the threshold.
Inspection is a lint layer by default: it logs decision=flagged events and does not block unless you enable it.
Classifier-backed inspection can send the extracted text to a governance service or PurpleLlama-compatible classifier endpoint before the normal block/redact decision:
./mcp-firewall --inspect \
--inspect-classifier-url http://127.0.0.1:8088/classify \
--inspect-classifier-provider purplellama \
--inspect-classifier-header "Authorization: Bearer $TOKEN" \
--inspect-classifier-threshold 0.7 \
--policy policy.example.yaml -- <server-command>The classifier receives JSON with provider, kind, and text. Responses may
return score, label, flags, and categories; matching responses are folded
into the same suspicionScore, suspicionFlags, classifier, and
classifierScore log fields used by the dashboard and enforcement modes.
methods: JSON-RPC method names (e.g.,tools/call,resources/read).tools: tool names intools/listortools/call.resources: resource URI patterns and scheme allow/deny lists.prompts: prompt names inprompts/listorprompts/get.
Rules are applied in this order:
- Deny list
- Allow list (if non-empty)
- Otherwise allowed
If an allow list is empty and strict=false, that section defaults to allow. Patterns use glob matching (*, ?) and are case-sensitive by default unless case_insensitive=true. Set methods.strict=true to default-deny unknown/unspecified methods. For resources, normalize=true canonicalizes schemes/hosts, decodes %XX, and cleans paths before matching.
MCP stdio uses line-delimited JSON in the current spec. This proxy defaults to --server-framing line, but you can switch to LSP-style framing with --server-framing lsp if needed.
Blocked traffic is logged as JSON lines to stderr by default. Use --log to write to a file and --log-allowed to log allowed events too. Suspicious outputs create decision=flagged log entries with suspicionScore and suspicionFlags fields. Each event includes stable requestId/traceId, direction, normalized targets, and the matched policyRule/policyPattern to make downstream analysis easier. The GUI reads logs via SSE from /api/logs/stream and renders a table with filters.
- Allowing a CLI tool still allows the model to fetch external data through that tool.
- Only MCP traffic routed through the wrapper or HTTP proxy is intercepted (direct stdio/HTTP connections bypass).
- HTTP mode is a reverse proxy for streamable HTTP servers (no legacy SSE transport adapter).
- Prompt-injection detection is heuristic and can produce false positives or negatives.
cmd/mcp-firewall/main.go- CLI entry pointinternal/firewall/*- proxy, policy, discovery, HTTP logic, and GUI assetspolicy.example.yaml- starter policy