IT · Model Context Protocol
Практика: від API додатку до tools для агента
Урок 6
Блок 1
Від мінімального сервера до реального проєкту
Вступ
На попередній лекції — мінімальний MCP на FastMCP і stdio: один tool і перевірка в Inspector.
Сьогодні підключаємо сервер до REST API навчального додатку qauto (Hillel Auto).
Мета: агент реєструє user, додає авто в гараж і читає список машин — природною мовою, без кліків у UI.
Вступ
Стек: mcp[cli], httpx, pydantic, python-dotenv.
Спочатку — додаток і повний JSON-контракт API. Потім — один main.py з базовим tool signup.
Спільний AsyncClient тримає cookie сесії після signup. На виході — робочий qauto MCP server, готовий до розширення новими методами.
Блок 2
Залежності для qauto MCP server
Середовище
Середовище
Раніше вистачало type hints: title: str, priority: str = "normal" — FastMCP сам будував JSON-схему. Це працює лише для плоских простих параметрів.
priority лише "low" | "normal" | "high"strБлок 6
Як описувати MCP tools для моделі
Практики
Модель не читає тіло функції. Вона бачить лише імʼя, docstring і схему параметрів.
Тому якість tool = якість опису, а не лише робочий код всередині.
Практики
Добре
@mcp.tool()
async def signup(...): ...
@mcp.tool()
async def get_profile(...): ...
@mcp.tool()
async def create_task(...): ...
# create_* get_* update_*
# delete_* list_* search_*
Погано
@mcp.tool()
async def task_handler(...): ...
@mcp.tool()
async def process(...): ...
# схожі імена плутають модель:
# update_task vs update_tasks
# → update_task + bulk_update_tasks
Практики
Модель читає docstring замість тіла функції
@mcp.tool()
async def create_task(
title: str,
priority: str = "normal",
) -> str:
"""Створює нову задачу в системі.
Використовуй, коли користувач просить додати
або створити задачу/тікет.
Не використовуй для оновлення — є update_task.
Args:
title: Коротка назва (обовʼязково, не порожня).
priority: "low", "normal" або "high".
За замовчуванням "normal".
Returns:
Рядок з ID і підтвердженням створення.
"""
Практики
Практики
Добре
# Одна фраза = один tool
create_task(...)
update_task(
task_id,
title=None,
priority=None,
)
delete_task(task_id)
# «створи задачу»
# «онови задачу»
# «видали задачу»
Погано
# Надто широко
manage_task(action=...)
# create|update|delete
# → розмита схема
# Надто дрібно
set_task_title
set_task_priority
set_task_status
Практики
from typing import Literal
from pydantic import BaseModel, Field
class CreateTaskRequest(BaseModel):
title: str = Field(
..., min_length=1,
description="Назва задачі",
)
priority: Literal["low", "normal", "high"] = "normal"
due_date: str | None = Field(
None,
description="Дата YYYY-MM-DD",
)
Literal замість голого strField(description=...) — бачить модельПрактики
@mcp.tool()
async def get_task(task_id: str) -> str:
"""Повертає задачу за ID."""
try:
data = await client.get(f"/tasks/{task_id}")
return str(data)
except httpx.HTTPStatusError as e:
if e.response.status_code == 404:
return f"Задачу {task_id} не знайдено."
return f"Помилка API: {e.response.status_code}"
except httpx.TimeoutException:
return "Сервіс не відповідає, спробуйте пізніше."
Практики
@mcp.tool()
async def delete_task(task_id: str) -> str:
"""
Безповоротно видаляє задачу за ID.
Цю дію не можна скасувати.
Використовуй лише якщо користувач
явно підтвердив видалення.
"""
Практики
@mcp.tool()
async def list_tasks(
limit: int = 20,
status: str | None = None,
) -> str:
"""Список задач (макс. limit, дефолт 20)."""
data = await client.get(
"/tasks",
params={"limit": limit, "status": status},
)
return format_tasks_compact(data)
limit / page з розумним дефолтомid — title — statusПрактики
Якщо змінюєте сигнатуру існуючого tool (обовʼязковий параметр, інший формат відповіді) — клієнти зі старою схемою можуть зламатись.
Нові параметри — опційні з дефолтом
Додавайте поля як tags: list[str] | None = None, а не як обовʼязкові.
Старі клієнти продовжать працювати без змін.
Не міняти формат відповіді без причини
Host уже «знає» стару схему. Раптова зміна структури повернення ламає очікування моделі й інтеграції.
Breaking change → новий tool *_v2
При серйозних змінах заведіть create_task_v2, а старий
create_task залиште для зовнішніх клієнтів.
Практики
@pytest.mark.asyncio
async def test_create_task():
result = await create_task(
title="Test task",
priority="high",
)
assert "створена" in result
Фінал заняття
На цій лекції — API qauto, MCP server в main.py і правила для якісних tools.
Далі — signin, add_car і get_cars для повноцінної роботи з гаражем.