Menu

MCP error codes explained: protocol errors vs tool errors

2026-10-27MCP Checkup

A tool call goes wrong. MCP gives your server two ways to say so, and the two end up in different places. One is a JSON-RPC error: the request failed as a request. The other is an ordinary tool result with isError: true: the request worked, the tool ran, and the tool is reporting a problem. The first may never reach the model. The second is written for it.

This guide lists the codes the spec defines and which revisions define them, shows where each kind of failure belongs, and ends with curl commands you can run against your own server. Replace https://your-server.example.com/mcp with your endpoint.

Two ways to fail

A protocol error is a JSON-RPC error response. There is no result, only error, with an integer code and a message:

{
  "jsonrpc": "2.0",
  "id": 3,
  "error": {
    "code": -32602,
    "message": "Unknown tool: invalid_tool_name"
  }
}

A tool execution error is a successful JSON-RPC response whose result says the tool failed:

{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "resultType": "complete",
    "content": [
      {
        "type": "text",
        "text": "Invalid departure date: must be in the future. Current date is 08/08/2025."
      }
    ],
    "isError": true
  }
}

Both examples come from the spec's tools page. resultType is new in 2026-07-28. Earlier revisions send the same result without it, and clients treat a missing resultType as "complete".

The tools page in 2025-11-25 and 2026-07-28 draws the line like this:

Protocol error Tool execution error (isError: true)
Unknown tool API failures
Malformed request: it doesn't match the CallToolRequest schema Input validation errors, such as a date in the wrong format or a value out of range
Server errors Business logic errors

The spec gives the reason too. Protocol errors "indicate issues with the request structure itself that models are less likely to be able to fix". Tool execution errors "contain actionable feedback that language models can use to self-correct and retry with adjusted parameters".

The JSON-RPC codes

MCP messages follow JSON-RPC 2.0, so its five standard codes apply in every revision. The right-hand column lists the uses the 2026-07-28 schema gives for each:

Code JSON-RPC name When an MCP server sends it
-32700 Parse error The body is not valid JSON
-32600 Invalid Request Valid JSON, but not a valid request object: a missing jsonrpc or method, or the wrong type for either
-32601 Method not found The method doesn't exist, or it belongs to a capability the server didn't advertise, such as prompts/list on a server without prompts
-32602 Invalid params An unknown tool or prompt name, an invalid or expired pagination cursor, an invalid log level; from 2026-07-28, also a resource that doesn't exist
-32603 Internal error Something broke inside the server

When the server can't read the request's id at all, as with a parse error, the reply can't echo it. JSON-RPC 2.0 sets the id to null in that case; the MCP schema makes id optional on error responses, so you may also see it left out.

On Streamable HTTP under 2026-07-28, two of these come with a fixed HTTP status. A method the server doesn't implement gets 404 Not Found with -32601. A request missing a required _meta field (io.modelcontextprotocol/protocolVersion or io.modelcontextprotocol/clientCapabilities) gets 400 Bad Request with -32602. The Streamable HTTP revisions before it (2025-03-26 to 2025-11-25) name no HTTP status for JSON-RPC errors, and in them a 404 means something else: the session ID is no longer valid.

The codes MCP defines itself

JSON-RPC reserves -32000 to -32099 for implementation-defined server errors. Revision 2026-07-28 splits that range in two:

  • -32000 to -32019 is legacy. Implementations used these codes before the policy existed. New implementations should not use them, and a receiver must not read a fixed meaning into them, with one exception, -32002, below.
  • -32020 to -32099 belongs to the spec. An implementation must not send a code from this range unless the spec defines it, and must use defined codes only for their defined meaning.

Three codes are defined there so far:

Code Name Sent when HTTP status
-32020 HeaderMismatch A required header (MCP-Protocol-Version, Mcp-Method, Mcp-Name) is missing or malformed, or disagrees with the body 400
-32021 MissingRequiredClientCapability Handling the request needs a client capability the request didn't declare; data.requiredCapabilities lists what is missing 400
-32022 UnsupportedProtocolVersion The server doesn't implement the requested version; data.supported lists the versions it does 400

Two older codes are retired, and neither will be reused:

  • -32002, resource not found, used up to 2025-11-25. Under 2026-07-28 a missing resource is -32602, and servers on that revision must not send -32002. Clients should still accept it from servers on earlier revisions.
  • -32042, URL elicitation required, existed only in 2025-11-25.

If your server needs codes of its own, the spec says to pick them outside the JSON-RPC reserved range of -32768 to -32000.

One version note. In the revisions with an initialize handshake (2025-11-25 and earlier), the spec's example for an unsupported protocol version is -32602 with data.supported and data.requested. Under 2026-07-28, the same situation is -32022.

Unknown tool: a protocol error in every revision

Revisions 2025-06-18, 2025-11-25 and 2026-07-28 all put "unknown tool" on the protocol side, and all three use the same example: -32602 with the message Unknown tool: <name>.

So when a client calls a tool you don't have, don't answer with:

  • an isError: true result, which tells the model the tool exists and ran;
  • a result without isError, which tells it the call worked;
  • an HTML error page or an empty body, which a client can't parse at all.

An unknown tool is also not an unknown method. tools/call is a method your server knows; the tool name is one of its parameters. That is why the code is -32602, not -32601, and why the 2026-07-28 rule that pairs -32601 with 404 doesn't cover it.

We call a made-up tool name on every server in our directory. In the latest check of each server (the week to October 26, 2026), 3,265 servers answered the call itself instead of asking for credentials with a 401. Of those, 1,390 (43%) answered with an isError: true result, 28 with an ordinary result that reports no error at all, and 19 with a reply that isn't valid JSON-RPC. The other 1,828 answered with a JSON-RPC error or refused the call with a 403; our records don't tell those two apart.

Bad arguments: where the revisions differ

This is the one place the spec has moved.

  • 2025-06-18 lists "invalid arguments" under protocol errors and "invalid input data" under tool execution errors, without saying where one ends and the other begins.
  • 2025-11-25 and 2026-07-28 make it explicit: input validation errors, such as a date in the wrong format or a value out of range, are tool execution errors. Protocol errors are for requests that don't match the CallToolRequest schema at all, for example a call with no name.

The 2026-07-28 schema file still lists "invalid tool arguments" among the uses of -32602, with an example message about a missing required property. So the spec's own material isn't fully consistent on this point.

A rule that fits the tools page: if the model could fix the problem by changing an argument, return isError: true and say what to change. Keep -32602 for calls that name a tool you don't have, or that aren't shaped like a tool call.

What the model sees

The client sits between your server and the model, and since 2025-11-25 the spec tells it what to forward:

  • Clients should give tool execution errors to the model, so it can correct itself.
  • Clients may give protocol errors to the model, "though these are less likely to result in successful recovery".

The schema has said much the same since 2025-06-18, in its note on isError: errors that originate from the tool should be reported inside the result with isError set, because "Otherwise, the LLM would not be able to see that an error occurred and self-correct."

So an isError result lands in the model's context as the tool's output, the way a success would. What happens to a protocol error depends on the client: it may be handed to the model, shown to the user, or end the step. Even when it does reach the model, it arrives as a failed request, not as the tool's answer.

Two consequences:

  1. Answer a fixable problem (a bad date, an unknown ID) with a protocol error, and you may take away the model's only chance to fix it.
  2. Answer an unfixable one (unknown tool, broken server) with isError: true, and the model may keep retrying something that can't work.

Writing an isError result the model can use

The text in content is the whole message, so make it specific:

  • Say what was wrong and what would be valid. "start_date must be today or later; today is 2026-10-27" gives the model something to do. "Invalid input" does not.
  • Use the argument names from your inputSchema, so the model can map the message to the call it made.
  • Say whether a retry can help. "Rate limited, retry in 30 seconds" and "This account can't access that project" call for different next steps.
  • Leave out stack traces, internal hostnames and file paths. They don't help the model, and they end up in a conversation a person reads.
  • Set isError: true explicitly. A failure described in text, with isError absent or false, reads as a success to any client that checks the flag.

The 2026-07-28 tools page applies the same idea to tools that keep state between calls: a call with an expired or unknown handle "should return a tool execution error that says so, so the model can recover by creating a new one".

Check it with curl

These commands use the 2026-07-28 headers. For a server on an earlier revision, run initialize first, send your session headers instead, and drop the _meta block. The JSON-RPC codes these commands look for are the same in 2025-06-18, 2025-11-25 and 2026-07-28; only the 404 for an unknown method is new in 2026-07-28.

Set up a small helper:

MCP_URL=https://your-server.example.com/mcp
META='"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"curl-test","version":"0.1"},"io.modelcontextprotocol/clientCapabilities":{}}'
call() {  # call METHOD PARAMS [more curl options]
  m=$1 p=$2; shift 2
  curl -sS "$MCP_URL" -w '\nHTTP %{http_code}\n' \
    -H 'Content-Type: application/json' \
    -H 'Accept: application/json, text/event-stream' \
    -H 'MCP-Protocol-Version: 2026-07-28' \
    -H "Mcp-Method: $m" "$@" \
    -d '{"jsonrpc":"2.0","id":1,"method":"'"$m"'","params":'"$p"'}'
}

An unknown method. Expect -32601, and under 2026-07-28 an HTTP 404:

call no/such_method '{'"$META"'}'

An unknown tool. Expect a JSON-RPC error with -32602. Not a result, not HTML:

call tools/call '{"name":"definitely_not_a_tool","arguments":{},'"$META"'}' \
  -H 'Mcp-Name: definitely_not_a_tool'

A wrong argument. Replace get_weather and the argument with one of your tools and a value it should refuse. Expect a result with "isError": true and a sentence that names the argument:

call tools/call '{"name":"get_weather","arguments":{"date":"not-a-date"},'"$META"'}' \
  -H 'Mcp-Name: get_weather'

Invalid JSON. Expect -32700, with "id": null or no id:

curl -sS "$MCP_URL" -w '\nHTTP %{http_code}\n' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2026-07-28' \
  -H 'Mcp-Method: tools/list' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"'

If a reply comes back as text/event-stream, the JSON is on the data: lines. In every case, an HTML page, an empty body or a dropped connection gives the client nothing to parse: whatever went wrong, the JSON-RPC error that would have explained it never arrived.

Checklist

Situation What the server should send
Tool name you don't have JSON-RPC error -32602, not isError, not HTML
Method you don't implement JSON-RPC error -32601; under 2026-07-28 with HTTP 404
Body that isn't JSON JSON-RPC error -32700, id null or absent
Argument value the model could fix Result with isError: true, naming the argument and what is valid
Failure inside the tool (upstream down, rate limit) Result with isError: true, saying whether a retry can help
Missing resource -32602 under 2026-07-28; -32002 only on earlier revisions
Unsupported version, 2026-07-28 -32022 with data.supported, HTTP 400
Header disagrees with body, 2026-07-28 -32020, HTTP 400
Your own error codes Outside -32768 to -32000; never an undefined code in -32020 to -32099
Any error text No stack traces, internal hostnames or file paths

To see how your own server answers a call to a tool that doesn't exist, paste your address at mcpcheckup.com. It connects from outside, the way an agent does, and tells you what kind of reply came back.