Skip to content

Tool governance

mcppipe sees every JSON-RPC request before it reaches your upstream, so it can govern what runs and how often — without you touching the MCP server. All controls in this guide are per-tunnel and live under the tunnel’s Settings and Tools tabs in the dashboard.

Governance is available on every plan. It only applies to tunnels in the default capture mode — a zero-knowledge tunnel routes raw bytes, so mcppipe never parses traffic and tool policy, rate limits, and pinning are all inactive there.

A tunnel’s tool policy decides which tools/call requests mcppipe forwards. Set it under Settings → Tool policy. There are three modes:

ModeBehaviour
Allow every toolForward every tools/call. The default — no enforcement.
Allow only the listed toolsForward a tools/call only when its tool name is in the list; deny the rest.
Deny the listed toolsDeny a tools/call whose tool name is in the list; forward the rest.

Tool policy editor in tunnel settings Choosing a mode and managing the tool list under Settings → Tool policy.

Policy applies only to tools/call. Other methods (tools/list, prompts/get, …) are always forwarded — the policy models tool-level governance, not method-level. A tools/call with no params.name is also forwarded, so the upstream produces its own error rather than mcppipe pre-empting it.

The list is capped at 32 tool names. Names are trimmed, sorted, and deduplicated when you save.

A blocked tools/call gets HTTP 403 and a JSON-RPC error envelope. The envelope mirrors the shape used for upstream errors, so an agent’s existing error handler needs no extra parsing:

{
"jsonrpc": "2.0",
"id": 42,
"error": {
"code": -32000,
"message": "Tool denied by tunnel policy",
"data": {
"policy_code": "tool_denied",
"http_status": 403,
"tool": "delete_file"
}
}
}

The response also carries an x-mcppipe-error: tool_denied header, and the refusal is recorded as an is_error event so it appears in the Inspector next to the original request.

Rate limits cap how often a method or a specific tool can be called per minute. Configure them under Settings → Rate limits as two tables of (name, per_minute) rows — one keyed by JSON-RPC method, one keyed by tool name.

Per-method and per-tool rate limit editor Method-scoped and tool-scoped limits, each a per-minute cap.

mcppipe enforces an approximate sliding window (current + previous minute buckets) that avoids the burst a fixed bucket would allow at a minute boundary. On a tools/call, the method rule and the tool rule are both evaluated — both must pass, and whichever bucket trips first refuses the request. A per_minute of 0 disables a rule; each map is capped at 32 entries.

An over-limit request gets HTTP 429 plus a Retry-After header (seconds until the next minute boundary, clamped to 1–60) and a JSON-RPC envelope:

{
"jsonrpc": "2.0",
"id": 7,
"error": {
"code": -32000,
"message": "Rate limit reached for this tunnel",
"data": {
"policy_code": "rate_limited",
"http_status": 429,
"retry_after_seconds": 17,
"limit_scope": "tool",
"limit_name": "run_query"
}
}
}

limit_scope is method or tool, and limit_name tells you which rule tripped. As with denials, the x-mcppipe-error header is set and the event is recorded for the Inspector.

The Tools tab shows per-tool, per-method aggregates over the last 24 hours of recorded events: call count, error count, error rate, average latency, p95 latency, and the last-call timestamp. For an aggregator tunnel, the upstream prefix (<upstream>__tool) is split into its own column. The table auto-refreshes every 30 seconds, so you can watch a live debug session without reloading.

Tools tab showing per-tool call stats Per-(tool, method) calls, error rate, and p95 latency over the last 24 hours.

Stats are computed from the same recorded event stream the Inspector reads. The window can be widened via the hours query parameter (1–168) backing the table.

A malicious or compromised MCP server can quietly change a tool’s description or input schema after you have approved it — a “rug-pull.” mcppipe detects this by pinning a SHA-256 of each tool descriptor (name + description + inputSchema, canonicalised so key reordering does not count as a change).

Every tools/list response that flows through mcppipe is hashed and compared to the stored pinset. The descriptor key order is normalised before hashing, so an upstream that merely re-serializes its JSON is not flagged.

Tool pins with approval status badges Each pin shows its description SHA and a status: unapproved, approved, or RUG-PULL.

In the Tools tab, each pin shows a status:

  • unapproved — mcppipe has seen the tool but you have not blessed a baseline yet.
  • approved — the live description matches your approved baseline.
  • RUG-PULL — the live description differs from the approved baseline.

Click Approve to stamp the tool’s current shape as the baseline (this calls POST /api/tunnels/:id/tool-pins/:tool/approve). From then on, any tools/list whose descriptor differs from that baseline is a drift.

Drift is always detected and emitted as lifecycle events:

  • A change against the last-seen pin fires tool.changed.
  • A change against the owner-approved baseline fires tool.rug_pull.

Both are delivered to any matching webhooks. What happens to the agent’s calls is controlled by the Rug-pull protection toggle under Settings:

ModeBehaviour
warn (default)Drift raises tool.rug_pull only. Calls are still forwarded.
blockDrift additionally refuses tools/call for the drifted tool before it reaches your server.

In block mode, a tools/call for a drifted tool gets HTTP 403:

{
"jsonrpc": "2.0",
"id": 9,
"error": {
"code": -32000,
"message": "Tool blocked: changed from approved baseline (rug-pull)",
"data": {
"policy_code": "tool_pin_violation",
"http_status": 403,
"tool": "transfer_funds"
}
}
}

Re-approving the tool’s new shape clears the block immediately. If the upstream reverts a tool to its approved shape, the block self-heals on the next tools/list.

Every governance refusal — denied tool, rate limit, or pin violation — is recorded as an error event with its policy_code in the payload, so you can trace exactly which control fired and why.

A blocked tool call surfaced in the live event stream A policy refusal recorded alongside the original request in live traffic.