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:

  • -32001 policy_denied, -32002 upstream_*/sse_truncated, -32003 rate_limited, -32005 unauthorized sit 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.)
  • -32020 and -32022 are emitted exactly as the spec defines them.
  • MCP’s resource-not-found error moved from -32002 to -32602 (Invalid Params); an upstream -32002 passing 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 -32004 response. 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, or X-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