Структурированный вывод
Машинный перевод
Эта страница переведена с английской документации автоматически, и основной версией остаётся английская страница. Если что-то читается неправильно, на странице Переводы объясняется, как об этом сообщить.
Инструмент, возвращающий обычную строку str, выдаёт результат дважды: как текст в content и как {"result": "..."} в structured_content.
Эта страница посвящена второму каналу: откуда он берётся, какие формы может принимать и как SDK следит за его корректностью.
Если коротко: аннотация возвращаемого типа и есть выходная схема. Вы её уже написали.
Выходная схема
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 и верните экземпляр:
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 даёт ту же схему:
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 по этим аннотациям.
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:
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-объектом, поэтому он не оборачивается:
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. Заметно становится в тот день, когда данные приходят из источника, который вы не контролируете:
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 — и инструмент станет чисто текстовым:
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, превращает автоматическое определение в требование: инструмент, по возвращаемому типу которого нельзя построить схему, выбрасывает исключение при импорте, а не переключается на текст.
Класс без аннотаций типов
Есть один способ оказаться без структурированного вывода, не прося об этом: вернуть класс, в теле которого нет аннотаций.
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отключает структурированный вывод для инструмента. Класс без аннотаций типов отключает его молча — следите за этим.
Теперь вы владеете всем, что инструмент может сказать в ответ. Дальше — второй примитив: Ресурсы.