MCP has two error channels and they are not interchangeable. Which one you use decides whether the model gets to try again.
| Protocol error | Tool execution error | |
|---|---|---|
| Shape | JSON-RPC error object | JSON-RPC result with isError: true |
| Means | The request was wrong | The request was fine, the work failed |
| Examples from the spec | Unknown tool, malformed request, server error | API failures, input validation errors, business logic errors |
| What the client does | MAY pass it to the model | SHOULD pass it to the model |
The specification's wording, revision 2026-07-28: protocol errors "indicate issues with the request structure itself that models are less likely to be able to fix", while tool execution errors "contain actionable feedback that language models can use to self-correct and retry with adjusted parameters".
A protocol error:
{
"jsonrpc": "2.0",
"id": 3,
"error": { "code": -32602, "message": "Unknown tool: invalid_tool_name" }
}
A tool execution error:
{
"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
}
}
Note what is inside the second one. The message names the field, states the rule and gives the value needed to satisfy it. That is what makes a retry possible on the next turn instead of a dead end.
Server code that validates arguments and throws produces a protocol error, because most SDK
wrappers turn a thrown exception at the transport layer into -32602. So a bad date, a
value out of range or an expired identifier arrives at the client as "the request was malformed".
Clients are only told they MAY show that to the model. When they do not, the model sees a failed call
with no reason and either repeats it or gives up.
The same content returned as isError: true is something clients SHOULD show. The fix
is usually to catch inside the tool handler and return a result rather than to let the throw
escape.
CallToolRequest schema, which is the case the spec names.An execution error is read by a model deciding what to do next, so write it for that reader. Name the parameter. State the constraint. Give the current value if it helps. Say whether retrying unchanged could work. "Invalid input" fails all four and is the most common thing servers return.
Source: the tools section of the MCP specification, revision 2026-07-28, at
modelcontextprotocol.io, fetched 2026-09-09. Both example payloads above are the specification's own.
The resultType field shown in the result is part of the same revision.
Yes. isError sits on the result alongside content and structuredContent, so an error result can still carry a machine-readable payload. If your outputSchema describes a success shape, be careful that an error result does not claim to conform to it.
The standard ones. The specification's example uses -32602, invalid params, for an unknown tool. Inventing codes in the reserved range is not useful because clients key on the presence of an error object, not on its code.
Yes, and on stdio the place for it is stderr, never stdout. Stdout carries MCP messages only, and anything else written there corrupts the stream. Clients typically capture stderr to a log file.
Call the tool with a deliberately bad argument and look at the top-level keys of the response. If you see "error", it is a protocol error. If you see "result" with isError true, it is an execution error. Anything that throws before your handler runs will be the former.
Only if you put it there. A thrown exception usually becomes a generic protocol error, and stack traces should not be sent to a model in any case. Catch it, decide what the model needs to know, and return that.
All MCP servers and prices · All guides · Buy the bundle $39