Блог • Claude Code / MCP
После Claude Code 2.1.310: как записать в JSON таймаут MCP-запроса в неинтерактивной сессии, чтобы тот же инструмент не вызывали снова?
23 октября 2026 Claude Code 2.1.310 изменил одну ошибку без диалога. Если вызов MCP истекает, пока запрос сервера без ответа, модели говорят: ответа не было, этот вызов не повторять.
Документация MCP того же дня: если задача в сессии без диалога просит ввод, Claude Code просит сервер отменить её и не запускает хуки Elicitation и onElicitation Agent SDK. Больший timeout не создаёт того, кто ответит. Повтор в следующем claude -p решает сохранённый JSON.
Остановка журнала при несовпадении OAuth — в остановке OAuth. Сервер stdio, который список разрешения снова загружает, — в списке env. Повтор item между провайдерами — другой конверт, в повторе Open Responses. Эта статья только про поля журнала после таймаута запроса без диалога.
- Таймаут без диалога в 2.1.310 говорит не вызывать снова. Это не таймаут для повтора.
- Если status равен input_required без диалога, документация требует отмену и не запускает хуки Elicitation.
- Локальный журнал полем reason отделяет запрос без ответа, таймаут и outcome unknown. Если repeat равен true, шлюз отклоняет первые два.
- Для outcome unknown не ставьте тот же repeat false. Документация разрешает второй вызов только после того, как ясно, что сервер уже сделал.
Сначала это: 2.1.310 отделяет отсутствие ответа от повторной попытки. Дальше по порядку status, reason, repeat и какое поле останавливает следующий вызов в журнале без диалога.
2.1.310 вынимает запрос без ответа из таймаутов для повтора
Изменение — в журнале изменений Claude Code, заметки к выпуску — v2.1.310. Смысл: без диалога вызов MCP, истекший при запросе без ответа, теперь говорит модели, что ответа не было и вызов не повторять. Интерактивный терминал, где форму можно заполнить, это правило не использует.
Обновлённая в тот же день документация подключения MCP записывает timeout каждого сервера в миллисекундах. Значения ниже 1000 игнорируются, уведомления о прогрессе не продлевают эти часы. Без диалога нет окна запроса, поэтому 120000 лишь дольше ждёт ответ, которого не будет.
{
"mcpServers": {
"reports": {
"type": "http",
"url": "https://mcp.example.com/mcp",
"timeout": 120000
}
}
}
Одноразовая строка claude -p тоже не становится задачей MCP. Документация говорит, что такой запуск задач не получает; чтобы следить за задачей, нужны интерактивное возобновление или --input-format stream-json. Таймаут запроса может случиться и на обычном вызове, журнал не должен требовать taskId.
Какие поля обязательны, когда сервер возвращает задачу, которой нужен ввод?
В редакции протокола 2026-07-28 долгую работу можно вернуть на tools/call как задачу. Поля — по расширению MCP tasks, на расширении tasks. Документация Claude Code требует resultType task, а также taskId, status, createdAt, lastUpdatedAt и ttlMs. status только working, input_required, completed, failed или cancelled. input_required несёт inputRequests, а elicitation/create обслуживается только в интерактивном терминале. Без диалога Claude Code просит сервер отменить задачу.
{
"resultType": "task",
"taskId": "export-7f3a9c2e",
"status": "input_required",
"createdAt": "2026-10-23T16:44:00Z",
"lastUpdatedAt": "2026-10-23T16:44:00Z",
"ttlMs": null,
"inputRequests": {
"confirm": {
"method": "elicitation/create",
"params": {}
}
}
}
Этот JSON — квитанция сервера, не переключатель повтора. Запуск без диалога, увидев input_required, должен отменить, а не вставить тот же объект в следующий claude -p и выполнить снова.
Как локальный журнал говорит не вызывать снова?
Не кладите repeat в status протокола. repeat и reason — ваши поля журнала. Без диалога и без того, кто ответит: reason elicitation_unanswered. Для ошибки таймаута 2.1.310: reason elicitation_timeout. Оба ставят repeat в false. taskId может отсутствовать, обычный вызов не всегда задача.
{
"session": "non_interactive",
"tool": "export_report",
"taskId": "export-7f3a9c2e",
"status": "input_required",
"reason": "elicitation_unanswered",
"repeat": false
}
Форму фиксирует schema ниже. session только interactive или non_interactive. status — только пять значений протокола. reason отделяет остановку от outcome unknown. additionalProperties false не даёт запихнуть весь текст ошибки в необъявленное поле.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"additionalProperties": false,
"required": ["session", "tool", "status", "reason", "repeat"],
"properties": {
"session": { "enum": ["interactive", "non_interactive"] },
"tool": { "type": "string", "minLength": 1 },
"taskId": { "type": "string" },
"status": {
"enum": ["working", "input_required", "completed", "failed", "cancelled"]
},
"reason": {
"enum": ["elicitation_unanswered", "elicitation_timeout", "outcome_unknown", "none"]
},
"repeat": { "type": "boolean" }
}
}
Почему отклонять repeat true после прохождения schema?
Schema выше требует только, чтобы repeat был булевым. Журнал ниже имеет все поля и верные типы, но сессия non_interactive, reason равен elicitation_unanswered, а repeat равен true. Шлюз должен вернуть repeat_conflict, следующий запуск без диалога этот вызов не отправляет.
{
"session": "non_interactive",
"tool": "export_report",
"taskId": "export-7f3a9c2e",
"status": "input_required",
"reason": "elicitation_unanswered",
"repeat": true
}
| Видимый журнал | Шлюз | Следующий запуск без диалога |
|---|---|---|
| Без диалога, input_required, repeat false | stop | Этот вызов инструмента не отправлять. |
| Тот же случай без ответа, но repeat true | repeat_conflict | Отклонить. Сначала поставить repeat в false. |
| reason равен elicitation_timeout, repeat false | stop | Таймаут 2.1.310 тоже не вызывать снова. |
| session равен interactive | not_headless | Эту остановку без диалога не применять. Интерактивная сессия может ответить на запрос. |
Почему outcome unknown — не та же остановка?
В документации есть другая строка transcript: после отказа сервера в идентификаторе задачи или другой постоянной ошибки Claude Code перестаёт следить и пишет outcome unknown. Дальше второй вызов может повторить работу, поэтому сначала спросите сервер, что уже сделано. Это не запрет повтора из 2.1.310.
Шлюз сначала проверяет schema, затем неинтерактивна ли сессия. reason outcome_unknown возвращает check_server и не сворачивается в stop. Запрос без ответа, elicitation_timeout и input_required без диалога требуют repeat false и возвращают stop. Остальные сочетания возвращают no_rule. Функция не решает за вас новую деловую задачу.
def actual_type(data: object) -> str:
if data is None:
return "null"
if isinstance(data, bool):
return "boolean"
if isinstance(data, list):
return "array"
if isinstance(data, int):
return "integer"
if isinstance(data, float):
return "number"
if isinstance(data, str):
return "string"
if isinstance(data, dict):
return "object"
raise TypeError(type(data))
def validate(data: object, schema: dict) -> list[str]:
errors: list[str] = []
if not isinstance(schema, dict):
return errors
types = schema.get("type")
if isinstance(types, str):
types = [types]
actual = actual_type(data)
if types and not any(t == actual or (t == "number" and actual == "integer") for t in types):
errors.append("type")
if "enum" in schema and not any(value == data for value in schema["enum"]):
errors.append("enum")
if isinstance(data, str) and "minLength" in schema and len(data) < schema["minLength"]:
errors.append("minLength")
if isinstance(data, dict) and schema.get("properties"):
for key in schema.get("required") or []:
if key not in data:
errors.append(f"required:{key}")
if schema.get("additionalProperties") is False:
for key in data:
if key not in schema["properties"]:
errors.append(f"additional:{key}")
for key, sub in schema["properties"].items():
if key in data and data[key] is not None:
errors.extend(validate(data[key], sub))
return errors
def headless_repeat(record: dict, schema: dict) -> str:
if validate(record, schema):
return "schema"
if record.get("session") != "non_interactive":
return "not_headless"
if record.get("reason") == "outcome_unknown":
return "check_server"
if record.get("reason") in {"elicitation_unanswered", "elicitation_timeout"} or record.get("status") == "input_required":
if record.get("repeat") is not False:
return "repeat_conflict"
return "stop"
return "no_rule"
Как в JSONNote отделить остановку от неизвестного исхода?
- 1Сначала форма через schema
Откройте JSON Schema. Schema журнала в первое поле, журнал остановки или с repeat true во второе. Пропуск tool или неверный reason отмечаются сразу. repeat true может пройти, шлюз всё равно нужен.
- 2Затем проверить, булев ли repeat
Откройте JSON Формат. Вставьте журнал целиком. Если repeat — строка false, а не булево false, schema не проходит и шлюз возвращает schema. Не считайте это уже остановленным.
- 3В конце сравните квитанцию задачи и локальный журнал
Откройте JSON Diff. Слева задача input_required сервера, справа локальный журнал. reason и repeat только справа — это остановка. Если repeat нет ни там ни там, следующему запуску без диалога ещё не на чем остановиться.
Вопросы
Если увеличить timeout до нескольких минут, запрос без диалога дождётся ответа?
Нет. В сессии без диалога некому заполнить запрос. Больший timeout лишь растягивает ожидание ответа, которого не будет. Ошибка 2.1.310 говорит модели не вызывать снова. Она не превращает этот таймаут в автоматический повтор.
Можно ли outcome unknown тоже записать как repeat false?
Не используйте ту же остановку. Про outcome unknown документация говорит, что второй вызов может повторить уже сделанную работу, поэтому сначала спросите сервер. Шлюз для этого reason возвращает check_server. Таймаут запроса — это stop.
Одноразовый claude -p входит в задачу MCP?
Документация говорит нет. Такой запуск задач MCP не получает. Чтобы следить за задачей, возобновите интерактивно или используйте --input-format stream-json и условия автоматического фона. Таймаут запроса может случиться и на обычном вызове, taskId в журнале не обязателен.
Нужно ли input_required в интерактивном терминале тоже писать как stop?
Нет. Эта остановка покрывает только запуск без диалога. Интерактивный терминал может ответить на elicitation/create. Документация также пишет, что отмена без диалога не запускает хуки Elicitation и onElicitation. Не переносите headless repeat false на интерактивную сессию.
Нужно ли писать repeat false в status протокола?
Нет. status протокола остаётся working, input_required, completed, failed или cancelled. repeat и reason остаются в локальном журнале. false в status не совпадёт с расширением задач и скроет, был это таймаут запроса или outcome unknown.
Итог
2.1.310 меняет таймаут, при котором в сессии без диалога никто не ответил. Это не политика повтора для каждой ошибки MCP.
Решение: без диалога при запросе без ответа или таймауте пишите repeat false и останавливайте следующий вызов; repeat true отклоняйте даже при прохождении schema; для outcome unknown сначала спросите сервер и не используйте ту же остановку.
Дальше: форму журнала через Schema, булев repeat через Format, квитанцию и локальную остановку через Diff.