博客 • Claude Code / MCP

升级到 Claude Code 2.1.310 之后,无交互会话里 MCP 征询超时,日志 JSON 要怎样记下不要再调同一个工具?

2026 年 10 月 23 日,Claude Code 2.1.310 改了一条无交互会话的错误。MCP 工具调用在服务器的征询还没人回答时超时,现在会告诉模型:没有回答到来,不要再调同一次调用。

同一天的 MCP 文档还写明:无交互会话里,任务若来要输入,Claude Code 会请服务器取消任务,不跑 Elicitation hook,也不调用 Agent SDK 的 onElicitation。把 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 task。文档写明,那种跑法得不到任务;要跟任务,需要交互恢复,或用 --input-format stream-json。征询超时仍可能落在普通工具调用上,日志不能只认 taskId。

服务器要输入时,返回的任务 JSON 有哪些必填字段?

协议修订 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 只在交互终端里处理。无交互时处理不了,就请服务器取消。

服务器返回的任务。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 的输入里再执行一遍。

本地日志要怎样写下不要再调?

协议 status 不要塞进 repeat。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" }
  }
}

schema 通过之后,为什么 repeat 为 true 仍要拒绝?

上面的 schema 只要求 repeat 是布尔值。下面这份日志字段齐全、类型也对,但无交互且 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 为 falsestop2.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 task 吗?

文档写明不会。那种跑法得不到 MCP task。要跟踪任务,用交互恢复,或 --input-format stream-json,并满足自动后台的条件。征询超时仍可能发生在普通工具调用上,所以日志里的 taskId 不是必填。

交互终端里的 input_required 也要写成 stop 吗?

不要。这条停令只覆盖无交互。交互终端可以回答 elicitation/create。文档还写明,无交互取消任务时不跑 Elicitation hook,也不调用 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 是布尔值,用 Diff 对比任务收据和本地停令。

返回博客