In MCP, when does a tools/call use isError instead of a JSON-RPC error?
answer
- Two layers: protocol versus execution
- Ask: did the tool actually run?
- One audience is the client, one the model
- Unknown tool name is -32602
- isError travels inside a successful result
basics
~20 sFailures of the tool's own work — an upstream 500, a missing file, a rejected input value — come back as a successful result with isError set to true, so the model can read them. Protocol-level failures such as an unknown tool or invalid params are JSON-RPC error objects.
solid answer
~50 sMCP splits tool failures into two layers. A **protocol error** means the request itself was wrong or unservable: unknown tool name, arguments that violate `inputSchema`, an unsupported protocol version. Those return a JSON-RPC error object — for example `-32602` invalid params — and never reach the model as tool output. A **tool execution error** means the call was well-formed and the tool ran, but its work failed: the API it wraps returned 500, the query timed out, the path did not exist. Those return a normal successful `CallToolResult` with `isError: true` and the failure described in the `content` blocks. The distinction is deliberate: an execution error is information the model needs so it can retry, choose different arguments, or tell the user, whereas a JSON-RPC error is usually swallowed by the client as a broken call. This split is unchanged in MCP revision 2026-07-28, where the result additionally carries `resultType: "complete"`.
code
json · 12 lines{
"jsonrpc": "2.0",
"id": 12,
"result": {
"resultType": "complete",
"isError": true,
"content": [
{ "type": "text",
"text": "Repository 'acme/widgts' not found. Check the spelling of the repo name." }
]
}
}go deeper
Remember there are two failure paths: a malformed or unknown call gets a JSON-RPC error, while a tool that ran and failed returns a normal result with isError set to true.
Explain the audience argument — JSON-RPC errors go to the client, isError content goes into the model's context — and name -32602 as the code for an unknown tool or bad arguments.
Judge real cases: keep secrets and stack traces out of error content, route authorization failures to 403 insufficient_scope, and make error text actionable enough that the model corrects itself instead of looping.
Own the failure taxonomy across a server fleet: consistent error wording, what is observable to the model versus only to operators, and how retry-visible errors interact with agent loop cost and runaway retries.
## Two layers of failure When a `tools/call` goes wrong, MCP asks a single routing question: *did the tool actually run?* - **No — the request was never servable.** That is a protocol error, expressed as a JSON-RPC error object with a `code`, `message` and optional `data`. Examples: the `name` does not match any tool the server exposes; `arguments` violate the declared `inputSchema`; the method is not implemented at all. - **Yes — the tool ran and failed at its job.** That is a tool execution error, expressed as a perfectly ordinary successful JSON-RPC *result* whose `isError` field is `true`, with the human- and model-readable explanation in the `content` blocks. ## Why execution errors are not JSON-RPC errors The reason is who the audience is. A JSON-RPC error is addressed to the *client*: the client library typically raises it as an exception, and the calling code decides what to do. The model never sees the text, so it never learns that its call failed or why. A tool execution error is addressed to the *model*. "Repository not found — check the owner/name spelling" is exactly the feedback that lets a model retry with corrected arguments rather than stalling or hallucinating success. Because the result is a success at the JSON-RPC layer, the client naturally hands the content back into the conversation as the tool's output, `isError` flagging it as a failure so the host can also style it differently in the UI or count it for metrics. So the rule of thumb: **if you want the model to read it, it belongs in the result with `isError: true`.** ## Common protocol-error codes - `-32601` — method not found; the server does not implement the JSON-RPC method at all. Over Streamable HTTP this is paired with HTTP 404. - `-32602` — invalid params; the canonical answer for an unknown tool name or arguments that fail `inputSchema` validation. In revision 2026-07-28 it is also the code for resource-not-found on `resources/read` (`-32002` is only accepted from older servers). - `-32603` — internal error; the server broke in a way unrelated to the tool's own semantics. - The band `-32020` to `-32099` is reserved by the MCP specification for its own codes, so do not mint private codes in that range. ## What a good execution-error result looks like Set `isError: true` and put an actionable message in a text content block. Say what failed, and where possible what the model could do differently. Do **not**: - leak raw stack traces, internal hostnames, connection strings or credentials — the content goes straight into the model's context and often into a transcript; - return `isError: true` with an empty `content` array, which tells the model nothing; - return a *successful* result with no `isError` flag but an error message inside the text, which trains the model to treat failures as data and makes host-side metrics impossible; - use `isError` for authorization failures on a *remote* server. Lack of permission is answered at the HTTP/OAuth layer — 401/403, with `403` plus `error="insufficient_scope"` for a step-up — not as tool output. ## Structured results and isError If the tool declares an `outputSchema`, the success path must return `structuredContent` conforming to it. On the error path you generally cannot produce a conforming structured result — so return `isError: true` with content blocks and omit the structured payload rather than inventing a value that satisfies the schema but means "failure". ## Where it fits in revision 2026-07-28 Every result in MCP 2026-07-28 carries a required `resultType`. An ordinary result — including an `isError: true` one — is `resultType: "complete"`; clients must treat a missing `resultType` from an older server as `"complete"`. A tool that needs more input from the client instead returns the interim `"input_required"` result type, which is neither a success nor an error but a request to come back again. Nothing about the isError/JSON-RPC split changed in this revision; what changed around it is that the request carries its protocol version and capabilities in `_meta`, so a version mismatch is itself a protocol error (`-32022`, `UnsupportedProtocolVersionError`) rather than something discovered during a handshake. ## How it gets asked Interviewers usually give you a scenario — "your tool wraps a REST API and it returns 404" — and listen for whether you route it to the model or to the client. The strong answer names both layers, gives the concrete code for the protocol side, and explains the audience argument rather than reciting the rule.
- What should a remote MCP server return when the caller's token lacks the scope a tool needs?Not `isError`. Authorization is handled at the HTTP/OAuth layer: the server answers `403` with `error="insufficient_scope"` so the client can run a step-up authorization flow and retry. Reporting it as tool output would hand the model a permissions message it cannot act on and would bypass the client's ability to re-authorize.
- Is it ever right to return isError: true with an empty content array?No. `isError` is a flag, not a message; with no content the model learns only that something failed and has nothing to adapt to, so it typically retries the identical call. Always include at least one text block naming what failed and, if you can, what to change. Keep it free of stack traces and internal identifiers.
- How does isError interact with a tool that declares an outputSchema?The schema constrains the success path: when the tool succeeds it must return `structuredContent` conforming to `outputSchema`. On failure, return `isError: true` with explanatory content blocks and no structured payload rather than fabricating a value that satisfies the schema, which would make a failure indistinguishable from a result to any consumer validating against the schema.
saying these in an interview costs you the question
- Returning a JSON-RPC error for an upstream 500 the tool handled
- Thinking isError means the JSON-RPC call itself failed
- Using isError for missing OAuth scope instead of 403 insufficient_scope
- Dumping stack traces into the error content the model reads
- Returning a plain successful result with the error text and no isError flag