# Валидация инструмента: добавьте предел и проверьте четыре отказа

Добавим необязательный аргумент `max_length`. Клиент должен увидеть целое число с default 80, а сервер — отклонить пустую подпись, превышение выбранного предела, недопустимый предел и отсутствующее обязательное поле. Получится отдельный сервер `label-validation`; рабочий файл из первого урока сохраняется.

## Перед началом

Нужна папка лаборатории из [первого урока](/uroki/pervyi-server-python/) с `.venv` и SDK 1.30.0, а также полный `lab_session.py` из [контракта](/uroki/chto-takoe-mcp/). Сохраните следующие два новых файла рядом с ним. Конфигурацию редактора не меняйте: мы проверим новую версию через отдельный stdio-клиент.

## 1. Создайте сервер с двумя аргументами

Сохраните `validation_server.py`:

```python
"""Add an optional length limit to the lab tool; keep its default at 80."""
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("label-validation")


@mcp.tool()
def normalize_label(text: str, max_length: int = 80) -> dict[str, str]:
    """Trim a label; use a length limit from 1 to 80 characters."""
    if not 1 <= max_length <= 80:
        raise ValueError("max_length must be between 1 and 80")
    label = text.strip()
    if not 1 <= len(label) <= max_length:
        raise ValueError("Label must contain 1 to max_length characters after trimming")
    return {"label": label}


if __name__ == "__main__":
    mcp.run(transport="stdio")
```

Проверки выполняются в определённом порядке. Сначала убеждаемся, что `max_length` находится в диапазоне 1–80. Затем удаляем крайние пробелы и проверяем длину подписи. Отрицательный предел не должен превращаться в странную ошибку текста, поэтому у него отдельное сообщение.

Тип `int` и значение по умолчанию попадут в `inputSchema`. Диапазон 1–80 в этом примере контролирует функция. В схеме не объявлены `minimum` или `maximum`; не обещайте клиенту ограничения, которых он не получил. Если вам нужна схема с диапазоном, это отдельное изменение аннотаций и тестов, а не замена сообщения об ошибке.

## 2. Создайте полный проверочный клиент

Сохраните `check_validation.py`:

```python
"""Check both the SDK input schema and the lab's additional length rule."""
import asyncio
import json
from lab_session import lab_session


async def main():
    async with lab_session("validation_server.py") as (session, _):
        tool = (await session.list_tools()).tools[0]
        schema = tool.inputSchema
        assert schema["properties"]["max_length"]["type"] == "integer"
        assert schema["properties"]["max_length"]["default"] == 80
        assert "max_length" not in schema.get("required", [])
        cases = [("default_80", {"text": "x" * 80}, False), ("five", {"text": "  Draft  ", "max_length": 5}, False), ("blank", {"text": " "}, True), ("over_limit", {"text": "Draft!", "max_length": 5}, True), ("bad_limit", {"text": "Draft", "max_length": 0}, True), ("missing_text", {}, True)]
        results = {}
        for name, arguments, expected_error in cases:
            answer = await session.call_tool("normalize_label", arguments)
            assert answer.isError is expected_error, name
            if name == "five":
                assert answer.structuredContent == {"label": "Draft"}
            if name == "default_80":
                assert answer.structuredContent == {"label": "x" * 80}
            results[name] = answer.model_dump(mode="json", exclude_none=True)
        print("PASS: schema, default limit, two successful calls, four errors")
        print(json.dumps({"input_schema": schema, "cases": results}, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    asyncio.run(main())
```

Проверка обращается к `validation_server.py`, не к исходному `label_server.py`. Она сначала утверждает опубликованную схему, затем проверяет два успеха: строку длиной 80 с default и Draft с пределом 5. После этого каждый отказ должен получить `isError: true`.

## 3. Запустите тест

```powershell
.\.venv\Scripts\python.exe check_validation.py
```

Ожидаемая первая строка:

```text
PASS: schema, default limit, two successful calls, four errors
```

Ниже будет схема с `max_length.type: "integer"`, `max_length.default: 80`; обязательным остаётся `text`. У короткого успешного вызова `structuredContent.label` равно Draft.

| Случай | Аргументы | Ожидаемый отказ |
|---|---|---|
| blank | text из пробелов | Label must contain 1 to max_length characters after trimming |
| over_limit | Draft!, max_length 5 | Label must contain 1 to max_length characters after trimming |
| bad_limit | Draft, max_length 0 | max_length must be between 1 and 80 |
| missing_text | пустые arguments {} | Ошибка схемы: Field required |

Не привязывайте приложение ко всему тексту диагностического сообщения SDK: его формат может измениться. Для этого упражнения флаг `isError`, успешные данные и схема являются проверяемыми условиями. Полный JSON помогает понять причину отказа.

## Если не получается

* `ModuleNotFoundError: lab_session` — сохраните общий файл из второго урока в эту же папку.
* В схеме нет `max_length` — клиент запустил старый сервер. Сверьте имя в `lab_session("validation_server.py")` и содержимое нового файла.
* Ошибки проверки входа возникают до тела функции: отсутствие text относится к SDK, а пустая строка — к вашему правилу.
* Если хотите увидеть новую схему в редакторе, нужна отдельная запись для нового сервера с его путём и перезапуском. Этот урок такой записи не создаёт и не заменяет рабочую .mcp.json.

## Проверьте себя

В копии проверочного клиента добавьте успешную подпись из одного символа с `max_length: 1` и отказ из двух символов при том же пределе. Сначала решите, какой ответ является правильным, затем внесите assert. Не меняйте ожидаемый ответ после падения, пока не выясните, нарушен тест или контракт.

## Версия и источники

Упражнение закреплено за Python 3.11 и `mcp==1.30.0`, SDK v1, протоколом 2025-11-25. SDK v2 требует отдельной миграции. Примеры используют синтетические подписи, без внешнего API и модели. Документация сверена 8 октября 2026 года.

На локальном stdio проверены опубликованная схема, два успеха и четыре отказа. Новая установка и UI редактора не выполнялись.

Источники: [FastMCP server v1](https://py.sdk.modelcontextprotocol.io/v1/server/), [клиент SDK v1](https://py.sdk.modelcontextprotocol.io/v1/client/), [tools и ошибки](https://modelcontextprotocol.io/specification/2025-11-25/server/tools), [Python str.strip](https://docs.python.org/3.11/library/stdtypes.html#text-sequence-type-str).
