Инструменты≈ 25 мин с практикой

Валидация инструмента: добавьте предел и проверьте четыре отказа

Добавим необязательный аргумент max_length. Клиент должен увидеть целое число с default 80, а сервер — отклонить пустую подпись, превышение выбранного предела, недопустимый предел и отсутствующее обязательное поле. Получится отдельный сервер label-validation; рабочий файл из первого урока сохраняется.

В результате

Необязательный предел, два успешных вызова и четыре ожидаемых отказа.

Перед началом

Первый сервер и lab_session.py из второго шага.

Скачать руководство целиком · Markdown
Граница версии

Python SDK v1 · mcp==1.30.0 · протокол 2025-11-25. Для SDK v2 требуется отдельная миграция.

Перед началом

Нужна папка лаборатории из первого урока с .venv и SDK 1.30.0, а также полный lab_session.py из контракта. Сохраните следующие два новых файла рядом с ним. Конфигурацию редактора не меняйте: мы проверим новую версию через отдельный stdio-клиент.

1. Создайте сервер с двумя аргументами

Сохраните validation_server.py:

validation_server.pyPython
Скачать
"""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")
validation_server.pyPython
"""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:

check_validation.pyPython
Скачать
"""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())
check_validation.pyPython
"""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
PowerShellPowerShell
.\.venv\Scripts\python.exe check_validation.py

Ожидаемая первая строка:

Ожидаемый вывод
PASS: schema, default limit, two successful calls, four errors
Ожидаемый вывод
PASS: schema, default limit, two successful calls, four errors

Ниже будет схема с max_length.type: "integer", max_length.default: 80; обязательным остаётся text. У короткого успешного вызова structuredContent.label равно Draft.

СлучайАргументыОжидаемый отказ
blanktext из пробеловLabel must contain 1 to max_length characters after trimming
over_limitDraft!, max_length 5Label must contain 1 to max_length characters after trimming
bad_limitDraft, max_length 0max_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 (в новой вкладке), клиент SDK v1 (в новой вкладке), tools и ошибки (в новой вкладке), Python str.strip (в новой вкладке).

Вопросы по материалу

Увидит ли клиент диапазон max_length от 1 до 80 в JSON Schema?

В этой версии примера он увидит целый тип и значение по умолчанию 80. Диапазон проверяет функция; minimum и maximum в схеме не объявлены. Чтобы передать диапазон клиенту, потребуется отдельное изменение аннотаций и тестов.

Чем отсутствие text отличается от строки из пробелов?

Без text вызов нарушает входную схему. Строка из пробелов проходит проверку типа, но после strip становится пустой и отклоняется правилом функции. В проверочном клиенте предусмотрены оба случая.

Продукты ATOMNO

Другие задачи для AI-помощника

Это отдельные продукты ATOMNO для задач с данными. Они не входят в учебные примеры.

Все продукты ATOMNO (в новой вкладке)