博客 • 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 對比任務收據和本地停令。