自宅の Mac で、夜のうちに claude -p を回している方には、思い当たる朝があるかもしれません。ログを開くと、MCP サーバーが「needs authentication」と出ていたり、タイムアウトの扱いのまま最後まで失敗が続いていたりします。前の晩に何かを変えた覚えはありません。
Claude Code 2.1.307 の更新履歴には、まさにこの状況に当たる修正が2つありました。
| 症状 | 2.1.307 での扱い(更新履歴の記述) |
|---|---|
| macOS でログインキーチェーンがロックされている間、`claude -p` で MCP が「needs authentication」と表示される | 理由を表示するようになり、アンロック後の次の実行では再サインインなしで接続される |
| macOS または Linux が MCP の接続中に sleep すると、タイムアウト扱いになり、ヘッドレス実行では失敗したままになる | 修正済み |
直るのであれば、アップデートして終わりです。ただ、私の場合は少し違う考え方をしました。直った後でも、夜間実行の前に「いま MCP が使えるか」を確かめる習慣は残したほうが、朝の損失が小さいと判断したのです。ここでは、その事前確認と、失敗が起きた朝に原因を切り分ける手順を書き残します。
まず、失敗を3つに分けて考えます
朝に見る失敗は、見た目が似ていても原因が違います。
- sleep 由来: 接続している最中に Mac が眠った。復帰後も失敗状態が残る
- keychain 由来: ログインキーチェーンがロックされていて、保存済みの認証情報を読めない
- そもそも認証切れ: サーバー側でトークンが失効した。これは待っても直りません
最初の2つは「待てば、または環境を整えれば自然に直る」種類で、3つ目は人が再サインインする必要があります。夜間実行の前に確認する目的は、この区別を実行前に付けて、直る失敗なら1回だけ待ち、直らない失敗なら最初から見送ることです。
事前確認スクリプト — claude mcp list を1行の JSON にします
claude mcp list の出力を読み、サーバーごとに接続済みか、認証待ちか、失敗かを分類します。終了コードで結果を返すので、シェルから扱えます。
#!/usr/bin/env python3
"""claude mcp list の出力を読み、夜間実行の前に MCP の状態を1行で返す。
終了コード: 0=全部接続 / 3=認証待ち・失敗・読み取り不能 / 4=claude が実行できない
"""
import re, subprocess, sys, json
STATE_PATTERNS = [
("connected", re.compile(r"Connected", re.I)),
("needs_auth", re.compile(r"Needs authentication", re.I)),
("failed", re.compile(r"Failed to connect|timed out|timeout", re.I)),
]
def classify(line: str) -> str:
for name, pat in STATE_PATTERNS:
if pat.search(line):
return name
return "unknown"
def parse(text: str) -> dict:
servers = {}
for raw in text.splitlines():
line = raw.strip()
# 例: "github: npx -y @x/server - ✓ Connected"。サーバー名は最初の ": " まで
m = re.match(r"^([\w.\-]+):\s+(.*)$", line)
if not m:
continue
servers[m.group(1)] = classify(m.group(2))
return servers
def main() -> int:
try:
out = subprocess.run(["claude", "mcp", "list"], capture_output=True,
text=True, timeout=90).stdout
except (FileNotFoundError, subprocess.TimeoutExpired) as e:
print(json.dumps({"preflight": "no_claude", "error": type(e).__name__}))
return 4
servers = parse(out)
if not servers: # 0 件は「全部接続」ではなく「読めていない」
print(json.dumps({"preflight": "empty_list", "raw_head": out[:120]}, ensure_ascii=False))
return 3
bad = {k: v for k, v in servers.items() if v != "connected"}
print(json.dumps({"preflight": "ok" if not bad else "degraded",
"total": len(servers), "bad": bad}, ensure_ascii=False))
return 0 if not bad else 3
if __name__ == "__main__":
sys.exit(main())claude mcp list の出力は、手元で確認できた範囲では「サーバー名: 起動コマンド - 状態」の形でした。バージョンによって記号や文言が変わる可能性があるため、正規表現は状態を表す語だけに絞っています。お使いの環境では、先に claude mcp list を一度実行して、行の形を確かめてからお使いください。
書いている途中で気づいた、直感に反する落とし穴
最初の版は、bad が空なら ok を返していました。ところが、claude mcp list が何も出力しない環境で実行すると、サーバー0件のまま ok と判定して終了コード 0 を返しました。
「問題のあるサーバーがない」と「サーバーが1つも読めていない」は別の状態です。0件のとき ok を返す事前確認は、いちばん役に立ってほしい場面(コマンド自体が期待どおりに動いていないとき)で、黙って通してしまいます。上のコードの empty_list の分岐は、この見落としを直したものです。MCP を使っていない構成の方は、この分岐を「0件は正常」に変えてください。