MCP Proxy

The MCP Proxy wraps any MCP server with Fuze protection — zero code changes required.

Usage

# Without Fuze
npx @modelcontextprotocol/server-postgres

# With Fuze (same server, fully protected)
npx fuze-ai proxy -- npx @modelcontextprotocol/server-postgres

The MCP server doesn't know Fuze exists. Fuze sits between the MCP client and the real server.

What gets intercepted

Every tools/call JSON-RPC request passes through Fuze:

  1. Budget check — is there budget remaining for this call?
  2. Loop detection — have we seen this tool+args combination before?
  3. Side-effect check — is this tool known to have side-effects?
  4. Audit logging — record the call in the trace

If all checks pass, the request is forwarded to the real MCP server unchanged.

CLI options

npx fuze-ai proxy [options] -- <server-command>
OptionDescription
--max-cost <n>Override max_cost_per_run from config
--max-iterations <n>Override max_iterations from config
--traceWrite all call traces to ./fuze-proxy-traces.jsonl
--verbosePrint intercepted calls and decisions to stderr
--daemonConnect to the Fuze daemon for cross-run enforcement

Configuration

Configure the proxy in your fuze.toml. Per-tool settings use the [proxy.tools.TOOLNAME] table format:

[defaults]
max_cost_per_step = 1.00
kill_on_loop = true

[proxy]
max_cost_per_run = 5.00
max_iterations = 50

[proxy.tools.query]
estimated_cost = 0.02

[proxy.tools.execute]
estimated_cost = 0.05
side_effect = true
max_calls_per_run = 10

[proxy.tools.delete_record]
estimated_cost = 0.01
side_effect = true
max_calls_per_run = 5
timeout = "10s"

Each [proxy.tools.<name>] section supports:

KeyTypeDescription
estimated_costfloatCost attributed to each invocation of this tool
side_effectboolWhether this tool mutates external state
max_calls_per_runintMaximum times this tool can be called in one run
timeoutstringPer-call timeout (e.g. "30s", "2m")

Graceful shutdown

When the proxy receives a shutdown signal (SIGINT, SIGTERM, or the MCP client disconnects), it performs a clean teardown:

  1. Calls await router.stop() to flush pending traces and finalize the run
  2. Writes any remaining trace entries to ./fuze-proxy-traces.jsonl (if --trace is enabled)
  3. Closes the connection to the daemon (if running in daemon mode)
  4. Exits the process

This ensures no trace data is lost, even if the client disconnects abruptly.

JSON-RPC request/response ID tracking

The proxy matches JSON-RPC responses to their originating requests by ID, not by payload shape. This is important because:

  • MCP servers may return responses out of order
  • Multiple tools/call requests can be in flight simultaneously
  • Matching by ID ensures the correct budget and trace entries are updated for each response

Each intercepted request's JSON-RPC id is stored in a pending map. When a response arrives, the proxy looks up the original request by id, attributes the cost, and records the result in the trace.

Trace output

When --trace is enabled, every tool call is logged to ./fuze-proxy-traces.jsonl as one JSON object per line. Traces are written only at result time (not at intercept time), so each line contains both the request and the response:

{"timestamp":"2026-03-28T12:00:00Z","tool":"query","args":{"sql":"SELECT ..."},"result":{"rows":42},"cost":0.02,"duration_ms":150}

This avoids duplicate trace entries and ensures every logged call has a known outcome.

Transports

The proxy supports all MCP transport types:

  • Stdio — pipes stdin/stdout between client and server
  • SSE — proxies HTTP Server-Sent Events streams
  • Streamable HTTP — proxies HTTP request/response

Combined with the daemon

# Terminal 1: Start daemon
npx fuze-ai daemon

# Terminal 2: Start proxied MCP server
npx fuze-ai proxy -- npx @modelcontextprotocol/server-postgres

With the daemon running, all MCP calls participate in cross-run pattern detection and org-wide budget enforcement.

Windows support

On Windows, the daemon socket uses a named pipe instead of a Unix domain socket:

\\.\pipe\fuze-daemon

The proxy detects the platform automatically -- no configuration needed. Path traversal checks also use path.relative() for correct behavior on Windows paths.