Complete the correct MCP error response shape for a tool logic failure
A tool logic failure goes in the JSON-RPC result with isError: true, so the model sees it. Only protocol faults like unknown methods use the error field.
Imagine ordering food through a waiter. If the kitchen burns your dish, the waiter still comes back to your table and tells you 'sorry, the dish failed'. Then you can decide what to do next. That is a tool error: the request was understood, but the work failed. The result comes back to you with a 'this went wrong' note attached. Now imagine you mumble an order for a dish that does not exist on the menu. The waiter cannot even take the order. That is a protocol error: the request itself was malformed. MCP keeps these two cases on separate channels. The model gets the kitchen failures it can react to. The broken requests come back a different way.
Concept explanation~2 min read
Everything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example. Click to expand.
Concept explanation~2 min read
Everything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example. Click to expand.
Tool errors sit at the boundary where the Model Context Protocol meets its JSON-RPC transport. Getting that boundary wrong is one of the most common production MCP bugs. The instinct from years of API design is that an error is an error, so it should go in the error field. MCP deliberately breaks that instinct.
The core idea is that there are two different kinds of failure, with two different audiences. A tool that ran and failed produces information the model must see and act on. A request that was never executable produces a fault the host must absorb. MCP routes these to two separate channels so each audience gets what it needs. The boolean that distinguishes them, isError, is small, but the whole design rests on it.
Two failure modes, two channels
Start with the transport. MCP messages are JSON-RPC 2.0. Every response is either a success carrying a result object, or a failure carrying an error object with a numeric code and a message. That envelope is fixed by JSON-RPC and predates MCP entirely. The specification reserves a band of codes for transport faults, and application code is expected to stay out of that band.
MCP then layers tool semantics on top. A tools/call request invokes a named tool with arguments. Two completely different things can go wrong, and they map to the two envelopes. Both can follow a single tools/call, so each failure has to go to the right channel.
The first mode is a protocol error. The request was malformed, the method name is unknown, or the named tool does not exist. The call never reached the tool. These map cleanly to the JSON-RPC error field, with standard codes such as method-not-found or invalid-params. The defining trait is that no business logic executed. The fault is in the request itself.
The second mode is a tool execution error. The method exists, the arguments parsed, the tool ran, and its business logic failed: a query errored, a file was missing, an upstream API returned 500. The tool executed and failed. Argument values that fail the tool's inputSchema also come back this way, so the model can correct them. This does not use the error field at all. The request was well-formed and fully understood. Only the downstream work failed, and that distinction dictates the response shape.
Situations where this technique stops working.
2–4 min · Everything important, quickly.
// Tool logic failure -> result with isError, NOT a thrown JSON-RPC error
server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const rows = await runQuery(request.params.arguments.sql);
return { content: [{ type: "text", text: JSON.stringify(rows) }] };
} catch (err) {
return {
content: [{ type: "text", text: `Query failed: ${err.message}` }],
isError: true // model sees this and can react
};
}
});| Aspect | Tool execution error | JSON-RPC protocol error |
|---|---|---|
| Where it lives | result, with isError true | error field, with a code |
| What failed | Tool logic ran and threw | Request never executed |
| Examples | DB timeout, missing file, API 500 | Unknown method or tool, malformed JSON |
| Who reacts | The model reads and retries | The host catches and logs |
| Visible to model | Yes, in the content array | Usually not; the host handles it |
Real products, models, and research that use this idea.
- The official MCP filesystem server returns isError true with a message when a read targets a missing path, rather than throwing a JSON-RPC error.
- Claude Code surfaces an isError tool result back into the model's context so Claude Opus 4.7 can retry a failed shell command on the next turn.
What an interviewer would ask next. Try answering before peeking at the approach.
QHow should a client distinguish a tool error from a protocol error when both can appear after a tools/call?
Check the response envelope first: an error field means a protocol fault; a result with isError true means the tool ran and failed. Branch the handler on which envelope arrived.
Red flags & common mistakes
The phrases that signal junior thinking. Click to expand.
Red flags & common mistakes
The phrases that signal junior thinking. Click to expand.
Returning a tool failure as a JSON-RPC error. That hides it from the model, which can no longer see the message or self-correct on the next turn.
60 second bullets to scan on the way to the call.
The two error channels MCP exposes and how they differ
Which channel a tool business logic failure uses
Primary sources. Browse if you want the original framing.
Same topic, related formats. Practice these next.