Error 403: ip_forbidden, tool_denied, suspended
A 403 means a per-tunnel control refused the request. Four different gates
return it, and they do not answer in the same shape — two send JSON, one sends
a JSON-RPC error envelope, and one sends plain text. Identify yours first.
Telling them apart
Section titled “Telling them apart”| What you got back | Gate |
|---|---|
{"error": "ip_forbidden", …} | IP allowlist |
Plain text this tunnel has been suspended | Operator suspension |
JSON-RPC -32000, data.policy_code = tool_denied | Tool policy |
JSON-RPC -32000, data.policy_code = tool_pin_violation | Tool pinning |
The two JSON-RPC cases also set an x-mcppipe-error response header carrying
the same code, which is usually the easiest thing to match on in a client.
ip_forbidden {#ip_forbidden}
Section titled “ip_forbidden {#ip_forbidden}”{ "error": "ip_forbidden", "error_description": "your client IP is not in this tunnel's allowlist"}The tunnel has a CIDR allowlist and the caller’s resolved IP is not in it. A
second variant reads "unknown client ip", which means mcppipe could not
resolve a client IP at all.
This gate runs before authentication and before quota, deliberately: a blocked address should not be able to spend your auth budget or move your quota counters just by knocking.
Fixes. Add the caller’s IP or CIDR in Settings, or apply a vendor preset. Two things catch people out:
- Cloud agents do not call from your IP. Claude Managed Agents and ChatGPT connectors reach your tunnel from the provider’s infrastructure, so the allowlist entry has to be the vendor’s egress range, not yours. That is what the presets are for.
- Behind a proxy, the resolved IP may not be the real client. Confirm your trusted-proxy configuration, or the allowlist will be comparing against the wrong address.
See IP allowlist & access control.
Suspended {#suspended}
Section titled “Suspended {#suspended}”this tunnel has been suspendedAn operator suspended the tunnel. This is checked before every other gate, so nothing else about the request matters. There is no machine-readable code in the body — plain text is all you get.
Contact support@mcppipe.dev.
tool_denied {#tool_denied}
Section titled “tool_denied {#tool_denied}”{ "jsonrpc": "2.0", "id": 1, "error": { "code": -32000, "message": "Tool denied by tunnel policy", "data": { "policy_code": "tool_denied", "http_status": 403, "tool": "delete_everything" } }}The tunnel’s tool policy does not allow that tool. The underlying message names
it: the tunnel's policy denies calling tool `<name>` .
The -32000 code is chosen on purpose — it is the JSON-RPC “server error”
range, and specifically not -32603 (Internal error), so an agent counting
internal errors does not suddenly start counting your policy decisions as
outages.
Fix. Allow the tool, or change the policy mode, in Settings → Tool policy. See Tool governance.
tool_pin_violation {#tool_pin_violation}
Section titled “tool_pin_violation {#tool_pin_violation}”{ "jsonrpc": "2.0", "id": 1, "error": { "code": -32000, "message": "Tool blocked: changed from approved baseline (rug-pull)", "data": { "policy_code": "tool_pin_violation", "http_status": 403 } }}The tool’s live description no longer matches the baseline the tunnel owner
approved, and pin enforcement is set to block. The full message reads:
tool `<name>` changed from its approved baseline (rug-pull); blocked by tunnel pin enforcement.
This is the gate doing its job. A tool whose description changes underneath a running agent can redirect what that agent does without any code changing — the “rug-pull” pattern. Before you approve the new baseline, look at what actually changed.
Fix. Review the diff in the Tools tab. If the change is legitimate (you upgraded the upstream server), approve a new baseline. If you cannot explain it, do not.
See Tool governance.
Not this error?
Section titled “Not this error?”- 401 — authentication, which runs after the IP allowlist.
- 404 — the host serves nothing at all.
- 429 — allowed, but too often or over quota.