Перейти до змісту

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

Машинний переклад

Цю сторінку перекладено автоматично з англомовної документації, і основною версією є англомовна сторінка. Якщо щось читається неправильно, на сторінці Переклади пояснено, як про це повідомити.

Інструмент, що повертає звичайний 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 заповнює всі три.

Повернення моделі

Оголосіть форму як BaseModel з Pydantic і поверніть екземпляр:

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_schemaNone, structured_contentNone, а текст, який читає модель, — це 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 вимикає це для інструмента. Клас без анотацій типів вимикає це мовчки; пильнуйте.

Тепер ви володієте всім, що інструмент може сказати у відповідь. Далі — другий примітив: Ресурси.