Три отказа MCP-инструмента в Claude Code: ошибка, зависание и пустой успех

Соло-разработчик, который подключает MCP-серверы в Claude Code, редко задумывается о том, как агент переживает сбой инструмента — пока сессия не уходит в бесконечное ожидание или модель уверенно строит план на пустом ответе. В разборе @rulestack разложил три режима отказа MCP и показал, что настройки .mcp.json и формулировки isError напрямую определяют, сколько ходов агент потратит впустую.
Всё началось с короткой записи в Bluesky: «an MCP tool that errors teaches the model something, one that hangs teaches it nothing». Ответ в ленте добавил третий кейс — инструмент, который возвращает успешный, но пустой результат: текущий ход не сгорает, зато «сжигает следующие шесть», потому что модель продолжает рассуждать поверх ответа, которого по сути не было.
Явная ошибка — единственный честный канал обратной связи
В спецификации MCP, как пересказывает автор, два разных канала сбоя. Protocol errors — JSON-RPC-ошибки вроде неизвестного инструмента или невалидных аргументов: «сломанный вызов», его читает клиентская обвязка. Tool execution errors — сбой внутри результата с isError: true: API упал, данные невалидны, бизнес-логика отказала; вызов формально прошёл, работа — нет. Этот текст модель видит в диалоге.
Поле content при isError: true — единственная телеметрия, которую агент получает от инструмента. Сообщение "Date must be YYYY-MM-DD, got '19/08/2026'" учит модель формату лучше, чем безликое "Error 500". Текст ошибки стоит писать для модели, а не для человеческого лога: что упало, почему, как выглядит валидный retry.
Честный execution error дешевле зависания и пустого успеха — агент хотя бы знает, где сломалось.
Зависание: стек таймеров Claude Code
Второй режим — зависание. Здесь решают не только ваш код, но и таймеры Claude Code и запись сервера в .mcp.json.
| Механизм | Поведение |
|---|---|
| Старт сервера | Ограничен MCP_TIMEOUT — например, MCP_TIMEOUT=10000 claude даёт 10 секунд на подъём |
| Вызов инструмента | Поле timeout (мс) в .mcp.json — пример "timeout": 600000 (10 минут); переопределяет MCP_TOOL_TIMEOUT для этого сервера; значения < 1000 игнорируются |
| Дефолт без настроек | MCP_TOOL_TIMEOUT по умолчанию — около 28 часов, не секунд |
| Idle timeout | Вызов без ответа и без progress notification прерывается: 5 мин для HTTP/SSE/WebSocket, 30 мин для stdio (Claude Code v2.1.187+, stdio с v2.1.203); настройка CLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT (мс), 0 отключает |
| Долгий вызов в чате | После 2 минут уходит в background task — «висит 10 минут» ≠ «сессия заблокирована 10 минут» |
Уведомления о прогрессе сбрасывают idle-таймер, но не продлевают лимит timeout по wall-clock. Для легитимно медленной работы автор рекомендует progress notifications; при риске зависания — быстрый отказ, потому что явная ошибка обходится дешевле молчаливого ожидания.
MCP_TIMEOUT=10000 claude
"timeout": 600000
Пустой успех: ловушка на несколько ходов вперёд
Третий режим — самый коварный для агентного цикла. Инструмент возвращает корректный успешный результат без содержимого: [], {"results": []}, пустая строка — без isError, без таймаута. Модель не отличает «искал и ничего не нашёл» от «не смог искать» и верит пустому ответу.
В примерах автора сбой всплывает через 3–6 ходов — дубликат пользователя, перезапись конфига — далеко от сервера, который молча вернул пустоту. Этот режим хуже явной ошибки и зависания: те хотя бы помечают место поломки.
Что менять на стороне MCP-сервера:
- Вместо голого
[]— явное описание: сколько строк совпало, сколько всего в таблице. - Три состояния: найдено N; ничего не найдено (запрос точно выполнен); запрос не выполнен — последнее через
isError: true. - Fallback при сбое не маскировать под успех: «lookup defaults to 0» превращает ошибку в ложный ноль.
Чеклист для тех, кто пишет MCP-инструменты
Общий принцип: строка результата — единственная телеметрия модели. Шесть практических пунктов:
- Execution failures →
isError: true+ что / почему / как retry. - Protocol-level проблемы → protocol errors, не прятать в «успешный» результат.
- Медленная работа → progress notifications; не полагаться на дефолт ~28 ч.
- Реалистичный per-server
timeoutв.mcp.json. - Пустые результаты с объяснением запроса и смысла «пусто»; не голый
[]. - Без тихих fallback на failure path.
Для соло-разработчика с MCP в Claude Code это не абстрактная гигиена API, а способ не тратить половину сессии на отладку агента, который честно поверил пустому ответу.