MCP仕様2026-07-28でプロトコルがステートレス化、initializeハンドシェイクとセッションIDが消えた
目次
Anthropicは7月28日、MCPの新しい仕様リビジョン2026-07-28を確定したと発表した。
initializeハンドシェイクとMcp-Session-Idヘッダがなくなり、リクエスト1回ごとに完結するプロトコルに作り替えられた。Sampling・Roots・Loggingの3機能も廃止予定に入った。
仕様策定を主導してきたAnthropicのDavid Soria Parraは「リモートMCPの登場以来もっとも重要なリリース」と言っている。
TypeScript・Python・Go・C#の4つのTier 1 SDK(最優先サポート層の公式SDK)は仕様公開と同日に対応した。
ただ、セッションIDへ依存するコードには移行作業がいるし、通信形式を自前実装しているコードは互換性が切れる箇所が多い。
最低12ヶ月という期間は廃止予定機能が削除可能になるまでの原則で、ステートレス化を含む破壊的変更全体に与えられた猶予ではない。
プロトコルレベルのセッションが消えた
MCPは2024年11月に、ローカルで動く開発者ツールをLLMに繋ぐためのプロトコルとして始まった。
標準入出力で1対1に繋がる前提だったので、接続の最初にinitializeリクエストとnotifications/initialized通知で挨拶を交わし、以後の状態を双方が覚え続けるステートフルな設計になっていた。
MCPの2026年ロードマップにも、ローカルツールを接続する手段として始まった経緯が書かれている。
この設計はリモートサーバー用のStreamable HTTPトランスポートにも引き継がれ、サーバーは必要に応じてMcp-Session-Idヘッダを発行できた。
ステートフルなサーバーを複数台に増やすと、同じセッションのリクエストを同じサーバーインスタンスへ送り続けるスティッキールーティングか、インスタンス間でのセッション状態共有が必要になる。
公開している機能自体はステートレスでも、大規模な本番デプロイの実装と運用が複雑になっていた。
サーバーレスやエッジのように、リクエストごとに別のインスタンスが応答する基盤には載せづらかった。
2026-07-28ではハンドシェイクそのものがなくなった。
プロトコルバージョンとクライアントのcapabilitiesは、各リクエストの_metaフィールド(io.modelcontextprotocol/protocolVersionなど)に毎回載せる。
Streamable HTTPでは全POSTにMCP-Protocol-Versionヘッダも必要で、_meta内の値と一致させる。
サーバーは結果の_meta['io.modelcontextprotocol/serverInfo']に自分の識別情報を含めることが推奨され、バージョンが合わなければUnsupportedProtocolVersionErrorを返す。
Mcp-Session-Idヘッダは削除され、tools/listやresources/listの結果が接続ごとに変わることもなくなった。
呼び出しをまたいで状態が必要な場合は、サーバーが発行したハンドルを普通のツール引数として受け渡す。
新設のserver/discoverはサーバー側に実装が必須のRPCで、対応バージョン・capabilities・識別情報を返す。
クライアントは最初に呼んで事前にバージョンを選んでもいいし、標準入出力接続で旧世代サーバーかどうかを判別するプローブとしても使える。
セッションIDを発行する場合の2025-11-25 Streamable HTTPの流れは次の通り。
flowchart TD
A1["initialize リクエスト"] --> A2["initialize結果で<br/>Mcp-Session-Idを受け取る"]
A2 --> A3["IDを付けて<br/>notifications/initialized通知"]
A3 --> A4["後続HTTPリクエストにIDを付けて<br/>tools/callなどを送信"]
2026-07-28仕様では、この前置きが丸ごとなくなる。
flowchart LR
B1["tools/call<br/>_metaにバージョンと<br/>capabilitiesを同梱"] --> B2["結果<br/>_metaのserverInfoは推奨"]
最小実装のサーバーで新旧のやり取りを比べた
仕様だけ眺めてもピンとこないので、新旧それぞれの最小サーバーを書いてcurlで叩いてみた。
検証環境
| 項目 | 内容 |
|---|---|
| マシン | M4 Mac mini(10コア / 16GB) |
| Node | 25.3 |
| 実装 | 公式SDKは不使用。Streamable HTTPのJSON-RPCを素のnode:httpで処理する最小実装(新旧各70行前後) |
| ツール | 2つの数を足すaddの1個だけ |
| 確認方法 | curlで直接叩き、レスポンスは実際の出力を掲載 |
旧仕様(2025-11-25)のサーバーは、セッションIDの管理がコードの大半を占める。
initializeでUUIDを発行してSetに覚え、以後はIDのないリクエストを拒否する。curlだと、addを1回呼ぶまでに3往復かかる。
# いきなりtools/callを呼ぶと拒否される
$ curl -s -X POST localhost:8931 -d '{"jsonrpc":"2.0","id":1,"method":"tools/call",...}'
{"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"Bad Request: No valid session ID provided"}}
# initializeでセッションIDをもらう
$ curl -si -X POST localhost:8931 -d '{"jsonrpc":"2.0","id":1,"method":"initialize",...}'
HTTP/1.1 200 OK
Mcp-Session-Id: 85c6e611-f672-4106-b74b-fca724f0dafb
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25",...}}
# 以後のリクエストは全部このヘッダを付ける
$ curl -s -X POST localhost:8931 -H "Mcp-Session-Id: 85c6e611-..." \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"add","arguments":{"a":2,"b":3}}}'
{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"5"}]}}
新仕様(2026-07-28)のサーバーからは、このセッション管理がまるごと消え、代わりに毎リクエストの検証が入る。ハンドラの要点はこんな感じ。
// ルーティング用ヘッダとボディの一致を検証(-32020 HeaderMismatch)
if (req.headers['mcp-method'] !== msg.method) return fail(-32020, 'HeaderMismatch');
// server/discover は実装必須。対応バージョンとcapabilitiesを返す
if (msg.method === 'server/discover') {
return reply({ resultType: 'complete', protocolVersions: ['2026-07-28'],
capabilities: { tools: {} }, serverInfo: SERVER_INFO });
}
// セッションの代わりに、毎リクエストの_metaでバージョンを検証する
const version = msg.params?._meta?.['io.modelcontextprotocol/protocolVersion'];
if (version !== '2026-07-28') return fail(-32022, 'UnsupportedProtocolVersion');
if (msg.method === 'tools/call') {
return reply({ resultType: 'complete', content: [{ type: 'text', text: String(a + b) }],
_meta: { 'io.modelcontextprotocol/serverInfo': SERVER_INFO } });
}
クライアント側は1往復で終わる。
$ curl -s -X POST localhost:8932 -H 'Mcp-Method: tools/call' -H 'Mcp-Name: add' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"add","arguments":{"a":2,"b":3},
"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28"}}}'
{"jsonrpc":"2.0","id":1,"result":{"resultType":"complete","content":[{"type":"text","text":"5"}],
"_meta":{"io.modelcontextprotocol/serverInfo":{"name":"demo-new","version":"0.1.0"}}}}
_metaのバージョンを外すと-32022、ヘッダとボディのメソッドをずらすと-32020が返ってきた。
再起動させてみると、新旧の差がはっきり出た。
旧サーバーはプロセスを再起動するとSetの中身が消えるので、発行済みのセッションIDを付けた同じtools/callが拒否される。クライアントはinitializeからやり直しになる。
新サーバーは再起動しても、同じリクエストの再送がそのまま通る。
# 旧: サーバー再起動後、同じセッションIDでtools/call
{"jsonrpc":"2.0","id":4,"error":{"code":-32000,"message":"Bad Request: No valid session ID provided"}}
# 新: サーバー再起動後、同じリクエストをそのまま再送
{"jsonrpc":"2.0","id":1,"result":{"resultType":"complete","content":[{"type":"text","text":"5"}],...}}
複数インスタンスに増やしたときの問題も同じで、旧仕様はこのSetを全インスタンスで共有するか、スティッキールーティングで同じインスタンスに当て続ける必要がある。
新仕様のサーバーには覚えるものがないので、どのインスタンスに当たっても、いつ再起動しても結果が変わらない。
サーバー発のリクエストはMRTRパターンに置き換え
これまでのMCPには、sampling/createMessage(サーバーがクライアント側のLLMに生成を頼む)、elicitation/create(ユーザーへの追加入力要求)、roots/list(作業ディレクトリの問い合わせ)のように、サーバーからクライアントへ逆向きに飛ぶリクエストがあった。
従来のStreamable HTTPでは、処理中リクエストのSSEレスポンスを開いたまま逆向きのJSON-RPCリクエストを送り、元の処理を継続していた。
代わりに導入されたのがMulti Round-Trip Requests、略してMRTRパターンだ。
追加情報が必要なサーバーは、resultTypeがinput_requiredのInputRequiredResultを返し、inputRequestsフィールドに必要な情報の要求を載せる。この結果を返せる元のリクエストはprompts/get・resources/read・tools/callに限られる。
クライアントは元のリクエストにinputResponsesを付け、サーバーからrequestStateが返されていれば同じ値も添えて、新しいJSON-RPCリクエストIDで再送する。
リクエストへの応答として「これが足りない」と返す往復に変わった。
flowchart TD
C1["クライアント: tools/call"] --> S1["サーバー: resultType input_required<br/>inputRequestsで追加情報を要求"]
S1 --> C2["クライアント: 新しいIDで元のリクエストを再送<br/>inputResponsesと返されたrequestStateを付ける"]
C2 --> S2["サーバー: resultType complete"]
これに伴い、すべての結果にresultTypeフィールドが必須になった。
通常の結果はcomplete、MRTRの中間結果はinput_required。
旧世代サーバーがこのフィールドを省略した場合、クライアントはcompleteとして扱う決まりだ。
変更通知はsubscriptions/listenへ
サーバーからクライアントへの変更通知も整理された。
Streamable HTTPのGETエンドポイントとresources/subscribe・resources/unsubscribeが削除され、subscriptions/listenという単一のRPC方式に集約された。Streamable HTTPでは、各listenリクエストが長寿命のPOSTレスポンスストリームを開く。
クライアントはtoolsListChangedやresourceSubscriptionsなど、受け取りたい通知の種類を明示的に選んで購読する。
複数のlistenリクエストを同時に開くこともでき、サーバーは最初に受け付けた通知種別と購読IDを返す。
進捗通知(notifications/progress)とログ(notifications/message)は、元のリクエストのレスポンスストリームに流れる。
SSEストリームの再開機能も消えた。
SSE(Server-Sent Events)はHTTPレスポンスを開いたままサーバーからイベントを流し続ける仕組みで、これまでの仕様には切断時にLast-Event-IDヘッダで途中から再開する再配送の仕組みがあった。
2026-07-28では、レスポンスストリームが切れたら処理中のリクエストは失われたものとして、新しいリクエストIDで再発行する。
再開に必要なイベント履歴の保持自体が、サーバーに状態を持たせる要因だった。
ping、logging/setLevel、notifications/roots/list_changedも削除された。
ログレベルは_meta['io.modelcontextprotocol/logLevel']でリクエストごとに指定する。
このフィールドを付けなかったリクエストに対して、サーバーはログ通知を流してはいけない。
MCP AppsとTasksが公式拡張になった
capabilitiesにextensionsフィールドが追加され、コア仕様の外に機能を足すための正式な枠組みができた。
2026-07-28の発表で中心に挙げられた公式拡張は、会話の中にインタラクティブなUIを描画するMCP Appsと、長時間かかる処理を扱うTasksだ。ほかに認可系の公式拡張もある。
拡張はコア仕様と独立してバージョン管理され、必ずコアへ取り込まれるわけではない。
Tasksは実験的機能としてコア仕様に入っていたが、io.modelcontextprotocol/tasks拡張として作り直された。
結果が出るまでブロックするtasks/resultはtasks/getによるポーリングに置き換えられ、実行中のタスクへクライアントから入力を送るtasks/updateが加わった。
tasks/listは削除された。クライアントが各リクエストでTasks拡張への対応を表明していれば、別のタスク化フラグなしにサーバーがタスクハンドルを返せるようになった。
Roots・Sampling・Loggingは廃止予定、削除可能になるのは原則12ヶ月後
今回から機能ライフサイクルポリシーが導入され、機能は有効(Active)、廃止予定(Deprecated)、削除済み(Removed)の3状態で管理される。
新たに廃止予定になった機能が削除可能になるまでの期間は原則最低12ヶ月で、実際の削除時期はその後の仕様改訂で判断される。未対処の重大なセキュリティリスクがある場合は最短90日へ短縮できる例外もあり、廃止予定の機能と最短時期はレジストリで追跡される。
その第1陣として、Roots・Sampling・Loggingの3機能が廃止予定に入った。
利用が少ないか、実装の混乱を招いていた機能だ。仕様が示す移行先は次の通り。
| 廃止予定 | 移行先 |
|---|---|
| Roots | 対象ディレクトリやファイルをツール引数・リソースURI・サーバー設定で渡す |
| Sampling | LLMプロバイダのAPIを直接呼ぶ |
| Logging | stderrへの出力(stdio)またはOpenTelemetry |
| HTTP+SSEトランスポート | Streamable HTTP |
| 動的クライアント登録(RFC 7591) | Client ID Metadata Documents |
HTTP+SSEトランスポートは2025-03-26仕様の時点で非推奨だったものが、ライフサイクルポリシー上の廃止予定として正式に分類し直された。
このトランスポートは移行規定の対象で、レジストリ上の最短削除時期はSEP-2596が最終状態(Final)になってから3ヶ月後とされている。
認可用の動的クライアント登録(クライアントが認可サーバーへ自分を自動登録するRFC 7591の仕組み)も廃止予定になり、新規実装ではクライアントが自分のメタデータをURLで公開するClient ID Metadata Documentsが推奨される。動的クライアント登録は、対応していない認可サーバー向けのフォールバックとして残る。
認可はエンタープライズの本番構成に合わせた
認可まわりは、Entra IDやOktaのような企業の認証基盤へそのまま繋ぐことを想定した強化が入った。
認可サーバーはレスポンスにissパラメータ(トークン発行者の識別子)を含めることが推奨になった。クライアントは受け取ったissを記録済みの発行者と照合し、不一致なら認可コードをトークンエンドポイントへ送らずに応答を拒否する。認可サーバーがiss対応を表明しているのにレスポンスから欠けている場合も同じ扱いだ。
RFC 9207に沿った変更で、悪意ある認可サーバーが、正規の認可サーバーが発行した認可コードをクライアントから攻撃者側のトークンエンドポイントへ送らせるOAuthミックスアップ攻撃への対策だ。
事前登録済みのクレデンシャルと、動的クライアント登録で取得して永続化するクレデンシャルは、発行元の認可サーバーに紐付けて保存し、別の認可サーバーへ使い回すことも禁止された。Client ID Metadata DocumentsのURLを使うクライアントIDは、認可サーバー間で移植できる。
キャッシュとルーティング向けの細かい変更
server/discover、tools/list、prompts/list、resources/list、resources/templates/list、resources/readのcomplete結果には、ttlMsとcacheScopeが必須フィールドとして加わった。
ttlMsはミリ秒単位の鮮度ヒントで、データが変わらないことの保証や自動ポーリング間隔ではない。cacheScopeがpublicなら認可コンテキストをまたぐ共有キャッシュで利用でき、privateなら同じ認可コンテキスト内でだけ再利用できる。
tools/listは毎回同じ順序で返すことが推奨になった。ツール一覧はLLMのプロンプトに入るため、順序を固定してプロンプトキャッシュのヒット率を上げる狙いだ。
Streamable HTTPのJSON-RPCリクエストにはMcp-Methodヘッダが必須で、tools/call・resources/read・prompts/getではMcp-Nameも必須になった。全POSTに必要なMCP-Protocol-Versionは、本文の_metaにある値と一致させる。
ロードバランサやゲートウェイがJSON-RPCのボディを読まずに、ヘッダだけでメソッド単位のルーティングやレート制限をかけられる。
_metaにOpenTelemetryのtraceparentなどトレースコンテキストを載せる規約も文書化された。
移行の影響とClaude側の対応
破壊的変更なので、旧世代のクライアントと新世代のサーバー、またはその逆は、双方が同じプロトコル世代をサポートしていない限り会話できない。
標準入出力ではserver/discoverをプローブに使い、Streamable HTTPでは新方式のリクエストに対するエラー本文を確認して旧方式へフォールバックする。両世代に対応する実装は、相手に応じて2026-07-28と旧仕様を切り替える。
Anthropicによると、MCPのSDKは月間ダウンロードが4億を超え、2026年に入って4倍になった。
2026-07-28サポートはClaude製品群へ順次展開中で、Claudeのコネクタディレクトリには950以上のMCPサーバーが掲載されている。
自前実装の移行は、仕様本文と各言語のSDK移行ガイドが起点になる。Roots・Sampling・Loggingがレジストリ上で削除可能になる最短時期は、2027年7月28日以後に公開される最初の仕様リビジョンだ。
参考
- MCP Specification 2026-07-28: Key Changes
- MCP Blog: The 2026-07-28 Specification
- MCP Specification: Versioning and Compatibility
- MCP Specification: Multi Round-Trip Requests
- MCP Specification: Subscriptions
- MCP Specification: Streamable HTTP
- MCP Specification: Deprecated Features
- MCP Specification: Authorization
- MCP Specification: Caching
- MCP Extensions: Tasks
- Anthropic: Bringing MCP 2026-07-28 to Claude