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

Інструменти

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

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

Інструмент — це функція, яку може викликати модель.

Щоб оголосити інструмент, додайте @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.
  • Опис, який бачить модель, — це docstring: 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=...): опис окремого аргументу, який модель читає разом із docstring.
  • Field(ge=1, le=50): числові межі. У схемі вони стають "minimum": 1, "maximum": 50.
  • Literal["fiction", "non-fiction", "poetry"]: перелік (enum). Модель може вибрати лише одне з цих значень.

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, описують інструмент, який пише: чи може він щось видалити, і чи два виклики дають те саме, що й один? Специфікація визначає обидві лише для інструментів, що не є read-only, тож на search_books вони нічого б не сказали.

Чемний клієнт використовує їх, щоб вирішувати на кшталт «чи треба спитати користувача, перш ніж це запускати?». Це підказки, а не механізм безпеки. Ніколи не покладайтеся на те, що клієнт їх дотримається.

Tip

@mcp.tool() також приймає name= і description=, якщо ви не хочете виводити їх з імені функції та docstring. Зазвичай хочете.

Підсумки

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

Що відбувається зі значенням, яке ви повертаєте через return, — на сторінці Структурований вивід.