Skip to main content

Coding Agent Protection — Local MCP Broker

The Local MCP Broker is a subsystem of pavri-endpointd that sits in front of every configured MCP server on a developer's workstation. It is the local enforcement point for D-06 (MCP tool calls outside allowlist) and the source of CodingActionEvent records for every observed tool call.

This page covers what the broker rewrites, how it enforces policy, what the supported transports are, and how to opt out for specific assistants or specific MCP servers.


What the broker does

When pavri-endpointd enrolls an endpoint, the broker subsystem:

  1. Discovers every assistant's MCP config file on the filesystem (Claude Desktop, Cursor, VS Code, JetBrains, Gemini CLI, Windsurf).
  2. Backs up the original config bytes alongside a SHA-256 fingerprint into the sensor's manifest.
  3. Rewrites each MCP server entry so the upstream URL or command points at a sensor-managed endpoint:
    • STDIO upstreams: rewritten to command: pavri-endpointd broker-stdio --upstream-id <id> so the assistant launches a thin stub that forwards over a UDS to the broker.
    • HTTP+SSE-legacy / Streamable HTTP upstreams: rewritten to a loopback URL (http://127.0.0.1:<port>/mcp/<upstream-id>/...) that the broker exposes.
  4. Watches every rewritten config for drift via fsnotify. If the file changes, the broker reads the new content, queries the file observer for the most recent attributed writer, and classifies the change. Each classification produces a ConfigDriftEvent with drift_kind set to one of:
    • user_edit — a non-assistant process (or no observed writer) wrote the change. Emit only; the broker does not auto-clobber a user's manual edit in CAP-1.3.
    • assistant_self_modification — the writer chain matches a registered assistant binary. No clobber. Emit and preserve the on-disk content. This is D-17 evidence.
    • schema_unknown — the new content does not parse against the rewriter's schema. Emit only.
    • restore_verification_failed — a Restore attempt produced content whose SHA does not match the manifest's OriginalContentSHA256. Emit only.

When the assistant issues a tools/call:

  1. The broker parses the JSON-RPC frame and looks up the session for the calling assistant.
  2. It evaluates endpoint.action.mcp_tool_call:<server>.<tool> against the active policy snapshot (cached locally; see Policy authoring).
  3. Allow → the original frame is forwarded to the real upstream and a CodingActionEvent with verdict = ALLOWED is emitted.
  4. Deny → a JSON-RPC error response is written back to the assistant, the upstream never sees the frame, and a CodingActionEvent with verdict = DENIED is emitted.
  5. No matching rule → treated as Allow (observe-mode fallback). CAP-1.5 will gate this on the per-tenant protection profile.

Hot-swap is sub-millisecond: a snapshot apply takes effect on the next frame the broker evaluates.


Supported transports

TransportWhenAdapter
STDIOThe assistant config defines command + args (most common case).Spawns the original upstream as a child process; bidirectional newline-delimited JSON-RPC framing.
HTTP+SSE-legacyThe assistant config defines url and transport: sse.Two endpoints: GET /mcp/<id>/sse (event stream from upstream), POST /mcp/<id>/messages (client → upstream).
Streamable HTTPThe assistant config defines url and transport: streamable_http.Single duplex endpoint at POST /mcp/<id> with chunked request/response bodies.

All three transports run through the same eval gate; only the framing differs.


What gets rewritten

Per assistant, the broker discovers and rewrites the following config files (existence depends on whether the assistant is installed):

AssistantConfig path (macOS)Config path (Linux)Format
Claude Desktop~/Library/Application Support/Claude/claude_desktop_config.json~/.config/Claude/claude_desktop_config.jsonJSON
Cursor~/.cursor/mcp.json~/.cursor/mcp.jsonJSON
VS Code<workspace>/.vscode/mcp.json and ~/Library/Application Support/Code/User/mcp.json<workspace>/.vscode/mcp.json and ~/.config/Code/User/mcp.jsonJSON
JetBrains~/Library/Application Support/JetBrains/<product>/mcp.xml~/.config/JetBrains/<product>/mcp.xmlXML
Gemini CLI~/.config/gemini/settings.json~/.config/gemini/settings.jsonJSON
Windsurf~/.codeium/windsurf/mcp_config.json~/.codeium/windsurf/mcp_config.jsonJSON

Windows paths are resolved analogously (Windows runtime is gated to CAP-2.1; the rewriter contract compiles and tests pass cross-platform).

Within each config, only the transport endpoint (URL or command) is mutated. Every other user field — env, headers, custom keys, ordering, comments where the format permits — is preserved byte-stable.

Secrets

The broker never persists secret values. Env values and auth header values are extracted as key names only and stored on the manifest as OriginalEnvKeys / OriginalAuthHeaderKeys. At upstream-spawn time the broker re-reads the values from the user's current environment. This means the manifest is safe to inspect and synchronize, and a sensor restart picks up rotated credentials without re-rewriting.


How to opt out

The broker is on by default once the sensor is enrolled. Opt-out in CAP-1.3 is policy-driven, not env-driven; the env-var knobs and operator-facing uninstall-broker CLI subcommand land in CAP-1.5 alongside the policy DSL.

Observation mode

The protection profile's observation_mode controls broker enforcement:

  • enforce (default once a deny rule is configured) — tools/call frames matching a deny rule are blocked at the broker.
  • observe — every tools/call is forwarded; the broker still emits CodingActionEvent for every frame, with verdict = ALLOWED regardless of policy match. Useful for staging policy changes without breaking developer workflows.

CAP-1.5 will surface this as a per-tenant DSL knob; until then, operators set it via the policy bundle's protection_profile.coding_agent block.

Restoring originals

To revert rewrites manually in CAP-1.3, inspect each *.pavri.bak file under the assistant's config directory and copy it back over the rewritten file. The sensor's fsnotify watcher will classify this manual revert as user_edit and emit a ConfigDriftEvent for audit; it will not re-rewrite the file (CAP-1.3 emits-only on user edits and defers re-rewrite to CAP-1.5).

If you need to disable the broker entirely on a specific endpoint, unenroll the sensor (the broker subsystem only runs while enrolled). A first-class pavri-endpointd uninstall-broker subcommand and per- assistant disable knobs ship in CAP-1.5.


Verifying the broker is working

After enrollment:

  1. Inspect claude_desktop_config.json (or your assistant's MCP config). Each command field should be pavri-endpointd broker-stdio --upstream-id <opaque-id>. Each url field for HTTP-based servers should resolve to 127.0.0.1.
  2. Make a tool call from the assistant — for example, ask Claude Desktop to list files in your home directory if you have the filesystem MCP server configured.
  3. In the Pavri dashboard, navigate to Coding > Sessions. The call should appear within 5 seconds with verdict = ALLOWED and the redacted argument set visible.

If the call does not appear, check:

  • Is the sensor enrolled? journalctl -u pavri-endpointd | grep enroll.
  • Did the rewrite succeed? Look for *.pavri.bak next to the assistant's config.
  • Is the broker UDS listening? ls $PAVRI_ENDPOINT_STATE_DIR/sockets/.

Performance

CAP-1.3 §11 exit gates: policycache.Evaluate p99 < 1ms cache-hit; broker.Evaluator.EvaluateFrame (full pipeline including session lookup, redaction, and telemetry emit) p99 < 5ms cache-hit.

Locally observed on a typical developer laptop: eval p99 ≈ 250ns, broker p99 ≈ 5µs. The broker should not be detectable in the end-to-end latency budget of any tool call that returns in fewer than several milliseconds.