Перейти к содержанию

Структурированный вывод

Машинный перевод

Эта страница переведена с английской документации автоматически, и основной версией остаётся английская страница. Если что-то читается неправильно, на странице Переводы объясняется, как об этом сообщить.

Инструмент, возвращающий обычную строку str, выдаёт результат дважды: как текст в content и как {"result": "..."} в structured_content.

Эта страница посвящена второму каналу: откуда он берётся, какие формы может принимать и как SDK следит за его корректностью.

Если коротко: аннотация возвращаемого типа и есть выходная схема. Вы её уже написали.

Выходная схема

server.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]

Важна строка с сигнатурой: -> int.

Благодаря ей инструмент, который SDK отправляет в ответ на tools/list, несёт output_schema рядом с входной схемой, построенной по параметрам (о ней — на странице Инструменты):

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

Голое значение int — не JSON-объект, поэтому SDK оборачивает его в {"result": ...}. Вызовите инструмент — и оба канала заполнены:

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

Ту же обёртку получает любой скаляр: str, int, float, bool, bytes, None.

Два канала

Зачем отправлять одно и то же значение дважды?

  • content — для модели. Языковая модель читает текст; это единственная часть результата, которую она видит.
  • structured_content — для приложения, внутри которого работает модель: для кода, которому нужно 17, а не предложение, содержащее «17».
  • output_schema — контракт между ними, опубликованный ещё до первого вызова инструмента.

Вы возвращаете одно значение Python. SDK заполняет все три.

Возврат модели

Объявите форму как Pydantic BaseModel и верните экземпляр:

server.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. Ни обёртки, ни ключа result:

{
  "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 — это сам объект, поле за полем:

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

И модель не остаётся в стороне. SDK сериализует тот же объект в JSON-текст для content:

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

Обратите внимание: Field(description=...) у temperature и humidity попали в схему. Тот же Field, который описывал входы, описывает и выходы.

Info

Если вы пользовались response_model в FastAPI, вам это уже знакомо: модель Pydantic как объявленный ответ, который за вас сериализуется и документируется. Единственное отличие — здесь всё объявление сводится к аннотации возвращаемого типа.

TypedDict

Не каждая форма заслуживает класса. TypedDict даёт ту же схему:

server.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")

Во время выполнения TypedDict — обычный dict, его вы и собираете и возвращаете. Схема, валидация и structured_content идентичны варианту с BaseModel (за вычетом описаний, которые в TypedDict разместить негде).

Dataclass

Dataclass тоже подходят, как и любой обычный класс, атрибуты которого снабжены аннотациями типов. SDK незаметно строит модель Pydantic по этим аннотациям.

server.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")

Три способа записи — одна схема. Используйте тот, что уже принят в вашей кодовой базе.

Списки

list[...] тоже не JSON-объект, поэтому получает обёртку {"result": ...}, а тип элемента попадает внутрь как ссылка $defs:

server.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)]
{
  "$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"
}

Запросите прогноз на два дня — и structured_content будет {"result": [{...}, {...}]}. content превращается в два блока TextContent, по одному на элемент: для модели список раскладывается поэлементно, а не сваливается в одну строку.

tuple[...], объединения и Optional[...] оборачиваются так же.

Словари

dict[str, ...] — единственный дженерик, который уже является JSON-объектом, поэтому он не оборачивается:

server.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}
{
  "additionalProperties": {"type": "number"},
  "title": "get_temperaturesDictOutput",
  "type": "object"
}
result.structured_content  # {"London": 16.2, "Reykjavik": 4.4}

Ключи должны быть str. dict[int, float] не может быть JSON-объектом, поэтому для него применяется запасной вариант — обёртка {"result": ...}.

Валидация

output_schema — не документация. Всё, что возвращает функция, проверяется на соответствие схеме до того, как покинет сервер.

Пока значение собирается вручную, этого не замечаешь: Pydantic уже позаботился о том, чтобы WeatherData был WeatherData. Заметно становится в тот день, когда данные приходят из источника, который вы не контролируете:

server.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])

Аннотация обещает WeatherData. Ответ вышестоящего сервиса перестал присылать humidity.

Check

Вызовите get_weather — и он не передаст клиенту молча полупустой объект. Вызов завершается ошибкой, и первые же строки ошибки называют поле:

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]

Этот текст возвращается как результат инструмента с is_error=True, так что модель знает, что вызов не удался, а не уверенно читает погоду, которой нет.

Кстати, вернуть обычный dict из инструмента с -> WeatherData вполне допустимо. Именно это и выдал json.loads. Проверяется значение, а не тип Python.

Отказ от структурированного вывода

Иногда аннотация возвращаемого типа нужна для проверки типов, а не для протокола. Передайте structured_output=False — и инструмент станет чисто текстовым:

server.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."

Ни output_schema, ни обёртки, ни валидации. structured_content равен None, а content — строка, которую вы вернули.

Обратный вариант, structured_output=True, превращает автоматическое определение в требование: инструмент, по возвращаемому типу которого нельзя построить схему, выбрасывает исключение при импорте, а не переключается на текст.

Класс без аннотаций типов

Есть один способ оказаться без структурированного вывода, не прося об этом: вернуть класс, в теле которого нет аннотаций.

server.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 задаёт name и online внутри __init__, но сам класс ничего не объявляет. SDK читает аннотации класса, не находит ни одной и сдаётся.

Warning

Сдаётся он молча. output_schema равен None, structured_content равен None, а текст, который читает модель, — это repr объекта:

"<server.Station object at 0x7f539d75b230>"

Ни ошибки, ни предупреждения — бесполезный инструмент. Перенесите аннотации в тело класса или передайте structured_output=True, что превратит это в жёсткую ошибку в момент импорта модуля: Function get_station: return type <class 'server.Station'> is not serializable for structured output.

Tip

Нужен полный контроль (собирать CallToolResult самостоятельно или прикреплять _meta, которые видит приложение, но не модель)? Это Низкоуровневый Server.

Итоги

  • Аннотация возвращаемого типа — это выходная схема. Она публикуется в tools/list как output_schema.
  • Скаляры, списки, кортежи и объединения оборачиваются в {"result": ...}. Модели, TypedDict, dataclass, классы с аннотациями и dict[str, ...] уже являются объектами и остаются как есть.
  • Каждый результат несёт content (текст, для модели) и structured_content (данные, для приложения).
  • Возвращаемое значение проверяется на соответствие схеме. Несоответствие — это ошибка инструмента, а не испорченный результат.
  • structured_output=False отключает структурированный вывод для инструмента. Класс без аннотаций типов отключает его молча — следите за этим.

Теперь вы владеете всем, что инструмент может сказать в ответ. Дальше — второй примитив: Ресурсы.