Інструменти
Машинний переклад
Цю сторінку перекладено автоматично з англомовної документації, і основною версією є англомовна сторінка. Якщо щось читається неправильно, на сторінці Переклади пояснено, як про це повідомити.
Інструмент — це функція, яку може викликати модель.
Щоб оголосити інструмент, додайте @mcp.tool() до звичайної Python-функції. Оце й увесь API.
Ваш перший інструмент
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.
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:
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-модель:
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 виводить сам, можна перевизначити в декораторі:
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, — на сторінці Структурований вивід.