mcp-example/
- pyproject.toml
- README.md
- .env.example
- data/ car_catalog.json
- src/qauto_mcp/ основний пакет
IT · Model Context Protocol
Від моноліту main.py до структурованого qauto server
Урок 10
Блок 1
Куди що класти і навіщо кожен шар
Архітектура
Ми зібрали qauto MCP server в одному main.py: tools, prompts, resources, HTTP-клієнт, Pydantic і сесія.
Це нормальний прототип. Але коли сервер росте — один файл важко читати, тестувати і розширювати. Потрібна цільова архітектура.
Архітектура
Поки все в main.py, легко змішати в одному файлі:
sid)Архітектура
Архітектура
Частина 1 — корінь і пакет
mcp-example/
src/qauto_mcp/
Архітектура
Частина 2 — models · services · MCP surface
models/
services/
tools/
resources/
prompts/
Блок 2
FastMCP і опис tools для LLM
Каркас
Каркас — це те, що FastMCP знімає з коду при реєстрації: імʼя сервера, instructions, lifespan і метадані tools.
Далі розберемо FastMCP(...), а потім окремо — що саме потрапляє в tools/list для LLM.
Каркас
Для stdio достатньо name + instructions + lifespan.
mcp = FastMCP(
name="qauto",
instructions=(
"Before add_car, read qauto://cars/catalog. "
"For analysis use prompt analyze_cars."
),
lifespan=app_lifespan,
)
Каркас
Блок 3
tools/list, декоратор і схеми
Що бачить LLM
Модель не читає тіло функції, імпорти чи валідатори — лише те, що Host віддав у tools/list.
FastMCP збирає JSON при реєстрації: декоратор → type hints → return type → docstring (якщо немає description=).
Що бачить LLM
Якщо параметри — звичайні типи (int, str) без Field, усе пояснення кладіть у docstring.
Без Args/Returns модель бачить майже лише імена й типи — сенс полів губиться.
Docstring
Без параметрів у декораторі: імʼя = імʼя функції, увесь docstring іде в tools/list як description.
@mcp.tool()
async def add_car(
carBrandId: int,
carModelId: int,
mileage: int = 0,
) -> str:
"""Create a car for the current user (POST /cars).
Before calling, read qauto://cars/catalog for valid ids.
Do not use for listing cars — use get_cars.
Args:
carBrandId: Brand id from catalog (>= 1).
carModelId: Model id for that brand (>= 1).
mileage: Current mileage, 0..999999.
Returns:
JSON string with status_code and body (Car or error).
"""
...
Docstring · tools/list
Весь docstring стає description. У inputSchema — лише типи, без описів полів.
{
"name": "add_car",
"description": "Create a car for the current user (POST /cars).\n\nBefore calling, read qauto://cars/catalog...\n\nArgs:\n carBrandId: Brand id...\n ...",
"inputSchema": {
"properties": {
"carBrandId": { "type": "integer" },
"carModelId": { "type": "integer" },
"mileage": { "default": 0, "type": "integer" }
},
"required": ["carBrandId", "carModelId"]
}
}
Що бачить LLM
Явний description у декораторі замінює docstring у tools/list.
@mcp.tool(
name="add_car",
title="Add car to garage",
description=(
"Add a single car to the current QAuto user garage "
"by brand id, model id, and mileage. "
"Before calling, read qauto://cars/catalog for valid ids."
),
)
async def add_car(...):
...
Що бачить LLM
Усі аргументи опційні. Без них FastMCP візьме імʼя функції і docstring.
None / True / FalseЩо бачить LLM · annotations
Підказки для Host (UX / ризик), не гарантія безпеки.
Для qauto щодня: name + title + description. Annotations — коли Host має відрізняти read-only.
Приклад 1 · без Pydantic
Якщо є description= — docstring для LLM не використовується. У схемі лишаються лише типи полів.
@mcp.tool(
name="add_car",
title="Add car to garage",
description=(
"Create a car (POST /cars). "
"Read qauto://cars/catalog first for valid ids."
),
)
async def add_car(
carBrandId: int,
carModelId: int,
mileage: int = 0,
) -> str:
...
Приклад 1 · tools/list
{
"name": "add_car",
"title": "Add car to garage",
"description": "Create a car (POST /cars). Read qauto://cars/catalog first for valid ids.",
"inputSchema": {
"properties": {
"carBrandId": { "type": "integer" },
"carModelId": { "type": "integer" },
"mileage": { "default": 0, "type": "integer" }
},
"required": ["carBrandId", "carModelId"]
}
}
У description — лише текст з декоратора. У полях схеми немає описів — для цього потрібен Field(description=...).
Приклад 2 · з Pydantic
Input і return type — Pydantic: у tools/list зʼявляться і inputSchema, і outputSchema.
class AddCarInput(BaseModel):
carBrandId: int = Field(ge=1, description="Brand id from catalog (>= 1).")
carModelId: int = Field(ge=1, description="Model id for that brand (>= 1).")
mileage: int = Field(default=0, ge=0, le=999_999, description="Current mileage.")
class AddCarResult(BaseModel):
status: Literal["added", "already_exists", "error"] = Field(
description="Result of the operation"
)
car_id: str | None = Field(default=None, description="ID if status is added")
message: str = Field(description="Human-readable explanation")
@mcp.tool(
name="add_car",
title="Add car to garage",
description=(
"Add a single car to the current QAuto user garage "
"by brand id, model id, and mileage."
),
)
async def add_car(car: AddCarInput) -> AddCarResult:
...
Приклад 2 · tools/list
{
"name": "add_car",
"title": "Add car to garage",
"description": "Add a single car to the current QAuto user garage ...",
"inputSchema": {
"properties": { "car": { "$ref": "#/$defs/AddCarInput" } },
"required": ["car"]
},
"outputSchema": {
"properties": {
"status": {
"type": "string",
"enum": ["added", "already_exists", "error"],
"description": "Result of the operation"
},
"car_id": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"description": "ID if status is added"
},
"message": {
"type": "string",
"description": "Human-readable explanation"
}
},
"required": ["status", "message"]
}
}
Return type → outputSchema: enum, nullable car_id, описи полів. Модель може планувати наступний крок ще до виклику.
Що бачить LLM
name / titledescription у декораторіЩо бачить LLM
Для resources і prompts — ті самі принципи: name, title, description.
Tools description читає переважно LLM. Resources/prompts — частіше людина в списку Host. Пишіть зрозуміло для обох.
Фінал заняття
На цій лекції ми розібрали цільову архітектуру qauto MCP server, каркас FastMCP і те, що бачить LLM у tools/list.
Головне для tools: docstring без декоратора vs явний description; plain types vs Pydantic з Field; annotations як підказки Host.
Далі — практичний перенос коду в структурований пакет і подальші покращення сервера.