MCP: как клиенту различать isError и ошибку JSON-RPC
Ассистент ищет счёт по номеру. MCP-сервер доступен, HTTP-запрос завершается успешно, но в чате появляется «счёт оплачен», хотя инструмент сообщил, что запись не найдена. Причина может находиться в адаптере клиента: он проверил транспорт и забрал текст ответа, проигнорировав isError.
В другом случае инструмент переименовали, клиент продолжает вызывать старое имя и получает ошибку JSON-RPC. Если обе ситуации превратить в одинаковую строку «что-то пошло не так», агент потеряет возможность исправить запрос, а разработчик — понять, почему вызов не состоялся. Для обработки нужны отдельные решения на уровне протокола и результата инструмента.
Что именно завершилось успешно
Ответ JSON-RPC с полем error сообщает о проблеме обработки запроса: например, неизвестном инструменте или некорректных аргументах. В этой форме нет обычного result инструмента. Клиенту следует сохранить код и безопасное сообщение ошибки, сопоставить имя вызова с доступным каталогом и остановить обычный путь обработки результата.
У ошибки выполнения инструмента другая форма: JSON-RPC содержит result, а внутри него isError=true. Сервер смог принять вызов и вернуть результат, но операция завершилась неуспешно. Примеры — отказ внешнего API или невозможность найти запрошенный объект. Такая причина может быть полезна модели: она попросит пользователя уточнить номер, вместо того чтобы сочинять статус счёта.
Отсутствие isError означает обычный результат, если остальные проверки прошли. Содержимое text само по себе не является надёжным индикатором: пользовательские данные могут включать слова error или failed. Статус нужно читать из поля протокола, затем использовать текст как описание. Точно так же HTTP 200 говорит о доставке ответа, но не заменяет разбор его тела.
Минимальная проверка адаптера
Установите python -m pip install "mcp>=2,<3". Пример разбирает три подготовленных сообщения и валидирует result через настоящий тип CallToolResult из Python SDK. Это тест границы обработки: запускать сервер, подключать модель и получать доступ к бухгалтерской системе для него не нужно.
from mcp.types import CallToolResult, TextContent
def classify(message):
if "error" in message:
return "protocol_error", message["error"]["message"]
result = CallToolResult.model_validate(message["result"])
text = "\n".join(
item.text for item in result.content
if isinstance(item, TextContent)
)
return ("tool_error" if result.is_error else "success"), text
messages = [
{"jsonrpc": "2.0", "id": 1, "error": {
"code": -32602, "message": "Unknown tool: find_invoice_old"
}},
{"jsonrpc": "2.0", "id": 2, "result": {
"content": [{"type": "text", "text":
"Invoice not found for the supplied invoice_id. Ask for the invoice number."}],
"isError": True
}},
{"jsonrpc": "2.0", "id": 3, "result": {
"content": [{"type": "text", "text": "Invoice INV-42: paid"}]
}}
]
actual = [classify(message) for message in messages]
assert [kind for kind, _ in actual] == [
"protocol_error", "tool_error", "success"
]
assert "Ask for the invoice number" in actual[1][1]
for kind, text in actual:
print(kind, text)
Функция возвращает отдельную категорию и текст. Проверка подтверждает, что результат с isError=True не попадёт в success, а полезная причина сохранится для следующего шага. В примере обрабатываются только текстовые блоки; рабочий клиент должен также иметь явную политику для изображений, ресурсов и остальных типов содержимого.
При использовании высокоуровневого ClientSession.call_tool SDK обычно уже разбирает конверт JSON-RPC: ошибка протокола приходит как исключение, а успешная доставка tool result — как CallToolResult. Тогда первую развилку делайте в обработчике исключений вокруг call_tool, а вторую — по result.is_error. Не требуйте у объекта CallToolResult поле error, которого в его контракте нет.
В примере используется MCP Python SDK 2.x: поле JSON isError после разбора доступно как is_error. Это различие важно при переносе кода из обработчика сырого JSON в SDK. Зафиксируйте основную версию зависимости в проекте и проверяйте имена атрибутов на её типах: иначе исправный ответ сервера превратится в AttributeError ещё до обработки бизнес-результата.
Какую причину отдавать модели
Сообщение «счёт не найден, запросите номер счёта» помогает изменить следующий ход разговора. Текст «ошибка» такой возможности не даёт. Вместе с тем стек исключения, SQL-запрос и ответ авторизационного сервиса не должны автоматически превращаться в контекст модели. На сервере сопоставьте известные причины с короткими безопасными описаниями, а технические подробности сохраните в журнале с идентификатором запроса.
Сначала определите продуктовый смысл отсутствующей записи. Для поиска по множеству счетов пустой список может быть корректным успешным результатом. Для операции «скачать этот конкретный счёт» отсутствие объекта может означать неуспех. isError не выбирает эту семантику за вас: правило должно быть одинаковым в реализации, описании инструмента и клиентских тестах.
Не превращайте tool_error в безусловный повтор. Новый вызов с теми же аргументами не создаст отсутствующий счёт. Если причина временная, политика повторов должна учитывать конкретный инструмент и наличие побочных эффектов. Ситуация, когда запрос оборвался и результат записи неизвестен, требует отдельного разбора неизвестного исхода операции.
Где закрепить поведение
Разместите обработку статуса в одном клиентском адаптере, через который проходят все MCP-вызовы. Затем протестируйте неизвестное имя, ошибку выполнения и нормальный ответ на одном и том же инструменте. Полезно проверить и пустой content: отсутствие текста не должно стирать категорию ошибки. Если SDK отклоняет повреждённый result, фиксируйте это как отдельную проблему контракта, не заменяя её выдуманным бизнес-результатом.
Платный курс AI Agent Integrations PRO: MCP ведёт от сервера с tools и resources к клиентскому циклу discovery → вызов → обработка результата. В программе есть типизированные контракты, тесты и логи: вы сможете проверить такие развилки на собственной интеграции. Откройте программу курса и начните с пробного урока о MCP.
