2025, Nov 06 00:03

Аннотированные модели в FastMCP: зачем Pydantic, если сеть не меняется

Разбираемся, зачем аннотированные модели Pydantic в FastMCP: они не меняют полезную нагрузку, но дают валидацию и контроль контракта данных. С примерами.

FastMCP обещает, что добавление аннотаций типа возвращаемого значения автоматически генерирует схемы выходных данных и позволяет клиентам десериализовать результаты обратно в объекты Python. На практике многие разработчики ожидают сразу увидеть по сети иной полезный груз или более богатые метаданные. Если вы заменили обычный dict[str, Any] на аннотированную модель Pydantic и не заметили никаких видимых изменений ни в описании сервера, ни в ответе на вызов инструмента, вы не одиноки. Возникает простой вопрос: какой реальный плюс даёт аннотированный класс, если проверка схемы вывода вам не нужна?

Краткое резюме проблемы

В документации сказано:

Когда вы добавляете аннотации типа возвращаемого значения, FastMCP автоматически генерирует схемы выводимых данных для проверки структурированных данных и даёт клиентам возможность десериализовать результаты обратно в объекты Python.

Однако переход от нетипизированного отображения к аннотированной модели может никак не повлиять на то, что вы видите в ответе клиента MCP или по сети. Транспортная полезная нагрузка остаётся прежней.

Минимальный пример: dict против аннотированной модели

Ниже — компактное сравнение двух подходов. Логика одинакова; отличается лишь тип возвращаемого значения.

from typing import Any
from pydantic import BaseModel, Field
from typing import Annotated
# Простой ответ dict
mcp.tool(name="calc_metrics")
def calc_metrics(token: str, limit: int) -> dict[str, Any]:
    ...
# Ответ с аннотированной моделью Pydantic
class OutputPacket(BaseModel):
    total: Annotated[int, Field(description="some description")]
    remaining: Annotated[int, Field(description="some description")]
mcp.tool(name="calc_metrics")
def calc_metrics(token: str, limit: int) -> OutputPacket:
    ...

Что происходит на самом деле

Аннотированный класс не заставляет FastMCP отправлять по сети больше данных. Замена возвращаемого типа с общего dict на модель Pydantic сама по себе не меняет байты, которые вы видите в ответе. Главная польза аннотированного класса — не в транспорте; она в контроле. Вы получаете более строгий контракт, который может проверять то, что выдаёт сервер и что принимает клиент, помогая убедиться, что данные корректны и не взломаны.

Практическая польза: валидация и границы

Ценность аннотированного класса в том, что вы можете задавать и обеспечивать выполнение ограничений. С полями Pydantic и Annotated можно описать допустимые диапазоны и форму возвращаемых данных, чтобы неверные значения сразу же отбраковывались. Это усиливает контроль над контрактом данных между вашим инструментом и его потребителями.

from typing import Annotated
from pydantic import BaseModel, Field
class StrictPacket(BaseModel):
    count: Annotated[int, Field(ge=0, le=100, description="bounded count")]
    remaining: Annotated[int, Field(description="some description")]
mcp.tool(name="calc_metrics")
def calc_metrics(token: str, limit: int) -> StrictPacket:
    ...

В таком виде count: Annotated[int, Field(ge=0, le=100)] гарантирует 0 <= count <= 100. Если значение выходит за пределы, это сразу обнаруживается. В этом и состоит практическое преимущество аннотированного возвращаемого типа по сравнению со свободным dict.

Почему это важно

Типизированные, проверенные выходные данные уменьшают неоднозначность и ловят некорректные или неожиданные значения максимально близко к источнику. Если для вас важно быть уверенным, что сервер отправляет корректные данные, а клиент получает корректные данные (не взломанные), аннотированные модели дают этот контроль. Стабильность внешнего поведения — это плюс: вы не меняете формат полезной нагрузки, вы укрепляете контракт.

Вывод

Если проверка схемы вывода вам не нужна, не стоит ожидать других сетевых ответов при возврате аннотированного класса. Выигрыш не в размере или форме полезной нагрузки, а в более жёстком, исполнимом контракте, который валидирует ваши данные, включая границы вроде ge/le. Параметры и ограничения на уровне полей смотрите в официальной справке: FastMCP: поля Pydantic.

Статья основана на вопросе на StackOverflow от omer и ответе от furas.