◉CLAUDE LABEN
●CODE 2.1.325 — zstd 圧縮応答の復号に関するセキュリティ修正が入りました(11/13)●CODE MODE — 2.1.323 で、Claude が JavaScript でツールを呼ぶ実験的な Code Mode が加わりました●SONNET 4.5 — Claude API での提供終了は 11/30、残り16日。移行先は Sonnet 5.5●STATUS — 2.1.323 で subagentStatusLine が消え、サブエージェントごとの費用が見えないという声が出ています●MANAGED — Claude Managed Agents を自前で動かせる self-hosted 版が公開されています(11/12)●NEW — Cowork が作った表の合計を元データと突き合わせる●CODE 2.1.325 — zstd 圧縮応答の復号に関するセキュリティ修正が入りました(11/13)●CODE MODE — 2.1.323 で、Claude が JavaScript でツールを呼ぶ実験的な Code Mode が加わりました●SONNET 4.5 — Claude API での提供終了は 11/30、残り16日。移行先は Sonnet 5.5●STATUS — 2.1.323 で subagentStatusLine が消え、サブエージェントごとの費用が見えないという声が出ています●MANAGED — Claude Managed Agents を自前で動かせる self-hosted 版が公開されています(11/12)●NEW — Cowork が作った表の合計を元データと突き合わせる
記事一覧/Cowork
◈ Cowork/2026-11-13中級

Desktop の更新後に MCP Apps の画面が出なくなったら — stdio の間に記録係を挟んで、誰が何を頼んだかを数える

Claude Desktop 4.23118.0 でローカル MCP の画面(MCP Apps)が出ない報告を受けて、stdio サーバーの手前に挟む記録用スクリプトを作りました。resources/read が来ているか、どの接続がツールを呼んだかを自分の手元で確かめられます。

Claude Desktop4MCP61MCP Appsトラブルシューティング70Python31

Issue を読んでいた夜、報告者の書き込みが三日のうちに二度、自分の見立てを訂正していく様子に手が止まりました。最初は「この版で壊れた」、次に「このログ行は前からあった」、そして「1 ターン目だけ出ない」。原因の候補が一つずつ消えていく、その順番がとても誠実だったのです。

話題は Issue #108122 です。macOS の Claude Desktop が 4.16336.0 から 4.23118.0 に自動更新されたあと、claude_desktop_config.json に書いたローカルの stdio サーバーの MCP Apps(ツール結果と一緒に出る小さな画面)が Chat に描かれなくなった、という報告です。ツールは呼ばれ、テキストの結果も返ります。出ないのは画面だけでした。

最初にお伝えしたいのは、この種の「出ない」は、画面を眺めていても切り分けが進まないということです。進んだのは、サーバーとクライアントのあいだで何が行き来したかを、報告者が一行ずつ数え始めてからでした。ここでは、その数え方を自分の手元で再現できるよう、どのサーバーにも挟める小さな記録係を書き残します。

先に断っておくこと

内容は公開されている Issue の報告とコメントに基づいています。2026 年 11 月 13 日の時点で Issue は open のままで、公式からの回答は見当たりません。私自身は Desktop 上で同じ症状を再現しておらず、下の記録係は、手元の端末でダミーのサーバーを相手に動作を確かめたところまでです。

報告の流れを短くまとめると、次のとおりです。

  • 11 月 7 日の報告: 4.23118.0 では、画面用の ui:// リソースを取りに行く resources/read が一度も送られません
  • 11 月 8 日の訂正: 疑われた「Era probe verdict: legacy」のログ行は、正常に描けていた旧版でも毎回出ていました
  • 11 月 10 日の発見: 新しいチャットの 1 ターン目だけ描かれず、次のターンで同じツールを呼ぶと resources/read が届いて描かれます
  • 11 月 11 日: 別の利用者も「会話を始めたあとなら出る。最初の依頼だと出ない」と同じ傾向を報告しています

一方で、途中には「自分たちの場合は、ツール定義の側に _meta.ui.resourceUri を付け忘れていただけだった」というコメントもありました。同じ見た目の症状に、原因が二つ混ざっているのです。

何を数えれば、原因の持ち主が分かるのか

Desktop の ~/Library/Logs/Claude/mcp-server-<名前>.log にも通信の記録は残ります。ただ、報告の中で分かったのは、ツールを実際に呼んでいるのは、このログに出ていない二本目の接続だということでした。サーバー側の標準エラーにだけ、local-agent-mode-<名前> と名乗るクライアントからの initialize と tools/call が現れていたそうです。

つまり Desktop 側のログだけを見ていると、ツールの呼び出しそのものが見えません。サーバーの手前で、全部の接続を同じ形式で記録する必要があります。確かめたいのは次の四つです。

見たいこと記録のどこで分かるか当てはまったときの持ち主
ツール定義に画面の URI が付いているかtools/list の返事の with_ui空ならサーバー側
どの接続がツールを呼んだかtools/call の行の client切り分けの材料
呼び出しの結果に _meta が載っているかtools/call の返事の meta無ければサーバー側
画面を取りに来たかresources/read の行の有無定義も結果も正しいのに無ければクライアント側

stdio の間に挟む記録係を作る

stdio の MCP は、1 行に 1 つの JSON-RPC メッセージを流します。ですから、本来のサーバーを子プロセスとして起動し、行を素通しさせながら写しを取れば、サーバーのコードには一切手を入れずに済みます。Python の mcp SDK でも Node のサーバーでも、同じものが使えます。

#!/usr/bin/env python3
"""stdio MCP サーバーの手前に挟み、行き来した JSON-RPC を1行ずつ記録する。
使い方: python3 mcp_tap.py --log ~/mcp_tap.log -- <本来のコマンド> [引数...]
"""
import json, os, subprocess, sys, threading, time
 
args = sys.argv[1:]
log_path = os.path.expanduser("~/mcp_tap.log")
if args[:1] == ["--log"]:
    log_path, args = os.path.expanduser(args[1]), args[2:]
if args[:1] == ["--"]:
    args = args[1:]
if not args:
    sys.exit("usage: mcp_tap.py [--log PATH] -- command [args...]")
 
# stdout は MCP の通信路なので、記録は必ずファイルへ(print は使わない)
child = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
lock = threading.Lock()
pending = {}            # 問いの id -> method(返事を引き当てるため)
client = {"name": "?"}  # initialize で名乗ったクライアント名
 
def describe(direction, msg):
    if direction == "->":
        method = msg.get("method", "-")
        if "id" in msg:
            pending[msg["id"]] = method   # 子へ渡す前に登録する(返事との競争を避ける)
        if method == "initialize":
            client["name"] = msg.get("params", {}).get("clientInfo", {}).get("name", "?")
        if method == "resources/read":
            return method, f"uri={msg.get('params', {}).get('uri')}"
        return method, ""
    method = pending.pop(msg.get("id"), "-")
    result = msg.get("result") or {}
    if method == "tools/list":
        tools = result.get("tools", [])
        ui = [t["name"] for t in tools if (t.get("_meta") or {}).get("ui", {}).get("resourceUri")]
        return method, f"tools={len(tools)} with_ui={ui} next={bool(result.get('nextCursor'))}"
    if method == "tools/call":
        return method, f"meta={sorted((result.get('_meta') or {}))} error={'error' in msg}"
    return method, ""
 
def log(direction, msg, note):
    stamp = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime())
    with lock, open(log_path, "a", encoding="utf-8") as f:
        f.write(f"{stamp}Z pid={os.getpid()} client={client['name']} "
                f"{direction} {msg[0]} id={msg[1]} {note}\n")
 
def pump(src, dst, direction):
    for raw in iter(src.readline, b""):
        try:
            msg = json.loads(raw)
            method, note = describe(direction, msg)
            ident = msg.get("id", "-")
        except ValueError:
            method, note, ident = "non-json", raw[:60], "-"
        dst.write(raw)
        dst.flush()
        log(direction, (method, ident), note)
    if direction == "->":
        child.stdin.close()
 
threading.Thread(target=pump, args=(sys.stdin.buffer, child.stdin, "->"), daemon=True).start()
pump(child.stdout, sys.stdout.buffer, "<-")
sys.exit(child.wait())

なぜこう書くのか、要点は三つです。

  • 記録はファイルだけに書きます。stdout に一文字でも余計なものが混ざると、それだけで接続が壊れます
  • 問いの id を、子に渡す前に控えます。先に渡すと、返事のほうが早く戻ってきて「何への返事か」を引けない瞬間が生まれえます
  • 接続ごとに pid と client 名を残します。Desktop は接続のたびに別のプロセスを起動するので、pid が違えば別の接続だと分かります

最初の版では、問いの登録を「子へ書き込んだあと」に置いていました。手元でダミーのサーバーを相手に流したところ、記録は正しく並んだものの、順序に頼った書き方だと気づいてすぐ入れ替えました。数ミリ秒の差で記録が欠ける道具は、肝心な夜に限って欠けるものです。

Desktop に挟んで、1 ターン目と 2 ターン目を比べる

claude_desktop_config.json の該当サーバーを、記録係を通す形に書き換えます。元の command と args を -- の後ろへそのまま移すだけです。

{
  "mcpServers": {
    "my-local-app": {
      "command": "/usr/bin/python3",
      "args": [
        "/Users/you/mcp/mcp_tap.py",
        "--log", "/Users/you/mcp_tap.log",
        "--",
        "/usr/local/bin/node", "/Users/you/mcp/server.js"
      ]
    }
  }
}

Desktop は端末とは違う PATH で起動するので、python3 も node もフルパスで書いておくほうが確実です。保存したら Desktop を完全に終了して開き直し、次の順で試します。

  1. フォルダを付けない新しいチャットで、画面付きのツールを使う依頼を一度だけ送ります
  2. 画面が出なければ、同じチャットで「もう一度開いてください」と送ります
  3. 記録を読みます
# 接続ごとの名乗りと、ツール呼び出し・画面の取得だけを時刻順に並べる
grep -E ' -> (initialize|tools/call|resources/read) ' ~/mcp_tap.log | awk '{print $1, $2, $3, $5, $7}'
# 定義の側に画面の URI が付いているか
grep ' <- tools/list ' ~/mcp_tap.log | tail -3

報告と同じ状態なら、1 回目の tools/call のあとには何も続かず、2 回目の tools/call の直後に resources/read が現れるはずです。with_ui=[] と出たなら、Desktop を疑う前にサーバーのツール定義を見直す番です。

記録を読んだあと、どこで手を止めるか

私は、記録が示した場所以外には手を出さないようにしています。見えないものは、まず数えられる形に。 いま思えば、数える前に設定を書き換え始めると、何が効いたのか分からないまま症状だけが動いてしまうからです。

  • 定義に URI が無い: サーバーのツール定義を直します。next=True なら続きのページも確かめます
  • 定義も結果も正しく、1 ターン目だけ resources/read が無い: Issue の報告と同じ形。2 ターン目で呼び直す運用でしのぎ、記録を添えて Issue に情報を足します
  • tools/call の client が見慣れない名前: 二本目の接続です。その接続の initialize 行も記録に残っているはずなので、並べて見比べます

関連して、プラグイン経由のサーバーだけ失敗する別の型は「プラグイン同梱の MCP だけ認証に失敗するときの切り分け」に、起動直後の一覧をどこまで信じるかは「起動直後の MCP 一覧を信用しなくなった理由」に書いております。

個人開発で小さな道具を Desktop につなぐ機会が増えるほど、「出ない」の原因は自分の側にもクライアントの側にも散らばるのだと感じています。まずは手元のサーバー一つに記録係を挟み、新しいチャットで二度呼んで、記録の行数を数えるところから始めていただければ幸いです。

シェア

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

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

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

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

◈ Cowork2026-11-02
プラグイン同梱の MCP だけ認証に失敗する — ${VAR} が展開されないときの切り分け
Cowork でプラグイン同梱の MCP サーバーだけ認証に失敗するとき、環境変数の ${VAR} が展開されないまま渡されている可能性があります。Issue #94395 の報告をもとに、ログでの確認と配布前の洗い出しスクリプトをまとめました。
◈ Cowork2026-10-29
共有カードの下端が切れていた OGP 画像を、宣言ではなくピクセルで検査して止めるまで
直したはずの OGP 画像が 1200×1200 に戻り、SNS の横長カードで下のサイト名とキャッチが切れていました。メタタグの宣言と実ファイルの寸法を突き合わせる検査、切り取りで失う帯の走査、クローラーを模した疎通確認を、ビルド前に並べた記録です。
◈ Cowork2026-10-29
サブフォルダの CLAUDE.md の後半だけが効かないとき、数えるのは容量ではなく文字数でした
Claude Code 2.1.309 から、サブフォルダの CLAUDE.md と path 指定のルールには文字数の上限がかかり、超えた分は先頭しか渡されません。手元のファイルで測った数え方の落とし穴と、走査スクリプト、コミット前の守り、rules への分け方をまとめた記録です。