MCP 2026-07-28 one lesson per page
23 lessons
Common source of bugsInteractions & Execution · 26%Track 1 · 3 / 9

A tool call end to end, and the two kinds of failure

A tool can fail in two very different ways. Get the difference right and the model can fix its own mistakes; get it wrong and it just gives up.

Listen to this lessonAudio overview in Gemini Notebook · about 15–25 min · opens in a new tab

The flight booked for yesterday

A user says "book me on the 8:40 to Denver tomorrow". The model, confused about time zones, calls book_flight with yesterday's date.

Version A: the server throws, and the client gets JSON-RPC error -32603 Internal error. The model sees "the tool failed", apologises, and stops. The user has to start again.

Version B: the server returns a normal result with isError: true and the text "Invalid departure date: must be in the future. Current date is 2026-10-03." The model reads it, realises its mistake, retries with tomorrow's date, and the booking goes through. The user never notices.

Same bug, same server. The only difference is which kind of error came back.

  1. Discover: tools/list. The list MUST NOT vary per connection or as a side effect of other requests; it MAY vary by the caller's authorization. Order SHOULD be deterministic.
  2. Choose: the model picks a tool. There SHOULD always be a human in the loop who can deny it; hosts SHOULD show the inputs and ask for confirmation on sensitive operations.
  3. Call: tools/call with name and arguments. The result may be input_required (MRTR) before it's complete.
  4. Result: content blocks (text, image, audio, resource_link, embedded resource), optional structuredContent (any JSON value), and isError.

Two kinds of failure: protocol errors (unknown tool, malformed request, server error) are JSON-RPC error responses. Tool execution errors (API failures, invalid input, business logic) are results with isError: true. Clients SHOULD give tool execution errors to the model to enable self-correction.

Structured output: if a tool declares an outputSchema, the server MUST return structuredContent that conforms, and clients SHOULD validate it. For older clients, the server SHOULD also include the JSON as a text block.

Names: 1–128 characters, case-sensitive, using only letters, digits, _, - and .; no spaces or commas. Hosts that combine several servers SHOULD disambiguate duplicate names (for example with a server prefix).

A model can only fix what it can read. Protocol errors mean "the call itself was broken", which only a developer can fix. Execution errors mean "the call worked, the answer is no, and here's why", which the model can act on. Structured output turns a tool from "text the model parses" into "data the host can trust and render".

Two ways to fail
Compare

Both calls fail. Before you switch, predict: which one lets the model recover on its own?

Host + LLMServertools/call book_fligth (typo)error -32602 "Unknown tool"A developer bug: nothing the model can fix
Solid arrows are requests, dashed are responses or notifications. Red ✕ is the unsafe or failing step; green is the step that keeps you safe.

The same failure, reported two ways

Errorprotocol error (for broken calls)
{  "jsonrpc": "2.0",  "id": 3,  "error": {    "code": -32602,    "message": "Unknown tool: invalid_tool_name"  }}
Safetool execution error (the model can act on it)
{  "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 verbatim from the spec. Rule of thumb: if the model could do something about it, it's isError.

Structured output that matches its schema

Examplethe tool's outputSchema
"outputSchema": {  "type": "object",  "properties": {    "temperature": { "type": "number", "description": "Temperature in celsius" },    "conditions": { "type": "string", "description": "Weather conditions description" },    "humidity": { "type": "number", "description": "Humidity percentage" }  },  "required": ["temperature", "conditions", "humidity"]}
Safetools/call result
{  "jsonrpc": "2.0",  "id": 5,  "result": {    "resultType": "complete",    "content": [      {        "type": "text",        "text": "{\"temperature\": 22.5, \"conditions\": \"Partly cloudy\", \"humidity\": 65}"      }    ],    "structuredContent": {      "temperature": 22.5,      "conditions": "Partly cloudy",      "humidity": 65    }  }}

Verbatim from the spec. The text block is the same data serialized, for clients that don't read structuredContent.

  • Exceptions everywhere. Frameworks that turn every exception into a JSON-RPC error hide fixable problems from the model. Catch business failures and return isError.
  • The opposite mistake. Returning isError for an unknown tool or malformed arguments; those are protocol errors.
  • Schema drift. Adding a field to structuredContent without updating outputSchema: clients that validate will reject your results.
  • Tool lists that change mid-conversation because the user called enable_admin: the list MUST NOT change as a side effect of other requests.
First time here? Set up the test helper (once per terminal)
ShellSetup
# 1. In a SECOND terminal, start the reference server (Node 18+, no dependencies)curl -sO https://www.diegozuluaga.dev/mcpa/reference-server.mjsnode reference-server.mjs                 # http://localhost:3000/mcp, logs appear here # 2. In THIS terminal, define the helper every test uses (bash or zsh)export MCP=http://localhost:3000/mcpMETA='"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}'mcp() {  # usage: mcp <method> '<json body>' [extra curl args...]  curl -sS -N "$MCP" \    -H 'Content-Type: application/json' \    -H 'Accept: application/json, text/event-stream' \    -H 'MCP-Protocol-Version: 2026-07-28' \    -H "Authorization: Bearer ${MCP_USER:-alice}" \    -H "Mcp-Method: $1" "${@:3}" -d "$2" \    -w '\nHTTP %{http_code}\n'}# Demo auth: the reference server treats the bearer token as the user's name.# Prefix a command with MCP_USER=bob to act as someone else.
ShellTests
# 1. Tool execution error: a past date comes back as a result with isError true (HTTP 200)mcp tools/call '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"book_flight","arguments":{"to":"DEN","date":"2020-01-01"},'"$META"'}}' -H 'Mcp-Name: book_flight' # 2. The model "fixes" itmcp tools/call '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"book_flight","arguments":{"to":"DEN","date":"2030-01-01"},'"$META"'}}' -H 'Mcp-Name: book_flight' # 3. Protocol error: an unknown tool is a JSON-RPC error, not isErrormcp tools/call '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"book_fligth","arguments":{},'"$META"'}}' -H 'Mcp-Name: book_fligth' # 4. Structured output: structuredContent plus the same data as textmcp tools/call '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"get_weather_data","arguments":{"location":"Lisbon"},'"$META"'}}' -H 'Mcp-Name: get_weather_data'

Answer all 4 correctly and this lesson is marked as learned.

Q1Your weather tool's upstream API times out. How should the server report it?

Q2The model calls a tool name that doesn't exist. What does the server return?

Q3A tool declares an outputSchema. Which statement is true?

Q4Spot the bug: after a user calls enable_admin_mode, the server starts returning three extra admin tools from tools/list on that same connection.

Feedback or a correction? Email diego [at] diegozuluaga [dot] dev or open an issue on GitHub.

Content CC BY 4.0 · Code MIT

Tip: ← and → move between lessons. Hover any heading and press # to copy a link to it.