MCPのsubscriptions/listenとは — 購読ストリームの新方式
MCPのsubscriptions/listenは、resources/subscribeとHTTP GETエンドポイントを置き換える新しい購読方式です。仕組みと確認応答の流れを解説します。
なぜresources/subscribeが置き換えられたのか
MCPの2026-07-28リビジョンは、サーバーからクライアントへ変更を通知する仕組みを1本化しました(MCPの全体像はMCPとはを参照してください)。それまであった resources/subscribe / resources/unsubscribe というRPCと、Streamable HTTPのGETエンドポイントの両方を廃止し、代わりに subscriptions/listen という単一のメソッドに統合しています。
背景にあるのは、プロトコル全体をステートレスにする方針です。同じリビジョンで initialize ハンドシェイクとセッションID(Mcp-Session-Id ヘッダー)が廃止され、プロトコルバージョンとクライアントの能力は、リクエストごとの _meta に載る形になりました。通知の受け口だけが古いGETエンドポイント方式のまま残っていると、この設計と整合しません。
以前の resources/subscribe は、更新を知りたいリソースごとに個別のRPCを呼ぶ設計でした。ツール一覧やプロンプト一覧の変更はそれとは別にHTTPのGETで待ち受けており、通知の種類ごとに窓口が分かれていました。subscriptions/listen はこの窓口を、POSTに対する長時間のレスポンスストリーム1本にまとめます。どの種類の通知がほしいかは、リクエストのパラメータで渡します。購読の開始・確認・終了が、1つのリクエストIDを軸に追えるようになりました。
常時ストリームを張らずに主要な状態変化だけ受け取りたい場合、Managed Agentsには署名付きのwebhookという代替の受け口もあります。
subscriptions/listenとは何か
subscriptions/listen は、サーバーからクライアントへの長時間ストリームを開くリクエストです。1回限りのリクエストと違い、クライアントが自分で終了させるまで開いたままになり、その間ずっと通知を届け続けます。
クライアントは、受け取りたい通知の種類を notifications フィルタで指定してリクエストを送ります。仕様に載っているリクエストの例は次のとおりです。
{
"jsonrpc": "2.0",
"id": 1,
"method": "subscriptions/listen",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": { "name": "ExampleClient", "version": "1.0.0" },
"io.modelcontextprotocol/clientCapabilities": {}
},
"notifications": {
"toolsListChanged": true,
"resourceSubscriptions": ["file:///project/config.json"]
}
}
}フィルタのフィールドは4つです。
| フィールド | 型 | 受け取る通知 |
|---|---|---|
toolsListChanged | 型boolean | 受け取る通知ツール一覧が変わったとき(notifications/tools/list_changed) |
promptsListChanged | 型boolean | 受け取る通知プロンプト一覧が変わったとき |
resourcesListChanged | 型boolean | 受け取る通知リソース一覧が変わったとき |
resourceSubscriptions | 型string[] | 受け取る通知指定したリソースURIの更新(notifications/resources/updated) |
すべて任意で、指定しなければその種類は購読しない扱いです。サーバーは、クライアントが明示的に要求していない種類の通知を送ってはいけません。
購読の一生は4つの動きでできている
購読は、開く・確認する・通知を受ける・終わる、の順に進みます。それぞれで守るべき決まりが違います。
subscriptions/listenの流れ
- 1
クライアントが開く
subscriptions/listenを送ります。このリクエストのJSON-RPC IDが、そのまま購読IDになります。 - 2
サーバーが確認応答を返す
最初のメッセージは必ず
notifications/subscriptions/acknowledgedです。サーバーが受け入れたフィルタだけがnotificationsに残り、非対応の種類は省かれます。 - 3
通知が流れる
すべての通知に
io.modelcontextprotocol/subscriptionIdが_metaで付きます。値は開いたリクエストのIDです。 - 4
どちらかが終わらせる
クライアントが閉じる、サーバーが完了レスポンスを返して閉じる、トランスポートが切れる、のいずれかで終わります。
確認応答は最初の1通で、フィルタの突き合わせはクライアントの仕事
確認応答より前に、その購読へ他の通知を送ることは禁止されています。仕様に載っている確認応答の例は次のとおりです。
{
"jsonrpc": "2.0",
"method": "notifications/subscriptions/acknowledged",
"params": {
"_meta": { "io.modelcontextprotocol/subscriptionId": 1 },
"notifications": {
"toolsListChanged": true,
"resourceSubscriptions": ["file:///project/config.json"]
}
}
}クライアントは、自分が頼んだフィルタと返ってきたフィルタを見比べ、非対応の種類があれば穏当に扱うことが求められています(仕様の表記はSHOULD)。サーバーが黙って落とした種類は、待っていても通知が来ません。
stdioでは「最初」の意味が購読ごとになる
stdioでは1つの通信路を全メッセージが共有します。そのため確認応答が最初という順序は、通信路ごとではなく購読IDごとに定義されています。別の購読のメッセージは、この確認応答より先に届いても構いません。
クライアントは、通知に載る subscriptionId でどの購読のメッセージかを見分ける必要があります(stdioでは仕様上MUST)。HTTPなら購読ごとに別のレスポンスストリームになるので、取り違えはストリーム単位で防げます。
進捗やログの通知はこのストリームを通らない
変更履歴によると、notifications/progress や notifications/message のようにリクエストに紐づく通知は、そのリクエスト自身のレスポンスストリームを流れ続けます。subscriptions/listen のストリームに載るのは、4つのフィルタで選べる種類だけです。ツール呼び出しの進捗が来ない理由をこのストリームに探しても見つかりません。
複数の購読を同時に開ける
クライアントは、ツール一覧を見張る購読と、特定リソースを見張る購読を、同時にいくつも持てます。それぞれは、開いた subscriptions/listen のJSON-RPC IDで識別されます。
通知に載る subscriptionId を見ずに処理する実装は、別の購読向けの更新を取り違えます。1本の通信路しかないstdioで特に起きやすい不具合です。
購読の終わり方は仕様の2ページで書き方が違う
終わる経路は3つです。クライアントが終わらせる、サーバーが自分の都合(シャットダウンなど)で終わらせる、トランスポートが切れる、の3つです。
サーバー側の終わらせ方の記述
Subscriptionsのページ
サーバーが購読を終えるときは、元の subscriptions/listen リクエストへの完了レスポンスを返してからストリームを閉じます。中身は resultType: "complete" と購読IDだけで、突然の切断と区別するための合図です。
Cancellationのページ
サーバーが購読のストリームを終わらせるときは、その subscriptions/listen のリクエストIDを指した notifications/cancelled を送ります。このページは、それ以外の目的で同じ通知を送ることを禁じています。
2つの記述は併存しており、片方がもう片方を取り消してはいません。クライアントを書く側は、完了レスポンスの到着と notifications/cancelled の到着のどちらも「購読の正常終了」として受けられるようにしておくと、どちらの書き方に沿ったサーバーとも話せます。レスポンスも通知もなくトランスポートが切れたときは、予期しない切断として扱い、再接続を試みてよいことになっています。
クライアントが終わらせるときの手順はトランスポートで違います。HTTPならSSEストリームを閉じるだけで、notifications/cancelled は不要です。stdioでは閉じるストリームがないので、対象のリクエストIDを指定した notifications/cancelled を送ります。購読の終了に、別の仕様であるCancellationの語彙をそのまま流用している形です。
stdioで接続が切れて再確立されたときは、サーバーは購読の状態を何も覚えていません。クライアントは自分から subscriptions/listen を送り直して、購読を作り直す必要があります。
Claude Codeはこのストリームを開いたままにしている
Claude Codeには、MCPサーバーに接続するクライアントランタイムが2つあります。v1はMCP TypeScript SDK 1.x、v2はMCP TypeScript SDK 2.0を使い、プロトコルリビジョン2026-07-28に対応するのはv2だけです(接続設定の詳細はClaude Code MCP設定ガイドを参照してください)。
v2で新しいリビジョンのサーバーにつなぐと、Claude Codeは list_changed 通知を、開いたままのストリームで受け取ります。ツール一覧が変わっても接続を張り直さずに追従できる点は、v1と同じです。v2の新リビジョン接続では、この追従をこのストリーム経由の通知が支えています。
v2ランタイムが選ばれる条件は2系統ある
どちらのランタイムを使うかは、起動のたびに決まり、終了まで変わりません。
- 機能フラグを取得するセッション: v2.1.232以降でv2を使う
- 機能フラグを取得しないセッション: v2.1.274以降でv2を既定にする。対象はAmazon Bedrock・Claude Platform on AWS・Google CloudのAgent Platform・Microsoft Foundry経由のセッション、Claude appsゲートウェイ経由でサインインしたセッション、
DISABLE_TELEMETRYなどでテレメトリーや機能フラグ取得を切ったセッション
クラウドプロバイダー経由のセッションがv1のままだと考えていると外れます。上の条件では、これらのセッションがむしろv2の既定の対象です。ただし、クラウドプロバイダー経由のセッションは、ホスト側のプラットフォームが CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST を設定している場合は除かれます。ランタイムを自分で固定したいときは、環境変数 MCP_SDK_GENERATION に v1 か v2 を指定します(v2.1.218以降)。
新しいリビジョンを確認しに行くサーバーの範囲
v2が新リビジョンを確認しにいくかどうかは、接続の種類とClaude Codeのバージョンで決まります。HTTPサーバーはv2.1.274以降、機能フラグを取得しないセッションでも既定で確認します。stdioサーバーはv2.1.292以降、すべてのインストールで既定で確認します。claude.aiコネクターは、v2.1.295以降なら機能フラグを取得しないインストールでも既定で確認します。
自作のstdioサーバーが subscriptions/listen に対応していれば、v2.1.292以降は何も設定しなくても、既定で新方式の確認が行われます。環境変数 MCP_PROTOCOL_NEGOTIATION(v2.1.221以降)を auto にすると、確認を明示できます。legacy にすると、どのサーバーにも確認しません。
# 自作のstdio MCPサーバーに新リビジョンを確認させる
MCP_PROTOCOL_NEGOTIATION=auto claude特定のサーバーだけを旧プロトコルや、このストリームの外に留めるための機能フラグも、Anthropic側にあります。
ストリームが切れたときの再開は2通りに分かれる
切れたストリームは、閉じるまでの長さで再開の挙動が変わります。
ストリーム切断後の再接続
10秒以内に再び閉じる
最大3回まで開き直し、その接続については諦めます。
10秒より長く開いてから閉じる
1時間に5回まで開き直したあと、次の再接続までおよそ6時間待ちます。サーバーレスホスティングのように短命な接続を繰り返す相手が想定されています。
再開するまでの間、Claude Codeはそのサーバーから最後に取得したツール・プロンプト・リソースを保持し続けます。変更を早く拾いたいときは、/mcp からそのサーバーを再接続します。再接続がうまくいかないときの切り分けはMCPサーバーに接続できないときの切り分け手順にまとめています。
更新の取得そのものに失敗した場合も、前に取得した一覧が残ります。v2.1.214より前は、一時的なエラーでツール・プロンプト・リソースが空の一覧に置き換わっていました。
チャンネルサーバーには届かない
v2ランタイムでは、新リビジョン(2026-07-28)で接続したチャンネルサーバーは、チャンネルメッセージを届けられないため、チャンネルとして登録されません。旧ハンドシェイクで接続するサーバーなら従来どおり登録されます。stdioのチャンネルサーバーを旧ハンドシェイクのまま使いたいときは、MCP_PROTOCOL_NEGOTIATION を legacy にします。
実装で踏みやすい落とし穴
実装してテストを書いて初めて表面化しやすい不具合を挙げます(デバッグ時にメッセージのやり取りを直接見たい場合はMCP Inspectorの使い方が役立ちます)。
- 確認応答より先に通知を送る: 仕様違反になります。内部イベントをそのまま流す実装で起きがちです
- フィルタを超えた通知を送る: クライアントが購読していない種類は、サーバー内部のイベントでも送れません
subscriptionIdを無視して通知を処理する: 複数の購読を持つクライアントで、更新を取り違えます- stdioの再接続後に購読を送り直さない: サーバーは状態を持たないため、通知が届かないまま気づけません
- 完了レスポンスも
notifications/cancelledも出さずにストリームを閉じる: クライアントは異常切断とみなし、再接続を試みることがあります - 進捗やログを
subscriptions/listenに載せる: それらはリクエスト自身のレスポンスストリームの担当です
まとめ
subscriptions/listen は、resources/subscribe とGETエンドポイントを、確認応答から始まる1本のストリームに置き換えた仕組みです。サーバーを実装するなら、フィルタの範囲を守ることと、終わり方を2ページの記述に合わせることが要点です。
Claude Codeで動作を確かめるときは、自作のstdioサーバーが新リビジョンで接続されているかを先に確かめます。接続方式が旧のままなら、ストリームの挙動を見ているつもりで旧方式を見ている可能性があります。実装の各論はClaude Code MCP設定ガイドで補ってください。