Создание tools с параметрами, валидацией через Pydantic, регистрация ресурсов и шаблонов ресурсов.
В этой теме мы углубимся в два главных объекта MCP: инструменты (tools) и ресурсы (resources). Туториал покажет, как создавать сложные инструменты с валидацией через Pydantic, регистрировать статические и динамические ресурсы, и использовать resource templates.
Инструменты — это действия (функции с аргументами), ресурсы — это данные (читаемые по URI).
Простые аннотации типов подходят для базовых случаев. Но что если нужно ограничить диапазон чисел, задать минимальную длину строки или связать несколько параметров в логическую группу? Здесь помогает Pydantic.
from pydantic import BaseModel, Field
from fastmcp import FastMCP
mcp = FastMCP("SearchServer")
class SearchParams(BaseModel):
query: str = Field(..., min_length=1, max_length=200, description="Поисковый запрос")
limit: int = Field(10, ge=1, le=100, description="Максимум результатов")
category: str = Field("all", pattern=r"^(all|users|products|orders)$")
@mcp.tool()
def search(params: SearchParams) -> str:
"""Ищет данные по заданным критериям."""
return f"Поиск: '{params.query}', лимит: {params.limit}, категория: {params.category}"Когда инструмент принимает Pydantic-модель как единственный параметр, FastMCP генерирует JSON Schema из полей модели. Каждый Field() задаёт ограничения:
min_length, max_length — ограничения на длину строкиge, le, gt, lt — числовые границы (greater/equal, less/equal)pattern — регулярное выражение для строкиdescription — описание поля, видимое AI-клиентуЕсли клиент передаст query: "" (пустую строку), Pydantic выбросит ValidationError — FastMCP перехватит его и вернёт клиенту структурированную ошибку. Функция search даже не вызовется.
Можно создавать несколько моделей для разных инструментов и переиспользовать их:
class Pagination(BaseModel):
offset: int = Field(0, ge=0)
limit: int = Field(20, ge=1, le=100)
class UserFilter(BaseModel):
name_contains: str | None = None
min_age: int | None = Field(None, ge=0)
is_active: bool | None = None
@mcp.tool()
def list_users(filter: UserFilter, pagination: Pagination) -> str:
"""Возвращает список пользователей с фильтрацией и пагинацией."""
return f"Фильтр: {filter.model_dump()}, Пагинация: {pagination.model_dump()}"Каждый параметр — отдельная Pydantic-модель. FastMCP сгенерирует вложенную JSON Schema, и AI-клиент будет передавать аргументы как filter.name_contains, pagination.limit и т.д.
Ресурс — это данные, доступные для чтения по URI. Простейший ресурс не принимает аргументов:
import json
APP_CONFIG = {
"version": "2.1.0",
"max_connections": 100,
"features": ["search", "analytics", "alerts"]
}
@mcp.resource("config://app")
def get_app_config() -> str:
"""Возвращает конфигурацию приложения."""
return json.dumps(APP_CONFIG, indent=2)Клиент читает ресурс через resources/read с URI config://app. Функция возвращает строку — содержимое ресурса.
Resource templates позволяют параметризовать URI. Переменные указываются в фигурных скобках и передаются в функцию как аргументы:
# База данных пользователей (имитация)
USERS_DB = {
"1": {"name": "Алиса", "role": "admin"},
"2": {"name": "Борис", "role": "user"},
"3": {"name": "Виктория", "role": "user"},
}
@mcp.resource("user://{user_id}/profile")
def get_user_profile(user_id: str) -> str:
"""Возвращает профиль пользователя по ID."""
user = USERS_DB.get(user_id)
if user is None:
return f"Пользователь {user_id} не найден"
return json.dumps(user, indent=2, ensure_ascii=False)Когда клиент запросит user://2/profile, FastMCP извлечёт user_id="2" из URI и передаёт в функцию. Один шаблон покрывает бесконечное число конкретных URI.
Шаблон может содержать несколько переменных:
@mcp.resource("files://{workspace}/{path}/info")
def get_file_info(workspace: str, path: str) -> str:
"""Возвращает информацию о файле в рабочем пространстве."""
full_path = f"/{workspace}/{path}"
return f"File info for: {full_path}"Клиент запрашивает files://project/src/main.py/info — workspace="project", path="src/main.py".
Важно: порядок переменных в URI определяется позициями в шаблоне, а не порядком параметров функции. FastMCP сопоставляет их по имени.
По умолчанию FastMCP считает ресурс текстовым (text/plain). Для бинарных данных можно указать MIME-тип:
from fastmcp import FastMCP
from fastmcp.server.context import Context
mcp = FastMCP("ImageServer")
@mcp.resource("images://{name}/thumbnail")
def get_thumbnail(name: str) -> bytes:
"""Возвращает миниатюру изображения."""
# В реальности — чтение файла с диска
return b"\x89PNG\r\n\x1a\n..." # бинарные данные PNGВозврат bytes сигнализирует FastMCP, что ресурс бинарный. Клиент получит данные в base64-кодировке.
Правило простое: если операция — чтение данных по адресу, используйте ресурс. Если операция — действие с аргументами, используйте инструмент.
Resource (чтение по URI):
config://appuser://123/profilefile:///var/log/app.logstatus://healthTool (действие с параметрами):
search_users(query="Алиса", limit=10)create_record(type="order", data={...})send_notification(user_id=123, message="Hello")Разница аналогична GET vs POST/PUT в REST API: ресурс читает, инструмент действует.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.