Complete the correct MCP error response shape for a tool logic failure
A tool logic failure rides 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' so you can decide what to do next. That is a tool error: the request was understood, but the work failed, and 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 tracks, so the model gets handed the kitchen failures it can react to, while the truly broken requests bounce back differently.
Detailed answer & concept explanation~7 min readEverything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example. Click to expand.
Everything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example. Click to expand.
Everything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example.
Everything important, quickly.
5 min: the two error channels + why tool errors ride the result with isError + which faults use the error field + the production exception-handling trap + the injection risk of error content.
// 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, bad params, malformed JSON |
| Who reacts | The model reads and retries | The host catches and logs |
| Visible to model | Yes, in the content array | No, intercepted by the host |
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.
- A Postgres MCP server catches a SQL syntax error inside tools/call and packs it into result content so the model can rewrite the query.
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?
QWhat belongs in the content array of an isError result, and what should you deliberately omit?
Don't say thisRed flags and common mistakes that signal junior thinking. Click to expand.
Red flags and common mistakes 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.
The night-before-the-interview bullets. Scan these on the way to the call.
Primary sources. Skim if you want the original framing.
Same topic, related formats. Practice these next.