Structured output: returning data, not sentences
A sentence is readable by a model and useless to a program. Return a Pydantic model and the client gets both: text for the model and data for code.
class Order(BaseModel):
id: str
item: str
status: Literal["processing", "shipped", "delivered"]
total: float@mcp.tool()
def lookup_order(order_id: str) -> Order:
"""Look up an order by its id."""
return Order(id=order_id, **ORDERS[order_id])The return type is now Order. **ORDERS[order_id] spreads the stored dictionary into the model's fields, and Pydantic checks each one, including that status is one of the three allowed values.
import asyncio
from mcp import Client
from shop import mcp
async def main():
async with Client(mcp) as client:
result = await client.call_tool("lookup_order", {"order_id": "B42"})
print(result.content[0].text)
print(result.structured_content)
print(result.structured_content["total"] + 5)
asyncio.run(main())content is the order as JSON text, for the model. structured_content is the order as a dictionary, with no result wrapper this time because a model is already an object. The last line does arithmetic on the total, which a sentence would never have allowed.
The output schema
import asyncio
from mcp import Client
from shop import mcp
async def main():
async with Client(mcp) as client:
tools = await client.list_tools()
print(tools.tools[0].output_schema)
asyncio.run(main())The return type became an output schema, published before anyone calls the tool. An application can know the shape of the answer in advance.
Data that does not match
The return value is checked against that schema on the server. If ORDERS held a status of "lost", the call would fail with an error instead of sending a record that breaks the promise the schema made.
- Set B42's status to
"lost"inORDERS, call the tool, and printis_errorand the text. - Change the return type to
dictand compareoutput_schema. - Add a
delivered_on: str | None = Nonefield toOrder.
Every expert started right here.