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
| Code | Message | HTTP | Meaning | When |
|---|---|---|---|---|
-32700 | parse_error | 400 | Body is not valid JSON | Pre-parse rejection |
-32600 | invalid_request | 400 | Body is JSON but not a valid JSON-RPC request | Missing required fields |
-32601 | no_route | 404 | No upstream matched the tool prefix and default_upstream is unset | Routing |
-32603 | internal_error | 500 | Unhandled gateway-side error | Bug — file an issue |
-32001 | policy_denied | 403 | A deny policy rule matched | Policy |
-32002 | body_too_large | 413 | Request body exceeded the 16 MiB cap | Pre-parse rejection |
-32003 | rate_limited | 429 | A rate-limit bucket was exhausted | input_rate_limit or policy |
-32004 | license_invalid | 503 | License missing, signature wrong, or expired beyond grace | Server-wide; affects every request until rotated |
-32005 | unauthorized | 401 | Missing, invalid, or expired API key | Auth |
-32010 | upstream_unreachable | 502 | TCP connection to upstream refused or DNS failed | Upstream |
-32011 | upstream_timeout | 504 | Upstream did not respond within upstreams[].timeout | Upstream |
-32012 | upstream_protocol_error | 502 | Upstream 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.
| 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 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.
decision | Meaning | Sets rule_id? |
|---|---|---|
allow | No rule matched, or an explicit allow rule matched | only if 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 rate_limit rule’s bucket was empty | yes |
input_rate_limited | The pre-parse input_rate_limit bucket was empty | no |
parse_error | Body not valid JSON | no |
body_too_large | Body exceeded 16 MiB | no |
read_body | TCP error reading request body | no |
no_route | No matching upstream | no |
upstream_unreachable | Connect failed | no |
upstream_timeout | Read deadline exceeded | no |
license_invalid | License failed validation | no |
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.
| Value | Mapped from |
|---|---|
parse | parse_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
| Header | When | Value |
|---|---|---|
Retry-After | 429, 503 | Seconds until the bucket refills (rate-limit) or grace expires (license). |
WWW-Authenticate | 401 | Bearer realm="mcpgw" |
X-MCPGW-Rule-ID | 403, 429 (when a rule fired) | Rule id that produced the decision. |
X-MCPGW-Request-ID | every response | UUID for cross-correlation with audit log and spans. |
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 >16 MiB request bodies; raise the cap (v1.1) or split 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