Tool results, structured output and errors

Model Context Protocol (MCP) · Practical · 12 min

Clear results and recoverable errors are what let a model use your tool correctly on the first or second try.

A tool call returns a result with three parts:

  • content: a list of blocks (text, images, audio, links to resources) that the model reads.
  • structured_content: typed JSON data for the client application to use in code.
  • is_error: True when the tool failed.

With MCPServer you just return Python values. If you annotate the return type (-> Note, -> int, -> list[str], a TypedDict or a Pydantic model), the SDK also publishes an output schema for the tool, fills structured_content with the data, and puts a JSON text version in content for the model. Plain types like int are wrapped as {"result": 3}.

Errors come in two kinds, and the difference matters:

  • Tool errors are failures the model should hear about and can fix: unknown title, bad date, permission denied. Raise ToolError("..."). The result has is_error=True and your message, so the model can retry or explain. Other unexpected exceptions also become is_error=True, but SDK v2 hides their message (the model only sees "Error executing tool ...") so internal details don't leak.
  • Protocol errors mean the request itself was invalid (unknown tool, malformed message). The model never sees these; the client gets a JSON-RPC error.

Write error messages like hints: say what was wrong and what would work, for example listing valid titles.

Example

from pydantic import BaseModel
from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ToolError

mcp = MCPServer("Notes")
NOTES = {"groceries": "milk, eggs"}


class Note(BaseModel):
    title: str
    body: str
    words: int


@mcp.tool()
def get_note(title: str) -> Note:
    """Return one note by its exact title."""
    if title not in NOTES:
        raise ToolError(f"No note {title!r}. Known titles: {sorted(NOTES)}")
    body = NOTES[title]
    return Note(title=title, body=body, words=len(body.split()))

# call_tool("get_note", {"title": "groceries"})
#   structured_content -> {'title': 'groceries', 'body': 'milk, eggs', 'words': 2}
# call_tool("get_note", {"title": "nope"})
#   is_error -> True, content -> "Error executing tool get_note: No note 'nope'. ..."

Remember

  • content is for the model, structured_content is for code
  • A return type annotation adds an output schema automatically
  • Raise ToolError for failures the model should read and recover from
  • Other exceptions are reported with a generic message in SDK v2

Watch out for

  • Raising a plain ValueError with a helpful message and expecting the model to see it; in SDK v2 only ToolError messages reach the model.

Check yourself

  1. A user asks for a note that doesn't exist. How should the tool report it?
  2. Who is structured_content mainly for?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: py.sdk.modelcontextprotocol.io/servers/structured-output/