Table of Contents
MCPの最新仕様2026-07-28版が出て、プロトコルレベルでセッションの概念が消えたりして、MCPサーバーの開発やデプロイが楽になったりならなかったりした。
この記事ではそのあたりを含め、2026-07-28版の変更点を解説する。
なお、この記事には生成AIの出力が含まれる。
MCP 2026-07-28版が出た
去年、ツールだけじゃない。MCPの全機能を完全に理解するという記事で、当時のMCPの機能を一通り解説した。 その後ズンドコMCPサーバーを作ってMCPの(ほぼ)全機能を実装し、MCPサーバーの実装についても書いた。
そのころのMCPが2025-06-18版だったんだけど、そこから一年余りを経て、2025-11-25版を経て2026年7月28日にMCPの新しい仕様が公開された。
2025-11-25版は認証周りの改善などの順当なアップデートだったんだけど、今回の2026-07-28版の改訂はマイナーチェンジではない。 ChangelogにはMajor changesが9項目、Minor changesが12項目、非推奨化が4項目も並んでいる。
大きいところでは、以下の2つの破壊的な変更があった。
initializeハンドシェイクやMcp-Session-Idヘッダーが廃止され、プロトコルからセッションという概念が消えた- サーバーからクライアントへリクエストを送る仕組みが廃止され、MRTR (Multi Round-Trip Requests) という新しいパターンに置き換わった
どちらも後方互換性の無い変更で、既存のMCPサーバーやクライアントはそのままでは新仕様の実装とは話せない。 ほかにも一部機能の非推奨化があったりするので、MCPサーバーやMCPホストの開発者は移行性を考慮しつつ新仕様への対応を進める必要がある。
次節以降では、まず旧仕様をおさらいしてから、セッションレス化(ステートレス化)とMRTRを中心に、その他の変更点も見ていく。
おさらい: 2025-11-25までのMCP
MCPの機能の全体像は以前の記事に書いたんだけど、ハンドシェイクやセッションについては触れなかったし、今回の変更点と関係ない機能もあるので、ここでは今回の変更に関わる部分をおさらいしておく。
ハンドシェイクとセッション
旧仕様のMCPでは、クライアントはサーバーにリクエストを送る前に以下のような3-wayハンドシェイクをする必要があった。
- クライアントが
initializeリクエストを送り、自分のプロトコルバージョンとcapabilities(i.e. 対応機能)を伝える - サーバーがセッションを開始して、自分のバージョンとcapabilitiesを返す
- クライアントが
notifications/initialized通知を送ってセッション確立
Streamable HTTPトランスポートでは、このinitializeのレスポンスでサーバーがMcp-Session-IdヘッダーにセッションIDを入れてを返し、クライアントは以降の全リクエストにそのヘッダーを付ける。
セッションを終わらせたいときは、Mcp-Session-Idヘッダーを付けてHTTP DELETEを投げる。
Mcp-Session-Id: abc123 C->>S: notifications/initialized
Mcp-Session-Id: abc123 Note over C,S: ハンドシェイク完了。以降の全リクエストに
Mcp-Session-Id: abc123 を付与 C->>S:
tools/call
Mcp-Session-Id: abc123 S-->>C: 結果 C->>S:
HTTP DELETE
Mcp-Session-Id: abc123 Note over C,S: セッション終了
つまり、ハンドシェイクで確立したセッションをサーバーが保持し、以降のリクエストはそのセッション上で処理されるというプロトコルだったため、サーバーはステートフルアプリケーションとして設計する必要があった。
サーバーからクライアントへのリクエスト
旧仕様のMCPは双方向プロトコルで、サーバーからクライアントにリクエストを送ることもできた。 サーバーから呼び出せるクライアント機能は以下の3つ。
- Roots: MCPサーバーに、サーバーがアクセスしていいディレクトリ(roots)を知らせる機能
- Sampling: MCPサーバーがMCPホスト側のLLMを利用するための機能。
- Elicitation: MCPサーバーがMCPホストのユーザーに追加の情報を要求するための機能。
Streamable HTTPトランスポートの場合、これらのリクエストは、クライアントがHTTP GETで開いた常設のSSEストリーム、またはリクエストのレスポンスを受け取るためのSSEストリームを使って送られる。
例えばAzure MCPサーバーは、特定のツール呼び出し(tools/callリクエスト)を処理している最中に、機密データの取り扱いについてユーザーの同意を得るため、Elicitationリクエストをクライアントに送る。
ステートフルと双方向通信によって生じた制約
ステートフルで双方向通信という設計は素直で分かりやすい反面、以下のような課題があり、サーバーの開発やデプロイを難しくしていた。
- セッションの状態をどこかに保持しないといけない。
Mcp-Session-Idに対応する状態(合意したバージョン、クライアントのcapabilities、アプリケーション固有の文脈)をサーバーが覚えている必要がある。 - ロードバランサーをステートフルにしないといけない。 セッションを握ったサーバーインスタンスに後続のリクエストが送られ続けるようにする(i.e. sticky session)か、全サーバーインスタンスでセッション状態を共有したり同期したりする必要がある。
- コネクションを開きっぱなしにしないといけない。 サーバーからリクエストを送る場合、クライアントからレスポンスを受け取り、サーバーの最終レスポンスを返すまでSSEコネクションを開きっぱなしにしておかなければならない。特にElicitationでユーザー入力をリクエストする場合、人の操作が入るので、コネクションは長めに維持されることになる。この間にオートスケールなどでサーバーインスタンスが落ちれば、リクエストは中断されてしまう。
要するに、旧仕様のMCPサーバーは、WebSocketサーバーっぽい慎重な運用を強いられていた。 ステートレスなコンテナで水平スケールしてラウンドロビンで捌く、みたいなHTTP APIサーバーでよく取られる弾力性の高い運用しやすい構成が適用しにくかった。
Aに送り続けないといけない C->>LB: tools/call (Mcp-Session-Id: abc123) LB->>SA: tools/call SA-->>C: 結果 Note over SB: BはAが確立したセッションを知らないので
リクエストを受けれない
MCP 2026-07-28版では、この制約は丸ごと取り払われた。
ステートレスになったMCP
新仕様のOverviewには、こう書いてある。
The Model Context Protocol (MCP) is a stateless protocol: all the information needed to process a request is contained in the request itself.
MCPはステートレスプロトコルになり、リクエストの処理に必要な情報は、すべてそのリクエストの中に入っている。 サーバーは各リクエストを独立に処理し、同じコネクション上の過去のリクエストからも文脈を推測してはならない。
2026-07-28版には、このステートレス化を実現するための大小の変更が含まれている。 それらを次節以降で解説する。
ハンドシェイクの廃止
ハンドシェイクは廃止され、ハンドシェイクで合意していた内容は各リクエストの_metaフィールドが運ぶようになった。
| キー | 型 | 必須 | 内容 |
|---|---|---|---|
io.modelcontextprotocol/protocolVersion |
string |
Yes | このリクエストのプロトコルバージョン |
io.modelcontextprotocol/clientCapabilities |
ClientCapabilities |
Yes | このリクエストに関係するクライアントのcapabilities |
io.modelcontextprotocol/clientInfo |
Implementation |
No | クライアントの名前とバージョン |
io.modelcontextprotocol/logLevel |
LoggingLevel |
No | このリクエストについてサーバーが出すログの最低レベル |
_metaの例:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": {
"location": "Seattle, WA"
},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {
"name": "ExampleClient",
"version": "1.0.0"
},
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}clientInfoは必須ではないが、特に理由が無ければ毎回入れるべきとされている。
逆にサーバーは、各レスポンスの_metaにio.modelcontextprotocol/serverInfoを入れて自身の情報をクライアントに伝えるべきとされている。
ただし、clientInfoとserverInfoは自己申告で、プロトコルは検証しないので、ロギングやデバッグ用に使えるくらいであって、これを見て挙動を変えたりセキュリティ判断するのはまずい。
Mcp-Session-Idも廃止
ハンドシェイクが廃止されたので、それで確立していたセッションもなくなり、セッションのIDを入れていたMcp-Session-Idヘッダーも廃止された。
プロトコル上のセッションがなくなり、MCPがステートレスになったというわけ。
とは言っても、実際MCPツールを実装する上では、クライアントとのやり取りに応じたステートをサーバーで持つ必要があることは少なくない。 ズンドコMCPサーバーも、クライアントが何回ズンを取得したかを覚えておかないと、キヨシチェックツールを実現できない。
では、そのようなリクエストをまたいだステートを持ちたい場合はどうするのかというと、サーバーで独自のハンドルを発行して、それをツールの引数として受け取る ようにしてね、ということになっている。
例えばズンドコMCPサーバーなら、ズンドコを取得するツール(get_zundoko)の戻り値で識別子をクライアントに渡して、クライアントはそれをキヨシチェックツール(check_kiyoshi)の引数に渡す、という感じになる。
コネクションやセッションに状態を隠すのではなく、状態をプロトコルの外に出す という方針だ。
同じ理屈で、stdioトランスポートも旧仕様のときは、1つのサーバープロセスが1セッションという形だった(複数セッションも扱えたけど、セッションを終了させる手段がプロセスを落とすしかないので、1プロセス1セッションの実装が多かった)のが、プロセスもセッションではなくなった。 むしろ、stdioプロセスは長時間起動しっぱなしにして、複数の独立したやり取りを1本のstdioに流して、無関係のリクエストやレスポンスが混ざっても適切に処理することが今後は強く求められる。
server/discover の追加
ハンドシェイクが無くなると、クライアントはサーバーがどのバージョンや機能をサポートしているかが事前にわからなくなる。 それに対応するため、server/discoverというリクエストが追加された。 クライアントがこのリクエストを送ると、MCP 2026-07-28版に対応したサーバーはサポートバージョンやcapabilitiesなどを返してくれる。
initializeとの重要な違いは、クライアントはserver/discoverを送らなくてもいいということ。
いきなりツール実行してもいいので、curlでtools/callのJSON-RPCオブジェクトを投げつけるみたいな、REST APIみたいな呼び出し方も2026-07-28版ならできる。
サーバーが2026-07-28版をサポートしてるかわからないときに、とりあえずserver/discoverを投げてみて、エラーだったらinitializeで2025-11-25以前のハンドシェイクにフォールバックする、みたいにも使える。
ちなみに現在のMCP仕様では、ハンドシェイクベースの2025-11-25以前のバージョンをLegacy、リクエストごとにメタデータを運ぶ2026-07-28以降のバージョンをModernと名付けて峻別している。
常設SSEストリームの廃止
旧仕様では、ハンドシェイクの後、クライアントからMCPエンドポイントに確立したセッションを指定してGETリクエストを送ることで、常設のSSEストリームを獲得することもできて、ログメッセージを受け取ったり、resources/subscribeしてリソース変更通知を受け取るといったことができた。
新仕様ではセッションがなくなったため、MCPエンドポイントへのGETとresources/subscribeも廃止された。
その代わり、subscriptions/listenという単一のリクエストが追加された。 このリクエストはロングポーリングの一種で、サーバーに送るとレスポンスストリームが開きっぱなしになって、通知を受け取ることができる。 クライアントは受け取りたい通知の種類を指定してサブスクライブする、という形。
SSEの再開もできなくなった
Last-Event-IDヘッダとSSEのイベントIDによるストリーム再開とメッセージ再送も廃止された。
2026-07-28版では、ストリームが切れたら、その処理中のリクエストは失われるので、クライアントは新しいリクエストIDで投げ直す必要がある。
これもサーバーは状態を持たないことの徹底で、サーバーは再送のために送信済みメッセージをバッファしておく必要が無くなった。
セッション単位のログレベル設定の廃止
セッションがなくなったので、セッションに対してログレベルを設定するためのlogging/setLevelリクエストも廃止された。
2026-07-28版ではログレベルはリクエストごとに_metaのio.modelcontextprotocol/logLevelで指定する。
ステートレスになったことの意味
ここまで削ぎ落とすと、Streamable HTTPのMCPサーバーは ただのHTTP APIサーバー になる。
公式ブログの表現を借りれば、どのリクエストも「共有ストレージ無しで、素のラウンドロビンなロードバランサーの背後にある任意のサーバーインスタンスで処理できる」。
どのサーバーインスタンスに振ってもいい
実務上のうまみは結構大きい。
- sticky sessionの設定が要らない: ALBのターゲットグループもKubernetesのServiceも、何も気にせずデフォルトのナイーブな設定でいい
- セッションストアが要らない: Redisやらセッションアフィニティやらをスケールアウトのために用意する必要がない
- スケールインでセッション切れが発生しない: サーバーはグレースフルシャットダウンだけ気にして、処理中のリクエストさえ捌き終わればインスタンスを落とせる
- サーバーレスに素直に載る。 AWS LambdaやCloud Runのような、インスタンスの生存を前提にできない実行環境にMCPサーバーをデプロイできる。旧仕様でこれをやろうとすると、セッション状態を外部で管理する作り込みが要った
Mcp-MethodとMcp-Name
ただのHTTP APIサーバーっぽくデプロイできるようになったMCPサーバーを、さらに普通のHTTPサーバーっぽく扱えるように追加された仕様にもここで触れておく。
Streamable HTTPトランスポートのMCPサーバーへのリクエストには、Mcp-MethodとMcp-Nameヘッダーの付与が必須になった。
Mcp-MethodはJSON-RPCリクエストオブジェクトのmethod、Mcp-Nameはparams.nameまたはparams.uriのコピーだ。
POST /mcp HTTP/1.1
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: get_weatherこれはつまり、リクエストボディをパースせずにHTTPヘッダだけでルーティングやレート制限、WAFのポリシー適用ができる ようになったということ。
「delete_*系ツールだけ認可を厳しくする」とか、「tools/callにはレート制限をかける」、といったことが、MCPを理解しないミドルウェアやリバースプロキシとかでもできるようになった。
当然ヘッダとボディが食い違うとセキュリティ問題になるので、サーバーは両者の一致を検証してからリクエストを処理することが求められる。
双方向通信からMRTRへ
セッションと常設ストリームを捨てたということは、サーバーからクライアントへリクエストを送る経路がなくなった ということ。
上のほうに書いた通り、旧仕様にはサーバーからクライアントにリクエストを送る仕組みがあり、ElicitationとSampling、Rootsといったクライアント機能を呼び出すことができる。 これらは新仕様でどうなったのか。
双方向通信は廃止された
新仕様には以下のように、サーバー起点のリクエストはサポートされないと明確に書かれている。
Servers MUST send server-to-client requests (such as
roots/list,sampling/createMessage, orelicitation/create) using the MRTR pattern. The previous pattern of server-initiated requests is no longer supported. This is a breaking change.
代わりに導入されたのが MRTR (Multi Round-Trip Requests) 。 サーバーは、クライアントからのリクエストを処理するのに情報が足りない場合、クライアントにリクエストを投げる代わりに、「追加情報を要求するレスポンス」を返して、リクエストの処理をいったん完了させる。
MRTRとは
MRTRは以下のような流れになる。
inputRequests + requestState Note over C,S: 追加情報を要求するレスポンスを返し、
最初のリクエストはここで終了 C->>U: 入力を求める U-->>C: 入力を返す C->>S: tools/call (id: 2)
元の引数 + inputResponses + requestState Note over S: requestStateから文脈を復元して
処理を完了 S-->>C: CallToolResult (id: 2)
この流れで重要なのは、二回目のリクエスト(リトライ)が、一回目とは別のrequest IDを持つ完全に独立したリクエストである こと。 二回目のリクエストを処理するサーバーは、そのリクエストに直接書いてある情報以外は何も要らないので、一回目のリクエストと無関係のインスタンスであっても問題ない。
InputRequiredResultの中身
一回目のリクエストに対するレスポンスが「追加情報を要求するレスポンス」で、その型がInputRequiredResult。
InputRequiredResultが持つのは2つのフィールドだけ。
inputRequests(任意): クライアントへの追加情報リクエストのマップ。キーはサーバーが振る識別子、値はElicitRequest、CreateMessageRequest、ListRootsRequestのいずれか- それぞれが旧仕様のElicitation、Sampling、Rootsに対応。
requestState(任意): リトライリクエストを処理するサーバー向けのステート情報を表す文字列。クライアントは内容を気にしてはいけない。
このrequestStateにMCPサーバーステートレス化のツケが集約されている。
InputRequiredResultは実質的にリクエスト処理の中断なので、サーバーは後で処理を再開するために中断時の状態を残す必要があるんだけど、プロトコル上はサーバー側に持てないので、クライアントに預ける感じになる。
クライアントは預かったrequestStateを覗いても改変してもいけないし、次回のリトライリクエストにしか使っちゃいけないことにはなっているけど、その約束を守ってくれるかはクライアント次第。
なのでセキュリティを確保するために、サーバーはrequestStateを以下のように扱わないといけない。
- クライアントによる改ざんや窃取を防ぐため、必要に応じてHMACやAEADで完全性・機密性を確保し、署名検証に失敗した状態はリクエストを拒否する
- 完全性を確保したうえで、リプレイ対策として、値の中に以下を入れて毎回検証する
- 認証した主体 (i.e. リクエストを送ってきたクライアントやユーザーの識別子)
- 短いTTL
- 元リクエストの識別子
- 同じクライアントが
requestStateを使いまわすことを防ぎたいなら、リプレイ対策に加えてワンタイムパスワードを値に含める
Elicitationはこう変わった
前の記事やズンドコMCPサーバーのElicitation編で書いたElicitationは、今回一番作法が変わった機能だ。
まず、Elicitationはサーバー起点のリクエストではなく、InputRequiredResultのinputRequestsに載せる要求になった。
で、その先がややこしくて、特にMCP 2025-11-25版で追加されたURLモードは複雑。
URLモードは、MCPサーバーがリソースサーバーにアクセスするときの認証情報をユーザーからもらいたいみたいなときに便利なモード。 従来のFormモードではユーザーがMCPクライアントに認証情報を入力して渡さないといけなかったけど、URLモードではユーザーは提示されたURLの先(e.g. リソースの認可サーバーなど)に認証情報を渡せばよくて安全。
このURLモードでは、旧仕様ではURLの先でのユーザー操作完了をサーバー起点での通知でクライアントに知らせてたけど、新仕様ではInputRequiredResultを返した時点でHTTPセッションが終わってるのでそれができない。
そのため、以下のような流れになる。
elicitation/create (mode: url) + requestState C->>U: このURLを開いていいか確認 U-->>C: 同意 C->>B: URLを開く C->>S: tools/call (id: 2)
action: accept + requestState Note over S: requestStateから対象を特定
完了までブロックすることもある B-->>S: ユーザーが外部で入力を完了 S-->>C: CallToolResult (id: 2)
フローの中で、リトライリクエストで送るaction: "accept"は「URLを開くことにユーザーが同意した」という意味でしかなく、URLの先での操作が完了したことは意味しない。
サーバーはこのリトライリクエストを受け取ったら、URLの先でのユーザー操作が完了するのを待ち、その結果をもってリクエスト処理を再開し、CallToolResultを作って返す、ということをしないといけないはず。
実装むずそう。
SamplingとRootsは非推奨になった
MRTRでは一応SamplingとRootsもできるようになってるけど、非推奨になってしまった。
今回同時に導入されたFeature Lifecycle and Deprecation Policyにより、非推奨になった機能は最低12ヶ月は仕様に残るので、既存の実装がすぐ壊れることはないが、新規実装では使うべきではない。
仕様が挙げている移行先はこうなっている。
| 非推奨 | 移行先 |
|---|---|
| Roots | ディレクトリやファイルはツールの引数、リソースURI、サーバーの設定で渡す |
| Sampling | MCPを介さずLLMプロバイダのAPIを直接叩く |
ちなみにLoggingも、サーバー起点の通知を使っていたからか非推奨になった。 Loggingは代わりにOpenTelemetryを使えとなっていて、MCPリクエストやレスポンスの_metaフィールドにトレースコンテキストをいれて伝播させる仕様になっている。
ちなみにちなみにPingも廃止に。これもサーバー起点の通知が可能だったから?
生死確認はトランスポート層でやれば十分ということか。
その他の変更点
以上、目玉のステートレス化と双方向通信廃止について説明した。 ほかの変更については大きそうなのを拾ってざっくり書く。
Tasksはコアから拡張機能へ
Tasksは長時間のジョブを非同期に実行できる機能。
2025-11-25で実験的機能として追加されたけど、今回コアプロトコルから外れてio.modelcontextprotocol/tasksという公式拡張になった。
フィードバックを素早く取り込んで仕様を安定させるための処置で、そのうちコアに戻ってくるかも。
キャッシュのヒントが必須になった
tools/listとかのレスポンスに、キャッシュ関係の2つのフィールドが必須になった。
ttlMs: キャッシュの寿命cacheScope:"public"か"private"。クライアント/サーバー間のプロキシとかがキャッシュしていいかを制御する
併せて、tools/listはツールを決定的な順序で返すべき となった。
この仕様の目的の一つは、LLMのプロンプトキャッシュのヒット率を上げること。
実運用を意識した細やかな配慮が興味深いけど、Claude Codeみたいにツールを遅延ロードするMCPホストには効果なさそう。
x-mcp-header
上のほうで、Streamable HTTPトランスポートの時、Mcp-MethodとMcp-Nameというヘッダーが追加されたというのを書いたけど、さらにx-mcp-headerという機能でHTTPヘッダーを拡張できるようになった。
サーバーがツール定義をクライアントに渡すとき、inputSchemaのプロパティにx-mcp-headerアノテーションを付けると、クライアントがツールを呼び出すときにそのプロパティの値をMcp-Param-xxxxというヘッダーに入れて送ってくれる、というもの。
例えばツール定義が以下のようになっていると、クライアントはツール呼び出し時にMcp-Param-Region: ap-northeast-1みたいなヘッダーを送ってくれるので、それを見てリバースプロキシでルーティングするみたいな使い方ができるはず。
{
"name": "execute_sql",
"inputSchema": {
"type": "object",
"properties": {
"region": {
"type": "string",
"x-mcp-header": "Region"
},
"query": { "type": "string" }
},
"required": ["region", "query"]
}
}ただ、クライアントがちゃんと送ってくれるか保証があるわけではないので、使い道は限られそう。
Dynamic Client Registrationが非推奨に
Dynamic Client Registration (DCR)が非推奨になり、Client ID Metadata Documents (CIMD)が推奨になった。
新仕様への移行
新仕様(Modern)と旧仕様(Legacy)は互換性がないので、サーバーにせよクライアントにせよしばらくは両仕様に対応して、どちらを使うか探りながらやらないといけなそう。
- Modernクライアント × Legacyサーバー: クライアントがハンドシェイクしないので、サーバーからセッションが無い旨のエラーが返って通信できない。
server/discoverを先に投げればわかりやすいエラーが返ってくるのでちょっとましになる。 - Legacyクライアント × Modernサーバー: クライアントが無駄にハンドシェイクを始めるので、サーバーからそんなメソッドないというエラーが返って通信できない。サーバーは
initializeリクエストに対するエラーの中にバージョン不整合を示すメッセージを入れておくとちょっと優しい。
サーバーを両バージョンに対応させたいなら、_meta付きのリクエストに対してはModern式で、initializeが来たらLegacy式で対応すればいい。
クライアントを両バージョンに対応させたいなら、とりあえずserver/discoverを投げて探り、ModernなレスポンスならModern式で、そうでなければLegacy式でというように切り替えればいい。
まとめ
MCP 2026-07-28版の変更は、一言でいえば 繋ぎっぱなしの接続を前提にするのをやめ、リクエスト単位で完結するようにした、という感じ。
セッションもハンドシェイクも常設ストリームも消え、MCPサーバーは普通のHTTP APIサーバーみたいに書いたりデプロイしたりしやすくなった。 sticky sessionもセッションストアは要らず、スケールイン・スケールアウトが簡単で、サーバーレスにもできる。 ヘッダーでどんなリクエストかわかるので、MCPを知らないプロキシでもルーティングや流量制御ができる。
一方で、状態管理自体が全くなくなったわけではなくて、MRTRのrequestStateという形で、サーバーのアプリケーションロジックの責務として残った。
サーバーの内部状態がクライアントに渡るので、暗号学的な完全性保護やリプレイ対策までが仕様上の要求になっている。このあたり、実装は面倒になったはず。
ElicitationのURLモードまで実装すると旧仕様よりだいぶ大変そう。
ズンドコMCPサーバーはもうやる気おきないけど、NASA Images MCP Serverはそのうち新仕様に対応させてみたい。