Блог • 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, reason, repeat и какое поле останавливает следующий вызов в журнале без диалога.

2.1.310 вынимает запрос без ответа из таймаутов для повтора

Изменение — в журнале изменений Claude Code, заметки к выпуску — v2.1.310. Смысл: без диалога вызов MCP, истекший при запросе без ответа, теперь говорит модели, что ответа не было и вызов не повторять. Интерактивный терминал, где форму можно заполнить, это правило не использует.

Обновлённая в тот же день документация подключения MCP записывает timeout каждого сервера в миллисекундах. Значения ниже 1000 игнорируются, уведомления о прогрессе не продлевают эти часы. Без диалога нет окна запроса, поэтому 120000 лишь дольше ждёт ответ, которого не будет.

timeout в записи сервера. Он не подставляет отсутствующий ответ.
{
  "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 просит сервер отменить задачу.

Задача от сервера. status равен input_required, метод elicitation/create.
{
  "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 может отсутствовать, обычный вызов не всегда задача.

Журнал, который может остановить следующий вызов без диалога. repeat равен false.
{
  "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 локального журнала. Законный тип repeat не значит, что вызов можно повторить.
{
  "$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, следующий запуск без диалога этот вызов не отправляет.

Schema может пройти. Шлюз всё равно должен вернуть repeat_conflict.
{
  "session": "non_interactive",
  "tool": "export_report",
  "taskId": "export-7f3a9c2e",
  "status": "input_required",
  "reason": "elicitation_unanswered",
  "repeat": true
}
Видимый журналШлюзСледующий запуск без диалога
Без диалога, input_required, repeat falsestopЭтот вызов инструмента не отправлять.
Тот же случай без ответа, но repeat truerepeat_conflictОтклонить. Сначала поставить repeat в false.
reason равен elicitation_timeout, repeat falsestopТаймаут 2.1.310 тоже не вызывать снова.
session равен interactivenot_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. Функция не решает за вас новую деловую задачу.

Шлюз журнала без диалога. Не схлопывайте check_server и stop в один булев.
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. 1
    Сначала форма через schema

    Откройте JSON Schema. Schema журнала в первое поле, журнал остановки или с repeat true во второе. Пропуск tool или неверный reason отмечаются сразу. repeat true может пройти, шлюз всё равно нужен.

  2. 2
    Затем проверить, булев ли repeat

    Откройте JSON Формат. Вставьте журнал целиком. Если repeat — строка false, а не булево false, schema не проходит и шлюз возвращает schema. Не считайте это уже остановленным.

  3. 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.

Назад в блог