블로그 • 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 시간 초과 오류는 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가 있다. 서버가 작업 ID를 거부하거나 영구 오류를 낸 뒤 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. 1칸에 로그 schema, 2칸에 정지 로그 또는 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로 작업 영수증과 로컬 정지를 비교한다.