IT · Model Context Protocol

Розробка qauto MCP server

Практика: від API додатку до tools для агента

Урок 6

План заняття

  1. 01 Вступ
  2. 02 Огляд додатку
  3. 03 Огляд API методів
  4. 04 Налаштування середовища
  5. 05 Додавання API-методу signup
  6. 06 Правила та рекомендації

Блок 1

Вступ

Від мінімального сервера до реального проєкту

Вступ

Від демо до qauto

На попередній лекції — мінімальний 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

Середовище

Що ставимо

mcp[cli] httpx python-dotenv pydantic

Середовище

Чому pydantic, якщо є FastMCP

Раніше вистачало type hints: title: str, priority: str = "normal" — FastMCP сам будував JSON-схему. Це працює лише для плоских простих параметрів.

  • Вкладені структури — обʼєкт із кількома полями всередині
  • Строга валідація — наприклад, priority лише "low" | "normal" | "high"
  • Чітка схема відповіді, а не просто str

Блок 6

Правила та рекомендації

Як описувати MCP tools для моделі

Практики

Що бачить модель

Модель не читає тіло функції. Вона бачить лише імʼя, docstring і схему параметрів.

Тому якість tool = якість опису, а не лише робочий код всередині.

Практики

1. Іменування

Добре

@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

Практики

2. Docstring

Модель читає docstring замість тіла функції

@mcp.tool()
async def create_task(
    title: str,
    priority: str = "normal",
) -> str:
    """Створює нову задачу в системі.

    Використовуй, коли користувач просить додати
    або створити задачу/тікет.
    Не використовуй для оновлення — є update_task.

    Args:
        title: Коротка назва (обовʼязково, не порожня).
        priority: "low", "normal" або "high".
            За замовчуванням "normal".

    Returns:
        Рядок з ID і підтвердженням створення.
    """

Практики

Docstring — чекліст

  • Що робить — одне речення на початку
  • Коли використовувати — і коли не використовувати, якщо є схожий tool
  • Формат / обмеження параметрів, якщо тип цього не покриває (наприклад, UUID)
  • Що повертається — за форматом, не за конкретним значенням
  • Пишіть так, ніби пояснюєте tool незнайомій людині, а не собі

Практики

3. Гранулярність

Добре

# Одна фраза = один 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

Практики

4. Типи і Pydantic

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 замість голого str
  • Field(description=...) — бачить модель
  • Опційні поля — завжди з дефолтом

Практики

5. Помилки

@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 "Сервіс не відповідає, спробуйте пізніше."
  • Читабельний текст як результат — не traceback
  • 404 ≠ 500 ≠ timeout — різні повідомлення
  • Без stack trace, внутрішніх URL і ключів

Практики

6. Побічні ефекти

@mcp.tool()
async def delete_task(task_id: str) -> str:
    """
    Безповоротно видаляє задачу за ID.
    Цю дію не можна скасувати.
    Використовуй лише якщо користувач
    явно підтвердив видалення.
    """
  • Незворотні дії — явно в docstring
  • За потреби патерн draft → confirm
  • Видалення, повідомлення, фінанси — з обережністю

Практики

7. Обсяг відповіді

@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
  • Не згодовуйте моделі гігантський сирий JSON

Практики

8. Сумісність

Якщо змінюєте сигнатуру існуючого tool (обовʼязковий параметр, інший формат відповіді) — клієнти зі старою схемою можуть зламатись.

Нові параметри — опційні з дефолтом

Додавайте поля як tags: list[str] | None = None, а не як обовʼязкові. Старі клієнти продовжать працювати без змін.

Не міняти формат відповіді без причини

Host уже «знає» стару схему. Раптова зміна структури повернення ламає очікування моделі й інтеграції.

Breaking change → новий tool *_v2

При серйозних змінах заведіть create_task_v2, а старий create_task залиште для зовнішніх клієнтів.

Практики

9. Тестування

@pytest.mark.asyncio
async def test_create_task():
    result = await create_task(
        title="Test task",
        priority="high",
    )
    assert "створена" in result
  • Спочатку Inspector / юніт-тест
  • Потім — підключення до Host
  • Ловить помилки схеми до «сліпих» викликів моделлю

Фінал заняття

Підсумки

На цій лекції — API qauto, MCP server в main.py і правила для якісних tools.

Далі — signin, add_car і get_cars для повноцінної роботи з гаражем.