Error code reference
mcpgw maps every error to both an HTTP status and a JSON-RPC error code. Clients should branch on the JSON-RPC code (stable across transports); humans usually look at the HTTP code first.
JSON-RPC error codes
The code is reused across several conditions; the message field (which equals the audit error_kind) is the precise discriminator. This table reflects exactly what the proxy emits.
| Code | Message | HTTP | Meaning |
|---|---|---|---|
-32700 |
parse_error |
400 | Body is not valid JSON-RPC |
-32700 |
body_too_large |
413 | Request body exceeded the 16 MiB cap |
-32700 |
read_body |
400 | Error reading the request body |
-32005 |
unauthorized |
401 | Missing, invalid, or expired credential (API key or Bearer token) |
-32001 |
policy_denied |
403 | A deny policy rule matched, or a guardrail webhook returned reject / was unreachable with fail_open: false (fail-closed; audit error_kind: guardrail_unreachable — the one case where the audit tag is more specific than the wire message) |
-32003 |
rate_limited |
429 | An input_rate_limit or policy rate_limit bucket was exhausted |
-32601 |
no_route |
404 | No upstream matched the tool and default_upstream is unset |
-32020 |
header_mismatch |
400 | Mcp-Method/Mcp-Name header disagrees with the JSON-RPC body (2026-07-28 anti-smuggling; never reconciled, always rejected) |
-32022 |
unsupported_protocol_version |
400 | Declared protocol revision is not in allowed_protocol_versions |
-32002 |
upstream_invalid |
502 | Gateway could not build the upstream request |
-32002 |
upstream_unreachable |
504 | Connect to upstream failed / no response |
-32002 |
upstream_read |
502 | Error reading the upstream response |
-32002 |
upstream_too_large |
502 | Upstream response exceeded the 16 MiB cap |
JSON-RPC errors with no associated request id (e.g. parse errors before id extraction) are returned with "id": null.
Allocation policy (2026-07-28)
The MCP spec partitions the JSON-RPC server-error range: -32000 to -32019 is implementation-defined (existing SDK/gateway usage grandfathered), -32020 to -32099 is reserved for the MCP specification. mcpgw is compliant on both sides:
-32001policy_denied,-32002upstream_*/sse_truncated,-32003rate_limited,-32005unauthorizedsit in the implementation-defined range. (The draft-era spec codes that shared these numbers were renumbered:HeaderMismatch-32001→-32020,UnsupportedProtocolVersion-32004→-32022— so there is no longer any collision with mcpgw’s codes.)-32020and-32022are emitted exactly as the spec defines them.- MCP’s resource-not-found error moved from
-32002to-32602(Invalid Params); an upstream-32002passing through the gateway should no longer be confused with it. mcpgw forwards upstream error bodies untouched either way.
License failures do not produce a JSON-RPC error. A missing/invalid/expired-beyond-grace license is fail-closed: the process refuses to start (exit 78) and a running gateway reports it via
/readyz→503. There is no per-request-32004response. Earlier drafts documented-32004,-32600,-32603,-32010,-32011, and-32012; none of those codes are emitted.
HTTP-only paths
These return plain HTTP without a JSON-RPC body.
| Path | Status | Meaning |
|---|---|---|
/healthz |
200 | Process alive |
/readyz |
200 | License valid (within grace) |
/readyz |
503 | License expired beyond grace |
| any other path | 404 | Unknown endpoint |
/mcp non-POST |
405 | Method not allowed |
Audit action values
The audit log’s action field (JSON key action, not decision) takes one of the following values. The reason a request failed is carried separately in error_kind, not in action — pre-parse and upstream failures all share action: reject and differ by error_kind.
action |
Meaning | Sets rule_id? |
|---|---|---|
allow |
No rule matched, or an explicit allow rule matched |
only if an explicit allow rule fired |
deny |
A deny rule matched |
yes |
redact |
A redact rule matched and at least one substitution was applied |
yes |
rate_limit_blocked |
A policy rate_limit rule’s bucket was empty |
yes |
strip_app |
A strip_app rule removed UI blocks (stripped counts them) |
yes |
reject |
Request rejected before/around routing — see error_kind for the cause (input_rate_limited, body_too_large, read_body, parse_error, unauthorized, no_route, header_mismatch, unsupported_protocol_version, upstream_invalid, upstream_unreachable, upstream_read, upstream_too_large) |
no |
deny_frame |
An SSE response frame was dropped by a server_to_client deny rule (direction: server_to_client) |
yes |
redact_frame |
An SSE response frame’s data: was rewritten by a redact rule |
yes |
synthesize_list |
Tool-search mode answered tools/list from the gateway index |
no |
synthesize_search |
Tool-search mode answered tools/call mcp_search from the gateway index |
no |
rule_id is empty whenever no rule fired. The full per-field schema lives in audit-log.md; error_kind/error_code carry failure detail.
Span mcp.error.kind values
The mcp.error.kind span attribute is set on the upstream, streaming, and
guardrail-callout failure paths:
| Value | HTTP | Meaning |
|---|---|---|
upstream_invalid |
502 | Could not build the upstream request |
upstream_unreachable |
504 | Connect to upstream failed / no response |
upstream_read |
502 | Error reading the upstream response |
upstream_too_large |
502 | Upstream response exceeded the 16 MiB cap |
sse_truncated |
200 (stream already begun) | SSE response cut mid-stream (e.g. frame exceeded max_sse_frame_bytes) |
guardrail_unreachable |
403 | Guardrail webhook callout failed (non-200, timeout, malformed verdict) with fail_open: false; the request was denied |
guardrail_body_too_large |
403 | The JSON-RPC envelope exceeded the endpoint’s max_body_bytes, so it was refused before any callout (never truncated), with fail_open: false; the request was denied. The endpoint is healthy — raise the cap or narrow the rule’s matchers |
Pre-parse, auth, policy, and rate-limit outcomes are not recorded on
mcp.error.kind; they appear in the audit log’s error_kind field and (for
policy) the mcp.policy.decision span attribute. The full per-request error
taxonomy is the audit error_kind field.
Headers set on errors
| Header | When | Value |
|---|---|---|
WWW-Authenticate |
401 | Bearer realm="mcpgw", plus , resource_metadata="<url>" when OAuth metadata is configured and , error="invalid_token" / , error="insufficient_scope" per RFC 6750. |
The gateway does not set
Retry-After,X-MCPGW-Rule-ID, orX-MCPGW-Request-ID. Earlier drafts documented those; they are not emitted.
Common debugging patterns
| Symptom | First check |
|---|---|
403 policy_denied from a known-good tool |
audit.jsonl rule_id — almost always a wildcard rule shadowing a specific allow |
404 no_route at client startup |
default_upstream is unset; spec-compliant clients send initialize first |
413 body_too_large |
A client is shipping a request body over the fixed 16 MiB limit; split or reduce the call |
429 rate_limited before parse/auth |
Tune input_rate_limit.requests_per_second and burst, or apply edge throttling before mcpgw |
429 rate_limited on a specific tool |
Tune the matching policy rule’s tokens_per_second and burst; the bucket is sized for steady state, not bursts |
401 unauthorized |
Check the client header, key expiry, and auth.keys[].hash in config |
502 upstream_unreachable immediately on every call |
Upstream service DNS/IP from inside the mcpgw container is wrong; not a gateway issue |
504 upstream_timeout on a specific tool |
Upstream is slow for that tool; raise upstreams[].timeout or fix the upstream |
503 license_invalid from a previously-working gateway |
License rotated badly or grace expired — see How-to: rotate license |
Related
- Audit log schema
- Telemetry reference —
mcp.error.kindenum - How-to: tail audit log