Перейти к содержанию

Ошибки протокола и ошибки инструмента

У сервера появились поиск и чтение. Теперь рассмотрим, как они сообщают о неудаче. Ошибка может возникнуть до запуска операции: неверная JSON-форма, отсутствующие метаданные или неизвестный метод. Либо запрос корректен, но предметный результат недоступен. После этого урока вы сможете выбрать подходящую категорию ответа и не смешивать отсутствие источника с поломкой протокола.

Наш сервер имеет собственные RpcError и DomainError. Они не являются универсальными типами внешнего SDK. Первый связан с протокольной границей, второй — с библиотечной операцией. Обработчик формирует JSON-RPC-ответ либо результат инструмента. Код в архиве полный, но примеры не выполнялись. Поэтому формы сообщений ниже объясняют ожидаемое поведение, а не изображают фактический журнал работы подключённого приложения.

Сначала прочитать запрос

Если строка не разбирается как JSON, предметный метод ещё неизвестен. Сервер отвечает ошибкой разбора без выдуманного id. Если JSON существует, но объект не имеет правильного jsonrpc или имени метода, возникает ошибка формы запроса. В таком состоянии нельзя доверенно отразить произвольное поле как идентификатор операции. Поэтому обработчик сначала проверяет структуру, а затем связывает дальнейший ответ с допустимым id.

Отсутствующая версия либо возможности клиента относятся к параметрам запроса. Неизвестная версия получает отдельный код несовместимости и список поддерживаемых редакций. Эти случаи полезно различать: в первом клиент должен исправить свой пакет, во втором — выбрать совместимый профиль или сообщить пользователю об отсутствии общей версии. Наш modern-only сервер не выполняет старый handshake в ответ на любую ошибку и не подменяет выбранную редакцию автоматически.

Неизвестный method отличается от неизвестного имени инструмента. В первом случае сервер не реализует такой протокольный маршрут. Во втором tools/call существует, но его аргумент не указывает доступную операцию. Мы возвращаем стандартный отказ метода для первого и ошибку параметров для второго. Клиент может показать понятное объяснение, а не бесконечно повторять один и тот же вызов в надежде, что сервер «догадается».

Предметный отказ инструмента

Запрос library.read с корректно оформленным неизвестным id проходит проверку схемы. Но библиотека не находит разрешённый документ и выдаёт DomainError. Обвязка возвращает результат с isError: true и текстом недоступности. Протокольный обмен состоялся: сервер понял вызов и сообщил предметный отказ. Это не повод считать соединение испорченным или менять версию MCP. Пользователю может понадобиться другой источник, а не повторная инициализация.

Нулевой результат поиска вообще не обязан быть ошибкой. В корпусе может не быть нужных слов, и items: [] честно описывает это состояние. Если обозначать его внутренней ошибкой, приложение начнёт диагностировать исправный сервер. Если превращать пустой поиск в выдуманный документ, потеряется граница источников. Наличие результата и корректность операции — разные свойства. Такая модель помогает приложению спокойно показать отсутствие материалов и предложить уточнение запроса.

При добавлении записи черновика предметными отказами станут отсутствие подтверждения, истёкший токен или уже существующий slug. Они тоже отличаются от плохой JSON-формы. Сервер должен объяснить доступное действие без раскрытия секретов. Например, «подтверждение отсутствует или не подходит» полезнее вывода самого токена и всего payload в лог. На данном шаге запись ещё не доступна, но выбранное разделение ошибок подготовит её без смены общей формы обработки.

Внутренний сбой и диагностика

Неожиданное исключение нельзя автоматически выдавать пользователю целиком. Traceback может содержать пути, содержимое запроса или подробности среды. Наш обработчик записывает короткое диагностическое сообщение в stderr и возвращает общий внутренний отказ. Это сознательная учебная политика: секреты и текст запроса не попадают в лог. В реальном сопровождении можно добавить безопасный идентификатор инцидента и отдельный доверенный журнал, сохраняя различие публичного ответа и внутренней диагностики.

Слишком общий лог также имеет ограничение: по фразе «internal failure» трудно найти причину. Поэтому production-система должна определить, какие сведения допустимо фиксировать и кто их читает. Нельзя решать это простым печатанием любого объекта исключения. В нашей маленькой лаборатории достаточно объяснить границу и не обещать полноту мониторинга. Мы не запускаем сервер и не создаём фактических инцидентов, поэтому в архиве нет полученного диагностического отчёта.

Уведомление не получает ответ. Иначе клиент увидит сообщение без ожидаемой пары id и может нарушить собственный цикл обработки. Наш синхронный профиль не использует длительные запросы или подписки; отмена после завершённой короткой операции не меняет уже возвращённый результат. Если требуется отзыв активной задачи, необходима диспетчеризация и проверка состояния выполнения. Простая функция respond сама по себе не обеспечивает асинхронный контроль.

Ожидаемые сценарии можно описать предметно: повреждённая JSON-строка даёт parse error, правильная форма с неизвестной версией даёт incompatibility, неизвестный документ даёт isError, неизвестные слова поиска дают пустой список. Эти сценарии не запускались. Такое описание помогает читателю подготовить собственное наблюдение, не выдавая перечень ожиданий за прошедшие автоматические проверки. Сам статус not_run относится ко всем состояниям серии.

Повтор не исправляет любой отказ

Если query содержит неизвестные слова, повтор того же запроса не сделает документ существующим. Если версия неправильна, повтор без изменения metadata даст тот же отказ. При внутреннем временном сбое повтор иногда полезен, но его политика должна учитывать операцию и возможный эффект. Универсальная команда «попробовать снова десять раз» смешивает разные причины и может увеличить расходы или создать дубли при записи.

Для нашей короткой reader-операции приложение может показать отсутствие источника и предложить уточнение. Для будущего черновика потеря ответа потребует устойчивого operation, а не нового запроса создания с другим ключом. Поэтому формат ошибки нужен не только человеку: он помогает вызывающему коду выбрать следующий шаг. Понятная категория уменьшает вероятность бессмысленной автоматизации, которая реагирует на любое неудобство одинаково.

В сервере неожиданный сбой получает общий internal error, но это не означает, что программа доказала временность причины. Клиент не должен выводить такую классификацию из одного кода. Для сопровождения потребуется безопасная диагностика и наблюдение конкретного состояния, которых авторская подготовка без запуска не создаёт.

На следующем шаге добавим ресурсы. Они представят те же документы через устойчивые URI и отдельные методы списка и чтения. Правила доступа и категории ошибок должны сохранить смысл: новый маршрут не получает права обходить уже существующую библиотечную границу только потому, что его название отличается от tools/call.

Важная граница ошибки без известного id: используем схему error response именно MCP 2026-07-28, где поле id допускает отсутствие при повреждённом запросе. Это не универсальная реализация standalone JSON-RPC, который описывает такой случай иначе. Наш клиент работает с корректными запросами и проверяет отражённый id; повреждённая строка рассматривается как отдельный протокольный отказ.

Фрагмент файла server.py из состояния lesson-06. Полное самостоятельное состояние находится в архиве серии; фрагмент не заменяет остальные определения.

def call_tool(params):
    named = next((t for t in tools() if t["name"] == params.get("name")),None)
    if named is None:
        raise RpcError(-32602,"Tool unavailable")
    args = params.get("arguments",{})
    validate(args,named["inputSchema"])
    try:
        if named["name"] == "library.search":
            data = search(args["query"], args.get("limit",3))
        elif named["name"] == "library.read":
            data = read_document(args["id"])
        else:
            from storage import create_draft
            data = create_draft(**args, allow_retry=STAGE>=10)
        return complete({"content":[{"type":"text","text":json.dumps(data,ensure_ascii=False)}],"structuredContent":data,"isError":False})
    except DomainError as exc:
        return complete({"content":[{"type":"text","text":str(exc)}],"isError":True})

Статус примера: not_run. При подготовке не выполнялись учебные программы, тесты, вызовы AI или деплой. Описанные эффекты ожидаемые.

Оглавление серии · Полные исходники

Первичные документы: Источник 1, Источник 2.