Error codes

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

CodeMessageHTTPMeaningWhen
-32700parse_error400Body is not valid JSONPre-parse rejection
-32600invalid_request400Body is JSON but not a valid JSON-RPC requestMissing required fields
-32601no_route404No upstream matched the tool prefix and default_upstream is unsetRouting
-32603internal_error500Unhandled gateway-side errorBug — file an issue
-32001policy_denied403A deny policy rule matchedPolicy
-32002body_too_large413Request body exceeded the 16 MiB capPre-parse rejection
-32003rate_limited429A rate-limit bucket was exhaustedinput_rate_limit or policy
-32004license_invalid503License missing, signature wrong, or expired beyond graceServer-wide; affects every request until rotated
-32005unauthorized401Missing, invalid, or expired API keyAuth
-32010upstream_unreachable502TCP connection to upstream refused or DNS failedUpstream
-32011upstream_timeout504Upstream did not respond within upstreams[].timeoutUpstream
-32012upstream_protocol_error502Upstream returned a non-MCP response (HTML error page, etc.)Upstream

JSON-RPC errors with no associated request id (e.g. parse errors before id extraction) are returned with "id": null.


HTTP-only paths

These return plain HTTP without a JSON-RPC body.

PathStatusMeaning
/healthz200Process alive
/readyz200License valid (within grace)
/readyz503License expired beyond grace
any other path404Unknown endpoint
/mcp non-POST405Method not allowed

Audit decision values

The audit log’s decision field uses these values. They are a strict superset of the JSON-RPC error space because they include success and pre-parse rejections.

decisionMeaningSets rule_id?
allowNo rule matched, or an explicit allow rule matchedonly if explicit allow rule fired
denyA deny rule matchedyes
redactA redact rule matched and at least one substitution was appliedyes
rate_limit_blockedA rate_limit rule’s bucket was emptyyes
input_rate_limitedThe pre-parse input_rate_limit bucket was emptyno
parse_errorBody not valid JSONno
body_too_largeBody exceeded 16 MiBno
read_bodyTCP error reading request bodyno
no_routeNo matching upstreamno
upstream_unreachableConnect failedno
upstream_timeoutRead deadline exceededno
license_invalidLicense failed validationno

rule_id is empty whenever no rule fired.


Span mcp.error.kind values

Set on error spans only. success cases do not set this attribute.

ValueMapped from
parseparse_error
policy_deny-32001 / decision deny
rate_limited-32003 / decision rate_limit_blocked
license_invalid-32004
upstream_timeout-32011
upstream_unreachable-32010
upstream_protocol-32012
body_too_large-32002
internal-32603

Headers set on errors

HeaderWhenValue
Retry-After429, 503Seconds until the bucket refills (rate-limit) or grace expires (license).
WWW-Authenticate401Bearer realm="mcpgw"
X-MCPGW-Rule-ID403, 429 (when a rule fired)Rule id that produced the decision.
X-MCPGW-Request-IDevery responseUUID for cross-correlation with audit log and spans.

Common debugging patterns

SymptomFirst check
403 policy_denied from a known-good toolaudit.jsonl rule_id — almost always a wildcard rule shadowing a specific allow
404 no_route at client startupdefault_upstream is unset; spec-compliant clients send initialize first
413 body_too_largeA client is shipping >16 MiB request bodies; raise the cap (v1.1) or split the call
429 rate_limited before parse/authTune input_rate_limit.requests_per_second and burst, or apply edge throttling before mcpgw
429 rate_limited on a specific toolTune the matching policy rule’s tokens_per_second and burst; the bucket is sized for steady state, not bursts
401 unauthorizedCheck the client header, key expiry, and auth.keys[].hash in config
502 upstream_unreachable immediately on every callUpstream service DNS/IP from inside the mcpgw container is wrong; not a gateway issue
504 upstream_timeout on a specific toolUpstream is slow for that tool; raise upstreams[].timeout or fix the upstream
503 license_invalid from a previously-working gatewayLicense rotated badly or grace expired — see How-to: rotate license