CLAUDE LABEN
MCPSPEC — MCP の 2026-07-28 仕様に対応しました。双方向ステートフルから request/response 型へ移り、サーバーをサーバーレスやエッジに置けるようになりますAUTHZ — 新仕様では OAuth と OIDC による認可が強化され、Apps と Tasks はバージョン付きの拡張として切り出されましたSDK400 — MCP の SDK ダウンロードが月間4億回を超えました。1年で約4倍という伸びですFORK — /fork で会話をそのまま新しいバックグラウンドセッションへ複製できます。手元の作業を止めずに別案を試せますMCPLOGIN — claude mcp login で MCP サーバーの認証をシェルから行えます。/mcp メニューを開かずに済み、logout で資格情報も消せますREVIEWBG — /code-review がバックグラウンドで走るようになりました。レビュー中も会話を続けられますMCPSPEC — MCP の 2026-07-28 仕様に対応しました。双方向ステートフルから request/response 型へ移り、サーバーをサーバーレスやエッジに置けるようになりますAUTHZ — 新仕様では OAuth と OIDC による認可が強化され、Apps と Tasks はバージョン付きの拡張として切り出されましたSDK400 — MCP の SDK ダウンロードが月間4億回を超えました。1年で約4倍という伸びですFORK — /fork で会話をそのまま新しいバックグラウンドセッションへ複製できます。手元の作業を止めずに別案を試せますMCPLOGIN — claude mcp login で MCP サーバーの認証をシェルから行えます。/mcp メニューを開かずに済み、logout で資格情報も消せますREVIEWBG — /code-review がバックグラウンドで走るようになりました。レビュー中も会話を続けられます
記事一覧/Claude Code
Claude Code/2026-07-29上級

MCPサーバーは繋がっているのに、ツールが0件で返る — 無人実行の権限劣化をツール表の差分で止める

資格情報が抜けてもMCPのハンドシェイクは成功し、tools/list は空配列を返します。例外が飛ばないまま実行が完走する構造と、期待ツール表との差分で無人実行を止めるpreflightの実装を、実測値つきで記録しました。

MCP49Claude Code206CI/CD19無人実行3運用設計16

プレミアム記事

無人実行のログを見ていて、手が止まった瞬間がありました。

終了コードは 0。エージェントは筋の通った要約を返している。けれど本来そこに載っているはずの、社内の集計サーバーから取った数字が、どこにも見当たりません。

エラーは1行も出ていませんでした。

原因は資格情報の欠落でした。MCPサーバーは起動し、ハンドシェイクも通り、そして提供したツールは0件。エージェントは「使えるツールがない」という前提で、手持ちの知識だけで答えを組み立てていました。破綻していないぶん、気づくのが遅れます。

この挙動は仕様として理解すれば当たり前ですが、実装を任せている側の感覚としては裏切られた気持ちになります。個人開発で夜間に回している処理では、翌朝まで誰も見ていません。

接続の成否と、ツールが生えているかは別の層にある

MCPの起動シーケンスは大きく2段です。initialize でプロトコル版と能力をすり合わせ、そのあと tools/list で実際に呼べるツールを取得します。

資格情報を見るのは、多くの実装で後者より更に奥、あるいは tools/list を組み立てる時点です。認証に失敗したサーバーが選ぶ振る舞いは、実装者の裁量に委ねられています。

  • エラーを返して接続ごと落とす
  • 接続は維持したまま、ツールを空で返す
  • ツール表は返すが、呼び出し時に初めて失敗する

このうち2番目が厄介です。プロトコル上は何も間違っていません。JSON-RPC のレスポンスは result を持ち、error は含まれません。クライアント側の例外ハンドラは何も捕まえられません。

そして Claude Code は、使えるツールが減っていても実行を止めません。止めない設計は対話中には親切ですが、無人実行では危うさに反転します。

資格情報の有無だけを変えて測る

推測で書きたくなかったので、依存ゼロの最小サーバーを立てて実際に測りました。標準入出力でJSON-RPCを話すだけの、80行ほどのPythonです。

#!/usr/bin/env python3
"""最小のstdio MCPサーバー。REPORT_API_TOKEN があるときだけツールを公開する。"""
import json, os, sys
 
TOKEN = os.environ.get("REPORT_API_TOKEN")
 
TOOLS = [
    {"name": "fetch_daily_report", "description": "Fetch the daily sales report",
     "inputSchema": {"type": "object", "properties": {"date": {"type": "string"}}, "required": ["date"]}},
    {"name": "list_report_dates", "description": "List available report dates",
     "inputSchema": {"type": "object", "properties": {}}},
]
 
def respond(rid, result):
    sys.stdout.write(json.dumps({"jsonrpc": "2.0", "id": rid, "result": result}) + "\n")
    sys.stdout.flush()
 
def main():
    for line in sys.stdin:
        line = line.strip()
        if not line:
            continue
        req = json.loads(line)
        method, rid = req.get("method"), req.get("id")
        if method == "initialize":
            # 資格情報の状態に関わらずハンドシェイクは成功する
            respond(rid, {
                "protocolVersion": "2026-07-28",
                "capabilities": {"tools": {}},
                "serverInfo": {"name": "report-server", "version": "1.0.0"},
            })
        elif method == "notifications/initialized":
            continue
        elif method == "tools/list":
            # 未認証時はエラーではなく「空配列」を返す
            respond(rid, {"tools": TOOLS if TOKEN else []})
        elif method == "shutdown":
            respond(rid, {})
            return
        elif rid is not None:
            respond(rid, {})
 
if __name__ == "__main__":
    main()

計測側は、ハンドシェイクの成否・tools/list の成否・得られたツール名を1回の起動でまとめて拾います。

#!/usr/bin/env python3
"""stdio MCPサーバーを叩いて、ハンドシェイクの結果とツール表を観測する。"""
import json, os, subprocess, sys, time
 
def probe(cmd, env):
    proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
                            stderr=subprocess.PIPE, text=True, env=env, bufsize=1)
    def call(method, params=None, rid=None):
        msg = {"jsonrpc": "2.0", "method": method}
        if params is not None:
            msg["params"] = params
        if rid is not None:
            msg["id"] = rid
        proc.stdin.write(json.dumps(msg) + "\n"); proc.stdin.flush()
        if rid is None:
            return None
        return json.loads(proc.stdout.readline())
 
    t0 = time.perf_counter()
    init = call("initialize", {"protocolVersion": "2026-07-28", "capabilities": {},
                               "clientInfo": {"name": "preflight-probe", "version": "0.1"}}, rid=1)
    call("notifications/initialized")
    listed = call("tools/list", {}, rid=2)
    elapsed_ms = (time.perf_counter() - t0) * 1000
    call("shutdown", None, rid=3)
    proc.wait(timeout=5)
 
    tools = listed.get("result", {}).get("tools", [])
    return {
        "handshake_ok": "result" in init,
        "server": init.get("result", {}).get("serverInfo", {}).get("name"),
        "tools_list_ok": "result" in listed,
        "tool_count": len(tools),
        "tool_names": sorted(t["name"] for t in tools),
        "elapsed_ms": round(elapsed_ms, 1),
    }
 
if __name__ == "__main__":
    server = [sys.executable, os.path.join(os.path.dirname(__file__), "report_server.py")]
    for label, extra in (("credential present", {"REPORT_API_TOKEN": "test-token"}),
                         ("credential missing", {})):
        env = {k: v for k, v in os.environ.items() if k != "REPORT_API_TOKEN"}
        env.update(extra)
        print(f"--- {label} ---")
        print(json.dumps(probe(server, env), ensure_ascii=False))

手元のLinux環境(Python 3.10)で走らせた出力です。

--- credential present ---
{"handshake_ok": true, "server": "report-server", "tools_list_ok": true, "tool_count": 2, "tool_names": ["fetch_daily_report", "list_report_dates"], "elapsed_ms": 19.8}
--- credential missing ---
{"handshake_ok": true, "server": "report-server", "tools_list_ok": true, "tool_count": 0, "tool_names": [], "elapsed_ms": 18.6}

事前の予想と逆だった点が、ここに出ています。

私は「認証が通らなければハンドシェイクの段階で異常が観測できる」と思い込んでいました。実際には handshake_oktools_list_ok も両方 true のまま、違いは tool_count の 2 と 0 だけ。所要時間も 19.8 ミリ秒と 18.6 ミリ秒で、有意な差はありません。

失敗が「遅くなる」でも「例外になる」でもなく、「静かに数が減る」形で現れます。監視対象を接続性に置いていると、この差はどこにも記録されません。

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
資格情報なしでも initialize と tools/list が成功する挙動を、素のstdioサーバーで実測(ツール数 2 → 0・所要19ミリ秒前後)
期待ツール表をロックファイルに固定し、差分ゼロでなければ終了コード78で落とす約120行のpreflight実装
preflight自体がクラッシュした原因と修正 — Popen を try の外に置くと設定ミスが実行基盤の障害に化ける
Stripe による安全な決済 · いつでもキャンセル可能

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Claude Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

Claude Code2026-07-18
宣言したMCPサーバーが一覧から消えていた — 名前空間をベンダーと共有する前提で設定を組み直す
MCPサーバー名がベンダーに予約され、自分の .mcp.json の宣言が黙って無効になる。無人実行で気づけないこの欠落を、宣言と実体を突き合わせる preflight と接頭辞規約で落とす設計をまとめました。
Claude Code2026-07-16
夜間セッションの常駐メモリが朝には数GB — 蓄積の四つの出どころと、RSS を刻んで切り分ける
夜間に回した Claude Code のプロセスを朝に見ると、常駐メモリが数GBまで膨らんでいました。2.1.209 で塞がれた四つの蓄積源と、自分の構成に残る分を RSS の傾きで切り分ける方法、しきい値で畳む運用までを個人開発の目線で整理します。
Claude Code2026-07-27
許可リストに書いた環境変数は誰の環境で展開されるのか — Managed MCP の実効ポリシーを指紋で確かめる
Managed MCP の allowlist・denylist に書いた環境変数参照の解決元が、起動時環境と managed-settings の env に変わりました。同じ設定ファイルが環境ごとに別のポリシーになる仕組みを手元で再現し、実効ポリシーを指紋で突き合わせる起動前チェックを組みました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →