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

Инструменты

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

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

Инструмент — это функция, которую может вызвать модель.

Чтобы объявить инструмент, достаточно повесить @mcp.tool() на обычную функцию Python. Вот и весь API.

Ваш первый инструмент

server.py
from mcp.server import MCPServer

mcp = MCPServer("Bookshop")


@mcp.tool()
def search_books(query: str, limit: int) -> str:
    """Search the catalog by title or author."""
    return f"Found 3 books matching {query!r} (showing up to {limit})."

Посмотрите, что получилось. Ни схем, ни JSON, ни протокола — просто функция. SDK извлекает из неё три вещи:

  • Имя инструмента — это имя функции: search_books.
  • Описание, которое видит модель, — это строка документации: Search the catalog by title or author.
  • Аргументы, которые модели разрешено передавать, берутся из аннотаций типов: query: str и limit: int.

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

По этим аннотациям типов SDK генерирует JSON Schema и отправляет её клиенту в ответе на tools/list:

{
  "type": "object",
  "properties": {
    "query": {"title": "Query", "type": "string"},
    "limit": {"title": "Limit", "type": "integer"}
  },
  "required": ["query", "limit"],
  "title": "search_booksArguments"
}

Оба аргумента попали в required, потому что ни у одного нет значения по умолчанию. Сейчас это исправим. (Ключи title — артефакты Pydantic; контракт составляют свойства, их типы и required.)

Tip

Аннотации типов здесь не документация. Это и есть контракт. Если клиент пришлёт "limit": "ten", SDK отклонит вызов ещё до того, как запустится функция.

Что получает модель в ответ

Вызовите инструмент с {"query": "dune", "limit": 5} — результат состоит из двух частей:

result.content             # [TextContent(text="Found 3 books matching 'dune' (showing up to 5).")]
result.structured_content  # {'result': "Found 3 books matching 'dune' (showing up to 5)."}

content — это текст, который читает модель. structured_content — типизированные данные для клиентского приложения. Они появились потому, что тип возвращаемого значения объявлен как -> str.

Пока не думайте о structured_content. Возвращайте из инструментов настоящие объекты Python, и всё сработает как надо; этому целиком посвящена страница Структурированный вывод.

Попробуйте сами

Запустите сервер через MCP Inspector:

uv run mcp dev server.py

Откройте URL, который он напечатает, перейдите на вкладку Tools и вызовите search_books.

Inspector отрисует форму с обязательным текстовым полем query и обязательным числовым полем limit. Эту форму он построил по аннотациям типов. Так же поступит любой другой MCP-клиент.

Необязательные аргументы

Дайте параметру значение по умолчанию, и он перестанет быть обязательным. Вот и всё. Это обычный Python.

server.py
from mcp.server import MCPServer

mcp = MCPServer("Bookshop")


@mcp.tool()
def search_books(query: str, limit: int = 10) -> str:
    """Search the catalog by title or author."""
    return f"Found 3 books matching {query!r} (showing up to {limit})."

Схема меняется соответственно:

{
  "type": "object",
  "properties": {
    "query": {"title": "Query", "type": "string"},
    "limit": {"default": 10, "title": "Limit", "type": "integer"}
  },
  "required": ["query"],
  "title": "search_booksArguments"
}

limit ушёл из required и получил "default": 10. Клиент, который его не укажет, получит 10 — ровно так же, как в Python.

Более подробные схемы с Field

Аннотации типов дают очень многое, но иногда аргумент хочется описать или ограничить.

Оберните тип в Annotated и добавьте Field из Pydantic:

server.py
from typing import Annotated, Literal

from pydantic import Field

from mcp.server import MCPServer

mcp = MCPServer("Bookshop")


@mcp.tool()
def search_books(
    query: Annotated[str, Field(description="Title or author to search for.")],
    limit: Annotated[int, Field(ge=1, le=50, description="Maximum number of results.")] = 10,
    genre: Literal["fiction", "non-fiction", "poetry"] | None = None,
) -> str:
    """Search the catalog by title or author."""
    where = f" in {genre}" if genre else ""
    return f"Found 3 books matching {query!r}{where} (showing up to {limit})."

Три нововведения, и все на параметрах:

  • Field(description=...): описание отдельного аргумента, которое модель читает вместе со строкой документации.
  • Field(ge=1, le=50): числовые границы. В схему они попадают как "minimum": 1, "maximum": 50.
  • Literal["fiction", "non-fiction", "poetry"]: перечисление. Модель может выбрать только одно из этих значений.

Check

Ограничения — не украшение. Вызовите инструмент с limit=999, и SDK ответит ошибкой инструмента ещё до запуска функции:

Input should be less than or equal to 50

Эта ошибка возвращается модели как результат инструмента, модель её читает и повторяет вызов с допустимым значением. Вы один раз написали le=50 и бесплатно получили самокорректирующихся агентов.

Info

Если вы работали с FastAPI или Pydantic, всё это вам уже знакомо. Это тот же Field, тот же Annotated, та же валидация. Ничего специфичного для MCP здесь учить не нужно.

Модель в качестве параметра

Когда инструмент принимает больше пары аргументов, сгруппируйте их в модель Pydantic:

server.py
from pydantic import BaseModel, Field

from mcp.server import MCPServer

mcp = MCPServer("Bookshop")


class Book(BaseModel):
    title: str
    author: str
    year: int = Field(ge=1450, description="Year of first publication.")


@mcp.tool()
def add_book(book: Book) -> str:
    """Add a book to the catalog."""
    return f"Added {book.title!r} by {book.author} ({book.year})."

Схема Book вкладывается во входную схему инструмента (как ссылка в $defs), модель заполняет её как JSON-объект, а функция получает настоящий экземпляр Book, уже проверенный, с атрибутами .title, .author и .year.

Можно сочетать как угодно: обычные параметры рядом с параметрами-моделями, вложенные модели, списки моделей. Везде один и тот же Pydantic.

async def

Если инструмент занимается вводом-выводом (вызывает API, читает файл, обращается к базе данных), объявите его через async def и используйте await внутри. SDK дождётся его выполнения.

Инструмент с обычным def тоже работает: SDK запускает его в отдельном потоке, так что сервер он не блокирует.

Больше ничего настраивать не нужно.

Имена, заголовки и аннотации

Всё, что SDK выводит сам, можно переопределить в декораторе:

server.py
from mcp.server import MCPServer
from mcp.types import ToolAnnotations

mcp = MCPServer("Bookshop")


@mcp.tool(
    title="Search the catalog",
    annotations=ToolAnnotations(read_only_hint=True, open_world_hint=False),
)
def search_books(query: str) -> str:
    """Search the catalog by title or author."""
    return f"Found 3 books matching {query!r}."
  • title — понятное человеку имя для интерфейсов. Клиенты покажут «Search the catalog» вместо search_books.
  • annotations — поведенческие подсказки для клиента:
  • read_only_hint=True: этот инструмент ничего не меняет.
  • open_world_hint=False: он работает с закрытым набором объектов (этим каталогом), а не с открытым интернетом.
  • Две другие, destructive_hint и idempotent_hint, описывают инструмент, который пишет: может ли он что-то удалить и равносилен ли двойной вызов одному? Спецификация определяет обе только для инструментов не только для чтения, так что на search_books они ничего бы не значили.

Добросовестный клиент использует их, чтобы решать вопросы вроде «нужно ли спросить пользователя, прежде чем это запускать?». Это подсказки, а не средство безопасности. Никогда не полагайтесь на то, что клиент будет их соблюдать.

Tip

@mcp.tool() также принимает name= и description=, если не хочется выводить их из имени функции и строки документации. Чаще всего хочется.

Итоги

  • @mcp.tool() на функции делает её инструментом. Имя — от функции, описание — из строки документации.
  • Аннотации типов и есть входная схема. Значения по умолчанию делают аргументы необязательными.
  • Annotated[..., Field(...)] добавляет описания и ограничения; Literal добавляет перечисления.
  • Параметр — модель Pydantic — это способ принять структурированное «тело».
  • Неправильные аргументы отклоняются за вас, с ошибкой, которую модель может прочитать и исправиться.
  • async def для ввода-вывода, обычный def для всего остального.

О том, что происходит со значением, которое вы возвращаете через return, — на странице Структурированный вывод.