Connect by JBRH Open Connect

Designing a resilient client

Only two failures here are worth retrying: a 429 and a genuine transport fault. Everything else — 400, 403, 404, 405, 413, 422 and every JSON-RPC error code — is an answer, and repeating the request repeats the answer. A tool that fails is different again: it returns 200 with isError set, not an HTTP error.

Status
Available What this means
Audience
developer
Last verified
Product version
6.3.2

The failures, sorted#

ResponseWhereMeaningRetry?
429 + {"error":{"code":"rate_limited"}}/api/public/*More than 240 requests in the last minute from your addressYes, after waiting
429, JSON-RPC -32603/mcpMore than 120 calls in the last minuteYes, after waiting
404 + {"error":{"code":"not_found"}}/api/public/*No page or manifest by that referenceNo — fix the reference
422/api/public/*A parameter outside its documented range; q under two characters, limit above 25No
403, JSON-RPC -32600/mcpAn Origin header was present and is not allowedNo
405 with Allow: POST/mcpA GET or DELETE. This revision has no GET stream and no sessionsNo
413/mcpBody over 256 KiBNo
400, JSON-RPC -32700/mcpThe body is not valid JSONNo
400, JSON-RPC -32020/mcpA header disagrees with the body — version, method or nameNo
404, JSON-RPC -32601/mcpUnknown methodNo
200 with isError: true/mcp tool resultsThe tool ran and could not answer — an empty query, an unknown pageNo
Connection reset, timeout, 5xxAnywhereTransport or server faultYes

Sorting failures this way is the whole of resilience. A client that retries a 404 turns one wrong reference into a hundred, and a client that does not retry a reset turns a two-second network hiccup into a user-visible error.

Backing off without a Retry-After#

No Retry-After header is documented on either surface, so the schedule is yours to choose. Two facts constrain it usefully: the limiter counts requests in a rolling sixty-second window, and it is measured on a monotonic clock, so the window is honest even across a clock adjustment.

  1. On a 429, wait before the next attempt. A rolling window drains as your oldest requests age out, so a short wait may be enough — but retrying immediately guarantees another 429 and adds to the count you are trying to get under.
  2. Use exponential backoff with full jitter: sleep = random(0, min(cap, base × 2^attempt)). Jitter matters more than the base here, because clients that back off in lockstep re-collide at every step.
  3. Cap total attempts, not just the delay. Three or four is right for a read that a person is waiting on.
  4. Treat a repeated 429 after several attempts as a signal to slow the whole client down, not to retry harder — the limit exists to stop a loop.

Every public route is a GET and every public MCP tool is annotated idempotentHint: true, so a retry can never duplicate anything. That is unusually comfortable, and it is a property of a read-only surface rather than a promise that would extend to a write.

Two errors that look like faults and are not#

-32020, a header mismatch
The MCP-Protocol-Version, Mcp-Method or Mcp-Name header did not match the request body. It exists so that a load balancer routing on a header and a server executing on the body cannot see two different requests. Fix the client — usually a stale header on a reused connection object — rather than retrying.
405 on a GET to /mcp
Not a misconfiguration. Protocol revision 2026-07-28 removed the standalone GET stream and protocol-level sessions, so a client that opens one is told plainly instead of waiting on a stream that will never carry anything. The response names the revision and sets Allow: POST.

A third case is worth naming because it is silent: sending Mcp-Session-Id or Last-Event-ID. Both are ignored, not rejected. If your client depends on a session it thinks it has, nothing will tell you — the requests simply behave as independent calls, which they are.

What to log when something fails#

Log the route or JSON-RPC method, the numeric code, error.code when there is one, and the request id you sent. That set is enough for anyone to reconstruct the call. Do not log the full response body of a documentation fetch — it can be sixty thousand characters of Markdown, and none of it helps.

Never log a credential, and note the trap: an integration key does not belong on these routes at all. A key sent to a documentation endpoint authenticates nothing and travels into every log and proxy along the way.

If the failure survives a careful retry, getting help as a developer lists what a report needs to be actionable on the first reply.

Questions#

Is a JSON-RPC error also an HTTP error?

Sometimes, and the pairing is deliberate. Transport-level problems carry a matching HTTP status — 400, 403, 405, 413, 429. An unknown tool, resource or prompt answers 200 with a JSON-RPC error, because the transport worked and the request was understood.

How do I tell a tool failure from a transport failure?

A tool failure is a successful response whose result carries isError: true and a text explanation. A transport failure has no result at all. Check for result first, then for isError inside it.

Should I retry a 403 from the MCP endpoint after removing my Origin header?

That is a fix rather than a retry, and yes, it works: a request with no Origin is accepted, because a non-browser client sends none. Only a present, invalid one is refused.