Skip to content
unzoi docs
Search and navigation
Start here
REST API
MCP
Limits and plans
Agent clients
SDKs
Guides

MCP errors

Two kinds of failure, and getting them the wrong way round is how an agent integration quietly dies.

The distinction

An MCP client that receives an HTTP error status for a tool call concludes the server has failed. It marks the connection unhealthy, and often stops calling the tools at all. That is the right reading for some failures and badly wrong for others.

Reported asMeansWhat a client should do
An HTTP status The request could not be accepted at all. Fix the connection, or give up on it.
A JSON-RPC error The message was unroutable — unknown tool, bad argument. Fix the call. The session is fine.
A tool error (isError: true) The tool ran and could not do the thing. Show the model the message. The session is fine.

What goes where

ConditionReported asWhy
Missing or invalid key 401 A client with no usable credential cannot open a session; saying otherwise inside a JSON-RPC envelope would mean pretending one exists.
Too many messages this minute 429 + retry-after, message_rate_limited Every HTTP message takes a message token, the handshake included. Transient: wait and send the same message again.
This minute's credits spent Tool error, credit_rate_limited The tool call was priced and refused before it ran. Transient: retry the call after retry_after_secs.
Unknown tool JSON-RPC error -32601 The request is unroutable, which is a protocol-level fact.
An argument the tool does not declare JSON-RPC error -32602, error.data.code: unknown_parameter Refused with a hint — Unknown parameter 'langauge'. Did you mean 'language'? — rather than ignored. The call never ran and costs no credits.
An unknown signal name, or a value outside its enum or range JSON-RPC error -32602, error.data.code: unknown_signal / invalid_parameter Same. A signal name that is not published used to match nothing, which looked like no coverage.
Account suspended Tool error The caller authenticated fine. They simply cannot spend right now — a bill, not an outage.
Not enough credits left this month, on a key that stops there Tool error, insufficient_credits Same. The refusal carries the call's price and the credits left.
Over the call's max_credits Tool error, credit_budget_exceeded The call was well-formed; the budget refused it before it ran, at no cost.
No shard answered Tool error Reported rather than returned as an empty result set: an agent that reads "no matches" when the fleet is degraded concludes the story does not exist.
Over the plan's watch cap Tool error, watch_limit The call was well-formed. The account has no room for another watch until one is deleted.
A watch tool where watches are not served Tool error, unsupported The local stdio server lists the watch tools but cannot keep a watch. The same call works on the hosted endpoint.

The shape of a protocol error

{
  "jsonrpc": "2.0", "id": 2,
  "error": {
    "code": -32602,
    "message": "Unknown parameter 'langauge'. Did you mean 'language'?",
    "data": { "code": "unknown_parameter" }
  }
}

error.data.code is one of unknown_parameter, unknown_signal, invalid_parameter. These are JSON-RPC errors, not isError results: the tool did not run, so there is no output to show the model, and the fix is to the call. Every tool's inputSchema says additionalProperties: false, so a schema-aware host can refuse the call before it leaves the process.

The shape of a tool error

{
  "jsonrpc": "2.0", "id": 3,
  "result": {
    "isError": true,
    "content": [{
      "type": "text",
      "text": "{\"error\":\"insufficient credits\",\"code\":\"insufficient_credits\",\"detail\":\"this call can cost up to 3 credits and 0 are left this month; ...\",\"retry_after_secs\":394200,\"credits\":{\"min\":3,\"max\":3},\"credits_limit\":1000,\"credits_used\":1000,\"credits_remaining\":0}"
    }]
  }
}

The content block is JSON, so an agent can act on it rather than pattern-matching prose.

codeMeansRetry?
suspended Subscription canceled or payment failed. Not until the account is settled.
insufficient_credits The key stops at its monthly allocation, and this call's worst case costs more than the credits left. After retry_after_secs — which may be days. A cheaper call may still fit; or upgrade.
credit_rate_limited This minute's credits are spent. Yes, after retry_after_secs — usually a second or two.
credit_budget_exceeded The call's worst case costs more than the max_credits argument. No. Raise max_credits or narrow the call; estimate: true shows the breakdown.
upstream_unavailable No index partition covering the request answered. Not an empty result. Yes, with backoff; narrow the time range.
not_found get_article, find_related or get_story on an id the index does not hold; get_watch, delete_watch or watch_events on a watch this account does not have. No. This one is a statement of absence, and safe to cache.
watch_limit create_watch on an account that already keeps as many watches as its plan allows. No. Delete one with delete_watch, or change plan.
unsupported A watch tool on a server with no watch store, such as the local stdio server, or on a session with no API key. Watches are served by the hosted endpoint. No. Make the call on the hosted endpoint.

retry_after_secs and the credit figures are the same numbers the REST headers carry, so a client that understands one surface understands the other. See Credits and overage. A successful result carries credits_charged in structuredContent.

The same codes on REST

The code vocabulary is shared. Over REST every 4xx and 5xx — including 401, 402 and 429, which used to be plain text — is the JSON Error body with the same code, and a 400 for an unknown parameter carries the same did you mean hint. A client that switches on code handles both surfaces with one table.

Successful results

A success carries the payload as structuredContent — the typed JSON object, exactly the body the equivalent REST endpoint returns, in the shape the tool's outputSchema declares — and as a single text content block holding the same JSON, for clients that predate structuredContent. Read structuredContent if your client exposes it; otherwise parse the text once. Never both: if you find yourself with two copies, or parsing twice, you are looking at a double-encoded envelope, which is a client bug (or a different server).

{
  "jsonrpc": "2.0", "id": 1,
  "result": {
    "structuredContent": {
      "total": 6, "total_relation": "exact", "has_more": false,
      "partial": false, "coverage": "complete", "from_clamped": false,
      "stories": [...]
    },
    "content": [{ "type": "text", "text": "{\"total\":6,\"total_relation\":\"exact\",...}" }],
    "isError": false
  }
}

A result that is not an error can still be incomplete. partial, coverage and from_clamped are in structuredContent on every search-shaped tool, and Reading completeness is the rule for them: an empty result under partial: true is not evidence that nothing was written.