Skip to content

Latest commit

 

History

History
292 lines (207 loc) · 9.81 KB

File metadata and controls

292 lines (207 loc) · 9.81 KB

MCP Firewall

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).

Easy setup (host-wide in minutes)

  1. Build:
go build ./cmd/mcp-firewall
  1. 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
  1. 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:8081

Open http://127.0.0.1:8081/ui.

Tip: use --host-dry-run to preview changes, and --host-uninstall --host-restore to roll back.

Quick start (stdio)

Build:

go build ./cmd/mcp-firewall

Run as a wrapper around a real MCP server:

./mcp-firewall --policy policy.example.yaml -- <server-command> <args>

GUI (local dashboard)

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.

Modes (mental model)

  • 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 observe is shorthand for --dry-run.
  • --mode contain enables --no-network automatically, but you should still set --allow-bin to 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>

Host-wide discovery + install

Scan common MCP host config locations (Claude Desktop, Cursor, VS Code) and workspace roots:

./mcp-firewall --host-scan --host-root . --host-root ~/Projects

Wrap 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 /mcp

The 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:8081

Global toggle

Use 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 --status

When --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.

HTTP mode (streamable HTTP)

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.yaml

Open http://127.0.0.1:8080/ui.

Notes:

  • --allow-origins is strongly recommended for browser clients.
  • The firewall expects streamable HTTP and supports SSE responses.
  • Use --routes ~/.mcp-firewall/routes.json to run in multi-upstream mode (paths like /mcp/<id>).

Discover allowlist policy

Generate an allowlist from a server's exact tool/prompt/resource names:

./mcp-firewall --discover --server-framing line -- <server-command> <args> > policy.discovered.yaml

Example: "message + CLI only"

Use 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-network only applies to stdio/discover modes because HTTP upstreams run elsewhere.
  • On macOS this uses sandbox-exec; on Linux it attempts firejail or unshare. Use --no-network-best-effort to proceed if no sandbox is available.
  • Use --allow-bin git,ls (repeatable) to restrict which executables the server can spawn; this is enforced with sandbox-exec or firejail.

Inspection (prompt-injection heuristics)

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-redact replaces suspicious text with [redacted by mcp-firewall].
  • --inspect-block blocks 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.

Policy model

  • methods: JSON-RPC method names (e.g., tools/call, resources/read).
  • tools: tool names in tools/list or tools/call.
  • resources: resource URI patterns and scheme allow/deny lists.
  • prompts: prompt names in prompts/list or prompts/get.

Rules are applied in this order:

  1. Deny list
  2. Allow list (if non-empty)
  3. 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.

Framing (stdio)

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.

Logging

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.

Limitations

  • 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.

Files

  • cmd/mcp-firewall/main.go - CLI entry point
  • internal/firewall/* - proxy, policy, discovery, HTTP logic, and GUI assets
  • policy.example.yaml - starter policy