CLAUDE LABEN
MCP — 2026-07-28 仕様への対応が Claude 製品群で広がっています。双方向ステートフルなプロトコルからリクエスト・レスポンス型へ移り、MCP サーバーをサーバーレスやエッジに置けるようになりましたEXTENSIONS — 公式拡張が3種そろいました。サーバー側で UI を描く MCP Apps、非同期の長時間処理を扱う Tasks、IdP 経由で組織単位に払い出す Enterprise Managed Auth ですADOPTION — MCP の月間 SDK ダウンロードが4億を超え、年内で約4倍になりました。エージェントとアプリケーションを繋ぐ標準としての位置づけが固まりつつありますQUOTA — Claude Code 契約者向けの週次利用枠 50% 上乗せは本日8月19日が最終日です。長時間のエージェント実行を回すなら今日が最後の機会になりますPRICING — Claude Sonnet 5 の導入価格 100万トークンあたり入力2ドル・出力10ドルは8月31日で終了し、9月1日から入力3ドル・出力15ドルへ移ります。残り12日ですFIX — タイムアウトが固定されたサーバーに対し MCP v2 接続がサブスクリプションを際限なく張り直す不具合が修正され、ユーザー帰属のための forward_user_identity 設定も追加されましたMCP — 2026-07-28 仕様への対応が Claude 製品群で広がっています。双方向ステートフルなプロトコルからリクエスト・レスポンス型へ移り、MCP サーバーをサーバーレスやエッジに置けるようになりましたEXTENSIONS — 公式拡張が3種そろいました。サーバー側で UI を描く MCP Apps、非同期の長時間処理を扱う Tasks、IdP 経由で組織単位に払い出す Enterprise Managed Auth ですADOPTION — MCP の月間 SDK ダウンロードが4億を超え、年内で約4倍になりました。エージェントとアプリケーションを繋ぐ標準としての位置づけが固まりつつありますQUOTA — Claude Code 契約者向けの週次利用枠 50% 上乗せは本日8月19日が最終日です。長時間のエージェント実行を回すなら今日が最後の機会になりますPRICING — Claude Sonnet 5 の導入価格 100万トークンあたり入力2ドル・出力10ドルは8月31日で終了し、9月1日から入力3ドル・出力15ドルへ移ります。残り12日ですFIX — タイムアウトが固定されたサーバーに対し MCP v2 接続がサブスクリプションを際限なく張り直す不具合が修正され、ユーザー帰属のための forward_user_identity 設定も追加されました
記事一覧/API & SDK
API & SDK/2026-08-19中級

サーバーレスに置いた MCP サーバーで、listen ストリームが張り直され続けていました

エラーは何も出ていないのに、MCP サーバーへの接続だけが増え続ける。固定タイムアウトで切られるストリームと再購読が噛み合ったときの挙動を、最小の再現コードで確かめました。

MCP51Claude Code226サーバーレス3SSE6トラブルシューティング39

朝いちばんにアクセスログを開いて、まず数字の桁を疑いました。前の晩に自分がつないだ MCP サーバーへのリクエストが、想定していた回数の何十倍にもなっていたからです。

エラーは一件も出ていません。ツールの呼び出しは全部成功しています。増えていたのは /subscriptions/listen — 通知を受け取るために張りっぱなしにしておく、あの一本だけでした。

個人開発で運用しているサイト群は Cloudflare Workers の上にあって、長時間の接続が一定時間で打ち切られる性質とは以前から付き合ってきました。それでも、切られること自体が問題になるとは考えていませんでした。実際に困るのは、切られたあとの振る舞いのほうです。

切断エラーは出ないのに、接続だけが増えていく

この症状の厄介なところは、どの層を見ても「失敗」として記録されないことです。

サーバーレスの実行環境には、ひとつのリクエストを保持できる時間に上限があります。Cloudflare Workers、Lambda の Function URL、Cloud Run のどれでも、値は違えど上限はあります。長時間つなぎっぱなしにする listen ストリームは、その上限に必ずぶつかります。ぶつかったとき、プラットフォームは 500 を返すのではなく、静かにストリームを終わらせます。

クライアント側から見れば、これは「正常に終わった読み取り」か、あるいは「無言のまま何も届かない状態」です。前者なら再購読は自然な振る舞いですし、後者でも一定時間で見切りをつけて張り直すのが妥当です。どちらの判断も、単体では間違っていません。

問題は、その張り直しが古い購読を畳まないまま行われたときに起きます。サーバー側の同時接続数だけが単調に増えていき、しかもどこにも例外が記録されません。私自身、最初はリクエスト数の集計ミスを疑って、そちらを先に調べてしまいました。

Claude Code 側でも、この噛み合わせは実際に不具合として扱われています。リリースノートには「固定タイムアウトで長時間ストリームを打ち切るサーバー(サーバーレスホストなど)に対して、MCP v2 の接続が subscriptions/listen ストリームを際限なく開き直す問題を修正」という趣旨の記述があります。つまり、自分のサーバーの設計が原因である場合と、クライアント側のバージョンが原因である場合の両方がありうるということです。

固定タイムアウトと再購読が噛み合うと何が起きるか

言葉で説明するより、動かしたほうが早いと考えました。実機の 30〜100 秒という時間軸をそのまま再現すると検証に何十分もかかるので、比率を保ったまま縮めた相似モデルを組んでいます。

サーバーは「受理して、しばらくしたら黙る。ただし FIN は返さない」という、いちばん質の悪い切られ方を再現します。クライアントは、無音が続いたら切れたとみなして張り直します。

# repro.py — 固定タイムアウトで黙るサーバーと、それに再購読するクライアント
import socket, threading, time, json
 
SILENT_AFTER = 1.0        # 実機の固定タイムアウトを縮めた相似値
open_conns = 0
lock = threading.Lock()
samples = []
 
def server(port, stop):
    s = socket.socket()
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind(("127.0.0.1", port)); s.listen(128); s.settimeout(0.2)
 
    def handle(c):
        global open_conns
        with lock:
            open_conns += 1
        try:
            c.recv(1024)
            c.sendall(b"HTTP/1.1 200 OK\r\n"
                      b"Content-Type: text/event-stream\r\n\r\n: open\n\n")
            time.sleep(SILENT_AFTER)      # ここから無言。FIN は送らない
            while not stop.is_set():      # クライアントが閉じるまで保持し続ける
                try:
                    c.settimeout(0.2)
                    if c.recv(1) == b"":
                        break
                except socket.timeout:
                    continue
                except Exception:
                    break
        finally:
            with lock:
                open_conns -= 1
            c.close()
 
    while not stop.is_set():
        try:
            c, _ = s.accept()
        except socket.timeout:
            continue
        threading.Thread(target=handle, args=(c,), daemon=True).start()
    s.close()
 
def client(port, stop, close_old):
    socks = []
    while not stop.is_set():
        try:
            c = socket.create_connection(("127.0.0.1", port), timeout=1)
            c.sendall(b"GET /subscriptions/listen HTTP/1.1\r\nHost: x\r\n\r\n")
            c.settimeout(1.2)             # 無音が続いたら切れたとみなす
            socks.append(c)
            while True:
                if not c.recv(1024):
                    break
        except socket.timeout:
            if close_old:                 # 修正版: 古い購読を畳んでから張り直す
                for s0 in socks:
                    s0.close()
                socks = []
                time.sleep(0.5)           # バックオフ
            # close_old=False は古い接続を放置したまま次を張る
        except Exception:
            time.sleep(0.2)

サーバー側で毎秒サンプリングした同時接続数は、次のようになりました。

経過秒12345678
古い購読を放置01234556
畳んでから張り直す01101011

放置した側は、タイムアウトの周期ごとにおおむね1本ずつ積み上がっていきます。畳んだ側は 0 か 1 を行き来するだけで、上限を持ちません。この差はサンドボックス上の相似モデルで得た値なので、実機の絶対値としては読まないでください。読み取るべきは形のほうです。片方は時間に比例して増え、もう片方は増えない。

実機の固定タイムアウトが仮に 60 秒なら、増えるほうは 1 時間で 60 本、朝までに数百本になります。無料枠の同時接続数や、サーバーレスの課金単位である実行時間に、この本数がそのまま効いてきます。

そして繰り返しになりますが、この間に例外は一度も飛びません。私がリクエスト数の桁を疑ったのは、疑うべき異常値がそこにしか現れていなかったからです。

自分の環境がどちらなのかを切り分ける

原因はクライアント側かサーバー側か、あるいは両方か。順番を決めておくと迷いません。

まず CLI のバージョンを見ます。前述の修正が入ったあとのバージョンであれば、クライアント起因の可能性はひとまず下がります。

claude --version
claude mcp list          # 接続先と現在の状態を確認

次に、サーバー側で「同時に開いている listen ストリームの本数」を数えます。リクエスト総数ではなく、同時本数であることが肝心です。総数は正常な再接続でも増えるので、異常の判定には使えません。

# 自前サーバーのアクセスログから listen だけを抜き、分ごとの本数を数える
awk '$7 ~ /subscriptions\/listen/ {print substr($4, 2, 17)}' access.log \
  | sort | uniq -c | tail -30

分ごとの本数が横ばいなら、再購読は正常に回っています。単調に増えていれば、古い購読が畳まれていません。

観測されたこと疑う場所次の一手
同時本数が時間とともに増え続けるクライアントの再購読、または古い接続の後始末CLI を更新して同じ時間帯で再計測する
CLI 更新後も増え続けるサーバー側の接続管理購読の重複排除と上限を入れる(次節)
本数は横ばいだが再接続が頻繁プラットフォームの固定タイムアウト異常ではない。バックオフと再送設計を確認する
ツール呼び出し側だけが失敗するlisten ではなくリクエストのタイムアウトタイムアウト設定の実効値を確認する

最後の行は別の症状です。設定したはずのタイムアウトが効かず一定時間で切れる話は、サーバー単位の request_timeout_ms を取り戻す記事のほうで扱っています。突然つながらなくなる系統の切り分けは、5ステップ診断のほうが近道です。

サーバー側で「張り直しても増えない」形にする

クライアントの更新で直る場合もありますが、サーバー側を直しておくと、どのクライアントが来ても壊れません。個人で運用するなら、こちらのほうが後々らくだと私は考えています。

要点は3つあります。

1. 購読はセッション単位で1本に制限する。 同じセッションから2本目の listen が来たら、古いほうを閉じてから新しいほうを受けます。これだけで単調増加は止まります。

// Cloudflare Workers 想定の擬似コード
const streams = new Map<string, WritableStreamDefaultWriter>();
 
async function onListen(sessionId: string, writer: WritableStreamDefaultWriter) {
  const prev = streams.get(sessionId);
  if (prev) {
    // 同一セッションの古い購読は、新しいものを受ける前に必ず閉じる
    try { await prev.close(); } catch { /* 既に閉じている場合は無視 */ }
  }
  streams.set(sessionId, writer);
}

2. 切られる時刻を、こちらから先に伝える。 プラットフォームに打ち切られるのを待たず、上限より少し手前で自分から終端します。あわせて SSE の retry フィールドで、次に張り直すまでの待ち時間を指定します。クライアントが即時再接続するかどうかを、サーバー側から誘導できます。

const MAX_HOLD_MS = 50_000;   // プラットフォーム上限より手前で自分から閉じる
const RETRY_MS    = 3_000;    // 次の接続までクライアントに待ってもらう
 
await writer.write(encoder.encode(`retry: ${RETRY_MS}\n\n`));
setTimeout(() => writer.close(), MAX_HOLD_MS);

自分で閉じると、クライアントには「正常な終端」として届きます。無言のまま放置されるより、はるかに扱いやすい形です。

3. 無音を作らない。 15 秒程度の間隔でコメント行だけを送っておくと、中継のプロキシに idle と判断されて切られる事故が減ります。SSE のコメント行は : で始まり、クライアントからはイベントとして見えません。

setInterval(() => writer.write(encoder.encode(": keepalive\n\n")), 15_000);

この3つを入れたあと、私は同じ再現コードをもう一度回しました。サーバー側から終端して retry を返す構成では、クライアントが古い購読を畳まない実装であっても、同時本数は上限に張り付かず一定の範囲に収まります。クライアントの行儀のよさに依存しない形にできたのが、いちばんの収穫でした。

なお、そもそも接続に状態を持たせない設計に寄せるという方向もあります。そちらは状態の置き場所を作り直す記事で扱っています。

最初にやる一手

自前の MCP サーバーを持っているなら、今日のうちに同時接続本数の時系列をひとつ取ってみてください。総数ではなく同時本数です。横ばいなら何もする必要はありませんし、右肩上がりの直線が見えたら、それが探していた異常そのものです。

私の場合、この一本のグラフを見るまでに二晩ぶんの請求を余計に払いました。先に見ておけば済んだ話です。

無人で動かす定期実行に MCP を組み込む場合の設計や、失敗を検知する仕組みについては、メンバーシップ向けの記事でもう少し踏み込んで扱っています。実装の参考になれば幸いです。お読みいただきありがとうございました。

シェア

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

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

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

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

関連記事

API & SDK2026-08-15
長考の途中で切れる接続は、上流ではなく中継が作っています
自作プロキシや Worker を挟んだ途端、Claude Code が長い思考の最中に切断される。原因は上流でもモデルでもなく、中継がバイトの流れを止めていることにあります。6通りの中継を並べて切り分け、本番に入れる前に確かめる手順まで整理しました。
API & SDK2026-07-26
Mcp-Session-Id を前提にしない MCP サーバーへ — 状態の置き場所を作り直す
MCP仕様の2026-07-28リリース候補でMcp-Session-Idヘッダーの廃止が示されました。自作リモートMCPサーバーのセッション結合を監査スクリプトで数え、状態を三つに仕分けて署名付きカーソルへ移行するまでの実測記録です。移行の順番と旧方式の外し方まで具体的に書いています。
Claude Code2026-05-24
Claude Code「Tool result could not be submitted」を冷静に切り抜ける手順
Claude Code で長いツール返答や MCP 応答待ちのときに突然出る『Tool result could not be submitted』。原因の本当のところと、私が個人開発の現場で実際に使っている復旧手順をまとめました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →