Структурований вивід
Машинний переклад
Цю сторінку перекладено автоматично з англомовної документації, і основною версією є англомовна сторінка. Якщо щось читається неправильно, на сторінці Переклади пояснено, як про це повідомити.
Інструмент, що повертає звичайний 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 заповнює всі три.
Повернення моделі
Оголосіть форму як BaseModel з Pydantic і поверніть екземпляр:
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вимикає це для інструмента. Клас без анотацій типів вимикає це мовчки; пильнуйте.
Тепер ви володієте всім, що інструмент може сказати у відповідь. Далі — другий примітив: Ресурси.