博客 • 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 为 input_required 时,文档要求取消任务,不跑 Elicitation hook。
- 本地日志用 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 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 只在交互终端里处理。无交互时处理不了,就请服务器取消。
{
"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 可以没有,因为普通工具调用不一定是任务。
{
"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" }
}
}
schema 通过之后,为什么 repeat 为 true 仍要拒绝?
上面的 schema 只要求 repeat 是布尔值。下面这份日志字段齐全、类型也对,但无交互且 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 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 对比任务收据和本地停令。