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-postgresThe 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:
- Budget check — is there budget remaining for this call?
- Loop detection — have we seen this tool+args combination before?
- Side-effect check — is this tool known to have side-effects?
- 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>| Option | Description |
|---|---|
--max-cost <n> | Override max_cost_per_run from config |
--max-iterations <n> | Override max_iterations from config |
--trace | Write all call traces to ./fuze-proxy-traces.jsonl |
--verbose | Print intercepted calls and decisions to stderr |
--daemon | Connect 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:
| Key | Type | Description |
|---|---|---|
estimated_cost | float | Cost attributed to each invocation of this tool |
side_effect | bool | Whether this tool mutates external state |
max_calls_per_run | int | Maximum times this tool can be called in one run |
timeout | string | Per-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:
- Calls
await router.stop()to flush pending traces and finalize the run - Writes any remaining trace entries to
./fuze-proxy-traces.jsonl(if--traceis enabled) - Closes the connection to the daemon (if running in daemon mode)
- 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/callrequests 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-postgresWith 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-daemonThe proxy detects the platform automatically -- no configuration needed. Path traversal checks also use path.relative() for correct behavior on Windows paths.