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

返回博客