error.code, and soft-behavior envelopes inside result.content[0].text — and a single failure can touch one, two, or all three. Triage from the outside in: HTTP status → JSON-RPC code → soft envelope.
For the projection grammar itself, see the JMESPath guide. For per-tool parameters and freshness budgets, see the Tools Reference.
Quick orientation
- HTTP status is the transport-layer answer. Most JSON-RPC replies — successes AND errors — come back as HTTP 200, per the JSON-RPC 2.0 convention. The handler only escalates the status when the failure is something a generic HTTP client must react to (auth, daily cap, service unavailable) and benefits from a
Retry-After/WWW-Authenticateheader. - JSON-RPC
error.codeis the application-layer answer. Six codes are in use:-32001,-32029,-32600,-32601,-32602,-32603. The handler never emits another code — if you see one, treat it as a wire bug and file an issue. - Soft-behavior envelopes are the high-volume failure mode.
tools/callsucceeds at the JSON-RPC layer (HTTP 200, noerrorfield), but the JSON sitting insideresult.content[0].textcarries a_budget_exceededor_jmespath_errordiscriminator. Clients that only inspect the JSON-RPC envelope will silently treat these as successes — parseresult.content[0].textand check for a leading_key before consuming the payload as data. - Executed calls stay charged.
_budget_exceeded,_jmespath_error, and tool-execution errors (-32603) all happen after the tool has run, so they consume the Pro daily-quota slot. Only pre-dispatch failures such as daily-cap rejection or quota-reservation service failure avoid charging the slot. - Both 401 paths set
WWW-Authenticatewithrealm="worldmonitor"and aresource_metadatapointer at/.well-known/oauth-protected-resource. RFC 9728-aware clients (Claude Desktop, MCP Inspector) bounce through the OAuth flow on this header without further intervention.
JSON-RPC error codes
Subsections below give the literal payload, the trigger site, and what to do for each code.
-32001 — Unauthenticated / invalid credentials
Fires from six api/mcp/auth.ts emission sites, always paired with HTTP 401 and a WWW-Authenticate header. Those sites collapse to five user-visible triggers, in order of how clients hit them:
- No
Authorizationbearer AND noX-WorldMonitor-Key— the client called/mcpwith no credentials. Authorization: Bearer <token>but<token>is invalid or expired — the token didn’t resolve to a context (revoked / TTL expired / never minted by/api/oauth/token).X-WorldMonitor-Key: <key>but<key>isn’t in the valid set — the API key is wrong.- OAuth token resolves but the Pro MCP token row is missing or cross-bound — the
mcpTokenIdno longer maps to the userId. Typically a revoke from Settings → Connected MCP clients. - OAuth token resolves but the subscription went away — entitlement re-check found
tier < 1,mcpAccess !== true, orvalidUntil < now. Includes the entitlements-service-unavailable fail-closed path.
/api/oauth/token with a fresh authorization code, OR refresh-grant with a valid refresh token). For API-key clients, double-check the X-WorldMonitor-Key header — user-issued wm_ keys and operator-issued enterprise keys must go in that header, NOT as Authorization: Bearer. The WWW-Authenticate header’s resource_metadata pointer is the canonical place to start the discovery flow from scratch.
-32029 — Rate limited (per-minute OR daily)
Per-minute and daily-cap triggers share this code; the HTTP status disambiguates.
Per-minute throttle — HTTP 200. Sliding-window limiter at 60 requests / minute keyed per API key (Starter+), per Pro user (combined across all that user’s tokens), or per IP for anonymous public discovery. Credentialed requests are limited after auth. Credential-less public discovery methods (initialize, notifications/initialized, tools/list, resources/list, resources/templates/list) and anonymous public-resource reads are served without auth but still pass through the anonymous discovery limiter. Credential-less data/quota methods, or metadata methods outside that public set, do not use anonymous discovery — they fail closed with -32001 / HTTP 401. Comes back as a JSON-RPC error inside HTTP 200 because the limiter is upstream of any per-id correlation. Fails OPEN on Upstash transient errors — single spikes in limiter-backend latency won’t take the API down.
When the per-minute limiter rejects, the handler emits a durable mcp.rate_limit_hit telemetry event with an allowlisted identity shape. The plan-limit scanner uses that event for sustained-burst notices; it does not infer customer-facing MCP burst notices from raw Upstash limiter internals.
The message text identifies which limiter fired. Three distinct strings:
Example payload (Pro variant — env_key clients get the same envelope shape with the API-key message string):
Retry-After. Hard daily cap (default 50 quota-consuming calls / UTC day) enforced by an atomic Redis reservation BEFORE the tool runs, so the exact call that crosses the boundary rejects. Only tools/call and resources/read of a data-bearing URI-template instantiation (the auth-symmetric resources path) count. wm_… API-key MCP callers do not enter this MCP daily reservation path; they are protected here by the 60 requests/minute/key limiter, while broader REST/API plan allowances are enforced outside the MCP handler. Exempt from the Pro/OAuth daily cap: describe_tool, tools/list, prompts/list, prompts/get, resources/list, resources/templates/list, logging/setLevel, initialize, notifications/initialized, ping, and resources/read of a public (concrete, metadata-only) resource such as worldmonitor://seed-meta/freshness. (These methods still count toward the per-minute limit above — daily and per-minute exemption sets are different.)
Retry-After (the value is seconds-until-UTC-midnight). If the Pro/OAuth daily cap is the binding constraint for batch work, use the API-key or REST/API path where appropriate, or contact Enterprise for a custom MCP limit.
Paid-plan customers also receive account notices and bounded-cadence email when sustained usage crosses a plan threshold. These notices never imply an automatic upgrade, automatic overage charge, or automatic move into API Business; support or checkout action is explicit.
-32600 — Invalid request envelope
Fires when the request body either isn’t valid JSON or is valid JSON without a string method field. Two sites in api/mcp/handler.ts. Strictly a client encoder bug — well-formed JSON-RPC clients will never see this in production.
method and (for any method besides notifications/*) an id field. If you see -32600 from a known-good client library, file an issue against this server — it should never reach you.
-32601 — Method not found
The method field was a string but didn’t match any handler. Methods this server speaks: initialize, notifications/initialized, ping, tools/list, tools/call, prompts/list, prompts/get, resources/list, resources/templates/list, resources/read, logging/setLevel.
capabilities block of your initialize response. Note that resources/subscribe is not implemented (the initialize handshake advertises resources.subscribe: false explicitly) — clients that try it get -32601.
-32602 — Invalid params
The most common error code, shared across tools/call, prompts/get, resources/read, and logging/setLevel. Six concrete triggers:
message — it always tells you what was missing or wrong. For tools, names are in tools/list. For prompts, names + argument schemas are in prompts/list. For resources, the concrete URIs are in resources/list and the parameterised URI templates are in resources/templates/list. For logging/setLevel, valid levels are the RFC 5424 subset listed above.
-32603 — Internal error
Three distinct conditions share this code; the HTTP status disambiguates whether retry is reasonable.
HTTP 200 — tool-execution failure. A tool dispatcher threw. Most commonly: every Redis key the tool reads returned null (cache_all_null — transient Redis blip or a still-warming seeder), or a sibling internal fetch failed mid-call. Pro quota is not rolled back: the tool already executed, so retrying consumes another slot.
Retry-After: 5 — service unavailable. Either the OAuth resolution service threw (Convex transient blip), or MCP_INTERNAL_HMAC_SECRET is unset on the deploy (a misconfig — Pro tool calls cannot sign their downstream fetches without it), or the Pro daily-quota reservation Redis pipeline failed with something other than cap-exceeded.
message text identifies the trigger. Three sites emit a 503; two distinct strings:
Client recovery is identical for all three (honour
Retry-After: 5); the message disambiguates only for log analysis.
HTTP 200 — resources/read payload was empty or unparseable. Defensive guard inside resources/read for the never-should-happen case where the inner tools/call dispatcher returned no content[0].text or non-JSON text.
What to do. For HTTP 200 tool errors: retry once after ~1 second; if a specific tool returns -32603 consistently, check status.worldmonitor.app for the relevant seeder. For HTTP 503: honour Retry-After. For the resources/read defensive case: file an issue — it indicates a dispatcher contract violation upstream of your call.
HTTP statuses
Every status the MCP handler can return. Most JSON-RPC replies — including most errors — are HTTP 200 by convention; the table calls out the cases where the handler escalates.
One HTTP status appears that isn’t a JSON-RPC error:
- 405 with an empty body comes from method-validation BEFORE JSON-RPC. The handler accepts
POST(the JSON-RPC path),GET(the standalone SSE stream /Last-Event-IDreplay channel),HEAD(a 200 ack withContent-Type: application/json, used by uptime probes), andOPTIONS(CORS preflight). A bareGETwith noLast-Event-IDalso returns 405 (no standalone stream is offered). Anything else gets 405 +Allow: POST, GET, HEAD, OPTIONS. The endpoint enforces noOriginallowlist: it advertises wildcard CORS and authenticates by explicitAuthorization/X-WorldMonitor-Keyheader, so browser-origin clients (any origin) are accepted.
Soft-behavior envelopes
Soft envelopes are the high-volume failure mode and the single most common parsing bug for clients that only inspect the JSON-RPC layer. Thetools/call returns HTTP 200 with no error field, the result.content[0].text parses as JSON, and the resulting object has a leading-underscore discriminator key. Always:
- Parse
result.content[0].textas JSON. - Check whether the parsed object has a
_budget_exceededor_jmespath_errorkey at its top level. If yes, treat as an error and do not consume sibling fields as data. - Otherwise, treat the parsed object as the tool’s normal response (cache tools wrap it as
{ cached_at, stale, data }; RPC tools return their declared shape).
_budget_exceeded — response too big for the per-tool budget
Every tool declares a per-tool output budget (_outputBudgetBytes) sized to keep responses inside the typical agent context window. When the serialised response exceeds that budget after all per-tool filters, summary, and JMESPath have been applied, the dispatcher swaps the oversized payload for this envelope — still inside the normal MCP result, still HTTP 200, still no isError:
text payload:
_budget_exceeded: true— discriminator. Always literallytrue; never present on success responses.budget_bytes: number— the per-tool budget the response was checked against.actual_bytes: number— UTF-8 byte length of the serialised response after all narrowing.hint: string— recovery advice. The text varies based on whether the caller already passed ajmespathargument; both phrasings tell you to narrow the result.
country, since, limit), or both. The JMESPath guide has worked examples for projection. The summary: true flag (every cache tool accepts it) returns a server-built counts-and-samples digest that is always under budget.
_jmespath_error — projection failed
Three failure kinds, all returned with the same envelope shape. The _jmespath_error value is a string (not an object); its content is <kind>: <details>. The discriminator is the leading kind token before the first :.
original_keys is the top-level keys of the unprojected response (bounded at 50 entries, with a ...<N more> sentinel when truncated). It is included specifically so the LLM can self-correct on its next tools/call without refetching — the projection failed, but the tool fetch itself succeeded.
Quota. The Pro daily-quota slot is NOT rolled back. The tool fetch succeeded; the user-supplied projection is what failed. A bad expression consumes one quota slot per attempt, which is why original_keys exists — to make the retry self-correcting in one extra call rather than guesswork over N.
The three kinds:
expression_too_long
The JMESPath expression itself exceeds 1024 UTF-8 bytes (JMESPATH_MAX_EXPR_BYTES). The cap is intentionally generous — typical real expressions are 50–200 bytes — and a 1024+ byte expression almost always indicates accidental copy/paste of a full payload into the argument.
invalid_expression
The expression parsed by the JMESPath engine threw — bad syntax, unclosed bracket, unknown function. The details after the kind token is the parser’s error message verbatim.
[?country == "Iraq"]) when JMESPath wants single quotes ([?country == 'Iraq']), and (b) using bare numeric literals ([?deathsBest > 0]) when JMESPath wants backticks ([?deathsBest > \0`]`). The JMESPath guide covers both pitfalls.
projection_too_large
The expression parsed and ran, but the projected output exceeded 256 KB (JMESPATH_MAX_OUTPUT_BYTES) after stringification. Almost always indicates a runaway multiselect-hash or multiselect-list duplicating fields across a large array.
[?...]), or slice the result ([0:N]). Pipe combinators (see example 12 in the JMESPath guide) compose well here.
Other tool-specific envelopes
A handful of tools return their own application-level error envelopes insidecontent[0].text rather than via JSON-RPC -32602. These are documented per-tool in the Tools Reference — the catalog calls them out so a client can recognise the pattern:
describe_toolreturns{ "error": "missing_tool_name", "hint": "..." }or{ "error": "unknown_tool", "requested": "...", "available": [...] }. Quota-exempt — bad input does not consume a quota slot. See Tools Reference →describe_tool.
_budget_exceeded, _jmespath_error) for the catalog-class envelopes and off a top-level error: string for per-tool envelopes.
Roadmap
- Auto-summarize on budget exceed. A future protocol revision may have
_budget_exceededresponses ship a server-built summary inline (one annotated content block in addition to the envelope) for the subset of tools where a summary is well-defined. Deferred until production telemetry justifies the per-tool tradeoff.
See also
- MCP Server overview — endpoints, auth modes, OAuth setup, plans and quotas.
- JMESPath Projection Guide — projection grammar + 12 worked examples; the right place to learn how to fix
_jmespath_errorand recover from_budget_exceeded. - MCP Tools Reference — per-tool parameters, response shapes, and per-tool soft envelopes (e.g.
describe_tool). - MCP Quickstart — five-minute zero-to-first-call onboarding.
- JSON-RPC 2.0 spec — the wire envelope shape the catalog references throughout.
- RFC 9728 — OAuth 2.0 Protected Resource Metadata — what the
WWW-Authenticateresource_metadatapointer means.
