# Structured Output

A tool that returns a plain `str` produces the result twice: as text in `content`, and as `{"result": "..."}` in `structured_content`.

This page is about that second channel: where it comes from, every shape it can take, and how the SDK keeps it honest.

The short version: **the return type annotation is the output schema**. You already wrote it.

## The output schema

```python title="server.py" hl_lines="9"
# docs_src/structured_output/tutorial001.py
from mcp.server import MCPServer

mcp = MCPServer("Weather")

READINGS = {"London": 17, "Cairo": 34, "Reykjavik": 4}


@mcp.tool()
def get_temperature(city: str) -> int:
    """Current temperature in a city, in whole degrees Celsius."""
    return READINGS[city]
```

The line that matters is the signature: `-> int`.

Because of it, the tool the SDK sends during `tools/list` carries an `output_schema` next to the input schema it builds from your parameters (**[Tools](https://py.sdk.modelcontextprotocol.io/servers/tools/index.md)** covers that one):

```json
{
  "properties": {
    "result": {"title": "Result", "type": "integer"}
  },
  "required": ["result"],
  "title": "get_temperatureOutput",
  "type": "object"
}
```

A bare `int` isn't a JSON object, so the SDK **wraps** it in `{"result": ...}`. Call the tool and both channels are filled:

```python
result.content             # [TextContent(text="17")]
result.structured_content  # {"result": 17}
```

Every scalar gets the same wrapper: `str`, `int`, `float`, `bool`, `bytes`, `None`.

## Two channels

Why send the same value twice?

* `content` is for the **model**. A language model reads text; this is the only part of the result it sees.
* `structured_content` is for the **application** the model runs inside: code that wants `17`, not a sentence containing "17".
* `output_schema` is the contract between them, published before the tool is ever called.

You return one Python value. The SDK fills in all three.

## Return a model

Declare the shape as a Pydantic `BaseModel` and return an instance:

```python title="server.py" hl_lines="8-11 15"
# docs_src/structured_output/tutorial002.py
from pydantic import BaseModel, Field

from mcp.server import MCPServer

mcp = MCPServer("Weather")


class WeatherData(BaseModel):
    temperature: float = Field(description="Degrees Celsius.")
    humidity: float = Field(description="Relative humidity, 0 to 1.")
    conditions: str


@mcp.tool()
def get_weather(city: str) -> WeatherData:
    """Current weather for a city."""
    return WeatherData(temperature=16.2, humidity=0.83, conditions="Overcast")
```

`WeatherData` **is** the schema now. No wrapper, no `result` key:

```json
{
  "properties": {
    "temperature": {"description": "Degrees Celsius.", "title": "Temperature", "type": "number"},
    "humidity": {"description": "Relative humidity, 0 to 1.", "title": "Humidity", "type": "number"},
    "conditions": {"title": "Conditions", "type": "string"}
  },
  "required": ["temperature", "humidity", "conditions"],
  "title": "WeatherData",
  "type": "object"
}
```

`structured_content` is the object, field for field:

```python
result.structured_content  # {"temperature": 16.2, "humidity": 0.83, "conditions": "Overcast"}
```

And the model is not left out. The SDK serializes the same object to JSON text for `content`:

```json
{
  "temperature": 16.2,
  "humidity": 0.83,
  "conditions": "Overcast"
}
```

Notice the `Field(description=...)` on `temperature` and `humidity` landed in the schema. The same `Field` that described your **inputs** describes your outputs.

!!! info
    If you've used FastAPI's `response_model`, you already know this: a Pydantic model as the declared
    response, serialized and documented for you. The only difference is that here the return annotation
    is the whole declaration.

## A `TypedDict`

Not every shape deserves a class. A `TypedDict` produces the same schema:

```python title="server.py" hl_lines="8"
# docs_src/structured_output/tutorial003.py
from typing import TypedDict

from mcp.server import MCPServer

mcp = MCPServer("Weather")


class WeatherData(TypedDict):
    temperature: float
    humidity: float
    conditions: str


@mcp.tool()
def get_weather(city: str) -> WeatherData:
    """Current weather for a city."""
    return WeatherData(temperature=16.2, humidity=0.83, conditions="Overcast")
```

A `TypedDict` is a plain `dict` at runtime, so that is what you build and return. The schema, the validation, and `structured_content` are identical to the `BaseModel` version (minus the descriptions, which `TypedDict` has no place for).

## A dataclass

Dataclasses work too, and so does any ordinary class whose attributes have type hints. The SDK builds a Pydantic model out of the annotations behind the scenes.

```python title="server.py" hl_lines="8-9"
# docs_src/structured_output/tutorial004.py
from dataclasses import dataclass

from mcp.server import MCPServer

mcp = MCPServer("Weather")


@dataclass
class WeatherData:
    temperature: float
    humidity: float
    conditions: str


@mcp.tool()
def get_weather(city: str) -> WeatherData:
    """Current weather for a city."""
    return WeatherData(temperature=16.2, humidity=0.83, conditions="Overcast")
```

Three spellings, one schema. Use whichever your codebase already has.

## Lists

A `list[...]` isn't a JSON object either, so it gets the `{"result": ...}` wrapper, with your item type as a `$defs` reference inside it:

```python title="server.py" hl_lines="15"
# docs_src/structured_output/tutorial005.py
from pydantic import BaseModel

from mcp.server import MCPServer

mcp = MCPServer("Weather")


class WeatherData(BaseModel):
    temperature: float
    humidity: float
    conditions: str


@mcp.tool()
def get_forecast(city: str, days: int) -> list[WeatherData]:
    """Daily forecast for a city."""
    return [WeatherData(temperature=16.2 + day, humidity=0.83, conditions="Overcast") for day in range(days)]
```

```json
{
  "$defs": {
    "WeatherData": {
      "properties": {
        "temperature": {"title": "Temperature", "type": "number"},
        "humidity": {"title": "Humidity", "type": "number"},
        "conditions": {"title": "Conditions", "type": "string"}
      },
      "required": ["temperature", "humidity", "conditions"],
      "title": "WeatherData",
      "type": "object"
    }
  },
  "properties": {
    "result": {"items": {"$ref": "#/$defs/WeatherData"}, "title": "Result", "type": "array"}
  },
  "required": ["result"],
  "title": "get_forecastOutput",
  "type": "object"
}
```

Ask for a two-day forecast and `structured_content` is `{"result": [{...}, {...}]}`. `content` becomes **two** `TextContent` blocks, one per item: a list is flattened for the model rather than dumped as one string.

`tuple[...]`, unions, and `Optional[...]` are wrapped the same way.

## Dictionaries

`dict[str, ...]` is the one generic that already *is* a JSON object, so it isn't wrapped:

```python title="server.py" hl_lines="9"
# docs_src/structured_output/tutorial006.py
from mcp.server import MCPServer

mcp = MCPServer("Weather")

READINGS = {"London": 16.2, "Cairo": 34.1, "Reykjavik": 4.4}


@mcp.tool()
def get_temperatures(cities: list[str]) -> dict[str, float]:
    """Current temperature for each city, in degrees Celsius."""
    return {city: READINGS[city] for city in cities}
```

```json
{
  "additionalProperties": {"type": "number"},
  "title": "get_temperaturesDictOutput",
  "type": "object"
}
```

```python
result.structured_content  # {"London": 16.2, "Reykjavik": 4.4}
```

The keys must be `str`. A `dict[int, float]` can't be a JSON object, so it falls back to the `{"result": ...}` wrapper.

## Validation

`output_schema` is not documentation. Whatever your function returns is **validated against it** before it leaves the server.

You don't notice while you build the value by hand: Pydantic already made sure your `WeatherData` was a `WeatherData`. You notice the day the data comes from somewhere you don't control:

```python title="server.py" hl_lines="9 21"
# docs_src/structured_output/tutorial007.py
import json

from pydantic import BaseModel

from mcp.server import MCPServer

mcp = MCPServer("Weather")

UPSTREAM = {"London": '{"temperature": 16.2, "conditions": "Overcast"}'}


class WeatherData(BaseModel):
    temperature: float
    humidity: float
    conditions: str


@mcp.tool()
def get_weather(city: str) -> WeatherData:
    """Current weather for a city."""
    return json.loads(UPSTREAM[city])
```

The annotation promises `WeatherData`. The upstream response stopped sending `humidity`.

!!! check
    Call `get_weather` and it does not quietly hand the client a half-empty object. The call fails,
    and the first lines of the error name the field:

    ```text
    Error executing tool get_weather: 1 validation error for WeatherData
    humidity
      Field required [type=missing, input_value={'temperature': 16.2, 'conditions': 'Overcast'}, input_type=dict]
    ```

    That text comes back as the tool result with `is_error=True`, so the model knows the call failed
    instead of confidently reading weather that isn't there.

Returning a plain `dict` from a `-> WeatherData` tool is fine, by the way. That's exactly what `json.loads` produced. Validation is on the value, not on the Python type.

## Opting out

Sometimes the return annotation is for your type checker, not for the protocol. Pass `structured_output=False` and the tool is text-only:

```python title="server.py" hl_lines="6"
# docs_src/structured_output/tutorial008.py
from mcp.server import MCPServer

mcp = MCPServer("Weather")


@mcp.tool(structured_output=False)
def weather_report(city: str) -> str:
    """A human-readable weather report for a city."""
    return f"{city}: 17 degrees, overcast, light rain easing by evening."
```

No `output_schema`, no wrapping, no validation. `structured_content` is `None` and `content` is the string you returned.

The opposite, `structured_output=True`, turns the automatic detection into a requirement: a tool whose return type can't produce a schema raises at import time instead of falling back to text.

## A class without type hints

There is one way to end up unstructured without asking for it: return a class that has **no annotations on its body**.

```python title="server.py" hl_lines="6-9"
# docs_src/structured_output/tutorial009.py
from mcp.server import MCPServer

mcp = MCPServer("Weather")


class Station:
    def __init__(self, name: str, online: bool):
        self.name = name
        self.online = online


@mcp.tool()
def get_station(name: str) -> Station:
    """Look up a weather station by name."""
    return Station(name=name, online=True)
```

`Station` sets `name` and `online` inside `__init__`, but the *class* declares nothing. The SDK reads class annotations, finds none, and gives up.

!!! warning
    It gives up **silently**. `output_schema` is `None`, `structured_content` is `None`, and the text
    the model reads is the object's `repr`:

    ```text
    "<server.Station object at 0x7f539d75b230>"
    ```

    No error, no warning, a useless tool. Move the annotations onto the class body, or pass
    `structured_output=True`, which turns this into a hard error the moment the module imports:
    `Function get_station: return type <class 'server.Station'> is not serializable for structured output`.

!!! tip
    Need full control (building the `CallToolResult` yourself, or attaching `_meta` that the
    application can see but the model can't)? That's **[The low-level Server](https://py.sdk.modelcontextprotocol.io/advanced/low-level-server/index.md)**.

## Recap

* The **return type annotation** is the output schema. It's published in `tools/list` as `output_schema`.
* Scalars, lists, tuples and unions are wrapped in `{"result": ...}`. Models, `TypedDict`s, dataclasses, annotated classes and `dict[str, ...]` are objects already and stay as they are.
* Every result carries `content` (text, for the model) **and** `structured_content` (data, for the application).
* What you return is validated against the schema. A mismatch is a tool error, not a corrupt result.
* `structured_output=False` opts a tool out. A class without type hints opts out silently; watch for it.

You now own everything a tool can say back. Next, the second primitive: **[Resources](https://py.sdk.modelcontextprotocol.io/servers/resources/index.md)**.
