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:Truewhen 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 hasis_error=Trueand your message, so the model can retry or explain. Other unexpected exceptions also becomeis_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.