AIエージェント
MCPのステートレス化で状態はどこへ?セッション廃止の理由と移行
- MCP
- AIエージェント
- ハンドル
- セッション
- ステートレス
- Model Context Protocol
- SEP-2567
- MRTR
- Equixly
- ツール引数
- キャッシュ
- スティッキーセッション
- 冪等性
- 廃止期間
- ゲートウェイ
- Streamable HTTP
- Cloudflare
- Durable Objects
- サブエージェント
- 買い物かご
- WAF
- OpenTelemetry
- ChatGPT
- JSON-RPC
目次
繰り返し届く要求に答えるプログラムは、前の要求で何が起きたかを覚えておく必要がある場面が多い。買い物かごに品物を足す処理を考えれば分かりやすい。二回目の呼び出しは、一回目でかごに何が入ったかを知らなければ答えを出せない。だから設計者は、この記憶をどこかに置く。
置き方の一つは、記憶を接続の中に持たせるやり方である。つながっている間だけ、両側が同じ記憶を暗黙のうちに共有する。呼び出す側は記憶の名前を書かなくてよいし、答える側は直前の要求の続きから処理を始められる。
このやり方はつながっている間は働くが、代償を伴う。一つ一つの要求が、特定のサーバープロセスに結びつく。接続が切れれば記憶は消え、プロセスが再起動しても消える。呼び出す側は、その記憶がまだ生きているかどうかを外から確かめられない。設計者は、記憶の置き場所を決めなければならない。
MCPはセッションを外したあと、状態をどこに置いたのか。
状態は消えていない。サーバーに残り、それを指すハンドルをモデルがツール引数として運ぶようになった。 ハンドルとは、ファイルハンドルと同じ意味の語で、サーバーが発行し、呼び出す側が次の呼び出しに添えて持ち回る識別子である。MCP(Model Context Protocol、AIエージェントのモデルに外部のツールやデータを渡すための共通の取り決め)は、2026-07-28版でプロトコル層のセッション(一つの接続に紐づく、複数の呼び出しにまたがる状態の入れ物)を取り除いた1。外したのは、その入れ物の寿命と範囲がクライアントごとに違い、ツール一覧も取っておけず、一つの入れ物に状態を一つしか持てなかったからである2。接続そのものは状態を持たない形(ステートレス、どの要求も単独で処理でき、前の要求の記憶に依存しない形)になった1。この移動は運用と安全の両方に届く。載せる側から見れば、セッションを抱えるための仕組みを丸ごと外せる3。守る側から見れば、これまでトランスポートに隠れていた参照が、モデルの入力の中へ出てきたことになる4。
メモを覚えるサーバーを、記憶の寿命が違う二つのクライアントから呼ぶ
※ この節の数値は説明のための仮定で、測定値ではありません。
メモに一行足すツールと、メモを読み出すツールを持つサーバーを考える。サーバーはメモを接続の記憶に結びつけている。このサーバーを、記憶の寿命が違う二つのクライアントから呼ぶ。
クライアントAは、ツールを一回呼び出すたびに新しい記憶を作る。「牛乳」を足してから読み出すと、二回目の呼び出しは新しい記憶の上で動くので、空のメモが返る。クライアントBは、アプリを起動したときに記憶を一つ作り、終了まで使い続ける。会話1で「牛乳」を足し、会話2で読み出すと、会話2には頼んでいない「牛乳」が返る。同じサーバーが、Aでは忘れすぎ、Bでは混ぜすぎる。
メモに名前を付ける形に変えると、この差は消える。メモを作るツールが memo-41 という名前を返し、モデルは足すときも読むときもその名前を引数に添える。Aでも二回目の呼び出しは memo-41 を指すので「牛乳」が返る。Bでも、会話2が自分で memo-42 を作れば別のメモを持てる。メモそのものは前と同じくサーバーにある。変わったのは、それを指す名前を誰が運ぶかであり、その代わりに memo-41 という文字列が、モデルの読む入力の中に現れる。
ツールの一覧の取り直しも数えられる。サブエージェント3つが、それぞれサーバー4つにつなぐとする。一覧が記憶ごとに変わりうるなら、クライアントは 3 × 4 = 12 回取り直す。一覧が接続に依らず鮮度の目安付きで返るなら、サーバーごとに1回、計4回で済む。
何が外れたのか
2026-07-28版の「Key Changes」は、2025-11-25版からの変更を列挙している1。外れたものから並べる。
仕様は、プロトコル層のセッションと、Streamable HTTPトランスポートの Mcp-Session-Id ヘッダを取り除いた。これに伴い、tools/list、resources/list、prompts/list の結果は接続ごとに変わらなくなった。仕様は、呼び出しを跨ぐ状態が要るサーバーは「明示的な、サーバーが発行するハンドルを、通常のツール引数として渡す」ものとしている(SEP-2567)1。
仕様は次に、initialize と notifications/initialized のハンドシェイクを取り除いた。変更履歴はこの項目に「MCPをステートレスにする」と見出しを付けている。ステートレスと呼んでいるのはプロトコル層であり、呼び出しを跨ぐ状態そのものを無くしたという意味ではない。その状態を指すのは、先に見たとおりサーバーが発行するハンドルであり、引数で運ばれるのはそのハンドルである1。各要求は、自分でプロトコルの版とクライアントの能力を運ぶ。運ぶ先は _meta で、キーは io.modelcontextprotocol/protocolVersion と io.modelcontextprotocol/clientCapabilities である。クライアントは要求ごとに io.modelcontextprotocol/clientInfo で自分を名乗ることが推奨され(SHOULD)、サーバーは各結果の _meta に io.modelcontextprotocol/serverInfo を載せることが推奨される(SHOULD)。版が食い違えば UnsupportedProtocolVersionError が返る(SEP-2575)1。
ハンドシェイクが消えた分、仕様は server/discover を足した。サーバーはこれを実装しなければならず(MUST)、対応するプロトコルの版、能力、身元を告知する。クライアントは他のどの要求よりも先にこれを呼んでもよい(MAY)1。
通知の受け取り方も変わった。仕様はHTTP GETのエンドポイントと resources/subscribe / resources/unsubscribe を、subscriptions/listen に置き換えた。これは一本の長命なPOSTレスポンスストリームで、クライアントは通知の種類ごとに受け取るかどうかを選ぶ1。
仕様は ping、logging/setLevel、notifications/roots/list_changed も取り除いた。ログの水準は、クライアントが要求ごとに _meta の io.modelcontextprotocol/logLevel で指定する1。
最後に、仕様はSSEストリームの再開機能とメッセージの再送、Last-Event-ID ヘッダとSSEのイベントIDを取り除いた。仕様はその帰結を書いている。「壊れたレスポンスストリームは処理中の要求を失う。クライアントはそれを、新しい要求IDを持つ新しい要求として出し直さなければならない(MUST)」1。ストリームの中断は、これまで再開の対象だった。これからは出し直しの対象になる。
外れたものの後に、置き換えられたものが一つある。仕様は、サーバーが roots/list、sampling/createMessage、elicitation/create のような要求を自分から送る従来の方式を取り除き、Multi Round-Trip Requests(MRTR)と呼ぶ型に置き換えた(SEP-2322)1。サーバーは resultType: "input_required" の結果を返し、処理に足りない情報の要求を inputRequests に載せる。クライアントは元の要求を出し直し、その答えを inputResponses に付ける1。あわせて、すべての結果に resultType 欄が必須になった1。開いたままのストリームに載っていた往復が、次の要求の中身へ移ったことになる。ハンドルとは別の場所で起きた、同じ向きの移動である。実験段階だった tasks も中核から公式の拡張 io.modelcontextprotocol/tasks へ出て、サーバーは要求ごとの同意なしにタスクのハンドルを返せるようになった(SEP-2663)1。
なぜセッションを外したのか
セッションを外した理由を、設計文書SEP-2567が書いている。文書が挙げる問題は三つある。寿命が決まっていないこと、ツール一覧を取っておけないこと、一つのセッションに一つの状態しか持てないことである。順に見る。
一つ目は寿命である。仕様は、セッションがいつ始まりいつ終わるかを決めていない。ホストのアプリケーションに任せているからである。その結果、同じ「セッション」という言葉で、クライアントごとに長さの違うものが作られている。文書が挙げる実例はこうである2。ChatGPTは、ツールを一回呼ぶたびに新しいセッションを作る。一回の呼び出しが終わればそのセッションも終わり、そこに預けた状態は次の呼び出しの前に消える。Claude.aiも最近まで同じだった。一方、デスクトップアプリやIDEの多くは、アプリを起動したときにセッションを一つ作り、終了するまで使い続ける。預けた状態は、起動中のすべての会話で共有され、再起動で消える。つまり同じ言葉が、一方では一回の呼び出しの間だけを、他方ではアプリを閉じるまでを指している。会話の単位でセッションを作るクライアントは少なく、切断や再起動のあとで前のセッションを再開するクライアントもほとんど無い2。
何をセッションに結びつけるかを決めるのは、サーバーを書く側である。正しく決めるには、セッションが何に当たるかを知らなければならない2。たとえばブラウザを一つ開いてセッションに結びつけるサーバーは、そのセッションが利用者の一回の発話なのか、一つのエージェントプロセスなのか、長い会話一つなのかを知る必要がある2。仕様はこれを決めておらず、ホストごとに答えが違う。文書はこの状態を、サーバーが「自分の制御しない意味論を持つ抽象に対して設計している」と言う2。
二つ目はツール一覧である。tools/list の結果はセッションの中で変わってよいと仕様が許していたので、クライアントは前のセッションで取った一覧を次のセッションでも有効だと見なせなかった。実際に変わるかどうかは関係ない。ツール一式が構築時に固定で一度も変わらないサーバーでも、クライアントにはそれが事前に分からないので、取り直すしかない2。文書はこう書く。「クライアントは、サーバーがセッション単位の変更を選んでいないと知らないかぎり、セッションの境界を跨いで tools/list をキャッシュできない」2。文書によれば、ツール一式が固定されたサーバーのほうが普通である2。サブエージェントを多く立てるホストでは、この取り直しがサブエージェントの数とサーバーの数の積で増える2。サブエージェントが短命で数が多ければ、この取り直しは実際のツール呼び出しの通信量を超えうる2。
三つ目は数である。セッションが持てる状態は、セッション一つにつき一つと決まっていた。買い物かごなら一つ、ブラウザなら一つで、二つ持つことも、持たないこともできない。文書が挙げるのは、複数のサブエージェントに商品を調べさせる場面である。かごは一つを共有したいが、ブラウザはそれぞれ別に持たせたい。親のセッションを共有すればブラウザがぶつかり、別々のセッションを与えればかごが分かれる。どちらの境界も両方を満たさない2。
三つを並べると、意図が読める。セッションはプロトコルが用意した入れ物でありながら、いつまで持つか、中身を当てにしてよいか、いくつ持てるかのどれも、サーバーを書く側の思いどおりにならなかった。文書はこの抽象を、見合う利得の無いまま制約を足しているものだと結論する2。だから、任意にして残すのではなく外す。任意にしても、クライアントはどのサーバーがセッションを使うか事前に知れないので、一覧の取り直しは消えないからである2。代わりに置くのは、状態に名前を付けて表に出す形である。次節で見る。
代わりに何を置いたのか
文書が示すのは、プロトコルの機能ではなく、ツールの設計の型である。「サーバーがこれまで暗黙のセッション単位の状態に頼っていたところでは……代わりに、ハンドルを返す明示的な生成ツールを公開する」2。文書が挙げる例は買い物かごである。create_basket() が basket_id を返し、モデルはそれを後続の add_item(basket_id, ...) の呼び出しへ引き渡していく2。
仕様が定めたのは、セッションを外すことのほうである。ハンドルを発行して引数で運ぶ仕組みは、サーバーを書く側がツールの形として自分で作る。プロトコルはハンドルを型として持たない。ただし定めが無いことと、書かれていないことは別である。同じ設計文書は規範ではない指針として、ハンドルの形・寿命・検証をまとめて置いている2。
もっとも同じ改版は、外すだけでなく、前節の二つ目の問題に答えるものを仕様の側で一つ足している。tools/list、prompts/list、resources/list、resources/read、resources/templates/list の結果に、ttlMs と cacheScope の二つの欄が必須になった(SEP-2549)1。ttlMs はミリ秒単位の鮮度の目安で、クライアントはこれを見て応答をキャッシュし、取り直しを減らせる。cacheScope は public か private で、共有の中間層が応答をキャッシュしてよいかを決める1。サーバーは tools/list のツールを決定的な順序で返すことも推奨された(SHOULD)1。設計文書は二つの関係をこう書く。こちらは変動の源としてのセッションを取り除いて、キャッシュできる安定した対象を作る。SEP-2549は、それをどれだけ持ってよいか、いつ無効にするかを決める2。
文書は自分で調べた数も載せている。オープンソースのMCPサーバーから1,000リポジトリを無作為に抜き、リポジトリごとにLLMで分類した自動調査である2。90.0%は、アプリケーション水準でセッションIDを参照していなかった。セッションIDをアプリケーションの意味に使っていたのは三つの区分で、合わせて3.7%である2。内訳は「セッションをキーにしたアプリケーション状態」が2.5%、「プロキシ/ゲートウェイのスティッキールーティング」が0.7%、JWTのクレームやPKCEの検証子をセッションに紐づける「認証の結び付け」が0.5%である2。最後の0.5%には、サーバーが生成するノンスかトークンの主体へ置き換える、という移行先が付いている2。この数は文書自身のLLMによる分類であり、本稿の論を支える土台ではない。論のほうは構造にある。状態はサーバーに残り、それを指す名前が、トランスポートのヘッダからモデルの引数へ移ったという構造である。
運用側が得たもの
状態を指す名前が接続の外へ出ると、サーバーを載せる側の要件も動く。Cloudflareのブログ記事、2026年8月6日公開、著者Matt Careyは、自社の基盤で何が要らなくなったかを書いている3。
Careyの記述はこうである。「MCPサーバーはいまやWorkerだけで動かせる。ステートフルなインフラは要らない」3。セッションを保持するために使われてきた McpAgent が要らなくなる。ただしCareyは同じ段落で、アプリケーション自身が状態を必要とするならDurable Objectsは依然として適切な基本機構だと限定し、記事末尾でも繰り返している3。合わせて「スティッキーセッション、ストリームを開いたままにしておくこと、メッセージの再送」も要らなくなり、サーバーは要求の単位で確保するインフラの上に載る3。
この利得は、仕様が外したものと一つずつ対応している。ハンドシェイクとセッションが外れたから、状態を共有ストレージへ置いたサーバーの前では、要求を特定のプロセスへ固定するスティッキーセッションが要らない2。仕様がストリームの再開とメッセージの再送を外したから、再送のための保持が要らない。仕様が外したものが、そのまま運用側で要らなくなったものになっている。ただし設計文書は、HTTPをstdioへ橋渡しして一つのセッションに一つの子プロセスを立てるゲートウェイを、機械的な書き換えでは済まない区分として残している2。
ハンドルを守る仕事は誰のものか
同じ移動を、逆の側から読む記事もある。API・MCPの侵入テストを販売するEquixlyが2026年8月5日に出した記事で、この変更が安全の側に何を持ち込むかを書いている4。
Equixlyもまず、状態そのものは消えない、と書く4。そのうえで、ツールが明示的なハンドルを発行し、モデルが次の要求の引数として返す型を、トランスポートの層に隠れたセッション状態より清潔だと評価している4。留保が付くのはその後である。「能力への参照は、いまやモデルのコンテキストを通って運ばれ、モデルからも、モデルの入力に影響できるあらゆるものからも見える」4。以前は、セッションIDがトランスポートの層にあった。モデルはそれを見なかった。ハンドルはツール引数なので、モデルの入力の中にある。トランスポートが隠していたものが、モデルの入力へ出てきた。設計文書も同じ露出を主要な安全上の考慮として挙げ、「露出面の変化であって、新しい種類の脆弱性ではない」と判断している2。根拠として文書は、Python SDKのセッション管理を例に、漏れたセッションIDでも別の認証主体による乗っ取りが起きうると書く2。呼び出しごとに (handle, auth_context) を検証するサーバーは、ハンドルが露出しても影響を受けない、とも書く2。
そこから実装者の仕事が増える。Equixlyは、MRTRでサーバーが input_required 結果に付ける requestState がクライアントを往復する点を「攻撃者が操作しうる入力」と呼び、認可に効かせる前に完全性を保護し、「それを主体と発行元の要求に結び付け、有効期限を与える」ことを求めている4。これはハンドルそのものではなく、往復する状態への処方である。ハンドルのほうは、SEP-2567が「サーバーへの指針」として、ハンドルを不透明にすること、認証のあるサーバーでは呼び出しごとに (handle, auth_context) を検証すること、認証の無いサーバーでは128ビット以上の暗号論的乱数で生成して寿命を区切ることを挙げている2。いずれも規範ではないので、守るかどうかはサーバーを書く側に残る。
冪等性も同じ場所へ落ちる。ストリームが壊れて出し直された要求は、新しい要求IDを持つ別の要求として届く1。プロトコルの中核は冪等性キーを用意していない。支払いや削除が二度実行されるかどうかは、下のツールが重複に気づくよう書かれているかで決まる4。
ゲートウェイの見え方も変わる。Equixlyは、Mcp-Method と Mcp-Name が必須ヘッダになったことで、ゲートウェイやWAFがJSON-RPCの本文を解かずに経路と認可を決められるようになったと書き、これを「ツール単位のポリシーがエッジで実用になる」と評価する4。中間層は制御を失ったのではなく、本文を開かずに効かせる手がかりを得た。ツール引数も、サーバーがスキーマで x-mcp-header を付ければ Mcp-Param-* ヘッダへ写される。同記事は、そのヘッダに当てるポリシーは被監視側のサーバーが露出を選んだ欄に掛かる、と注意している4。
必須になったヘッダが本文と食い違う場合の扱いも、Equixlyは挙げている。実装はヘッダが要求の本文と食い違う場合にこれを拒否し、400 Bad Request とJSON-RPCエラー -32020, HeaderMismatch を返さなければならない4。Equixlyはそのうえで、試すべき面はヘッダと本文が食い違いうるかどうかではないと書く。実装がその比較を本当に行っているか、そしてその下の降格経路である4。同記事は、ヘッダを信じる中間層には、宣言された版がヘッダと本文の照合を求める版かを確かめるよう求めている4。照合の要件を書いたのは仕様だが、照合を実際に行うかどうかは実装しだいである。
Equixly自身も、仕様の変更で何が片づかないかを書いている。著者らはこう書く。「私たちがMCPサーバーに見つける脆弱性は、サーバーが公開している機能の側にある。仕様はそこに手を触れていない」4。仕様の変更は攻撃面の形を変えるが、サーバーが何を実行できるかという問題は仕様の外に残る。
移行の段取り
既存のサーバーは、この変更をいつどう受け取るか。
セッションの除去には廃止期間が無い。設計文書は、セッションが次の版で一斉に除去され、セッション単位の状態に頼るサーバーはハンドルへ移行するまで今の版にとどまって版の交渉で共存する、と書いている2。いま最も多い状態つきのサーバーは、プロセスの寿命に頼るstdioのサーバーで、既定の配置ではこの変更で壊れない2。それでも文書は、プロセスの寿命に頼らずハンドルへ移ることを推奨している(SHOULD)2。
仕様が用意した期間の枠は、非推奨に入った機能のためのものである。機能のライフサイクルと廃止方針を定め、Active、Deprecated、Removedの三つの状態と、最低12か月の廃止期間を置いている1。非推奨(仕様には残るが、新しい実装は採用しない扱い)に入ったのはRoots、Sampling、Logging、そしてOAuth 2.0の動的クライアント登録である1。HTTP+SSEトランスポートと、Samplingの includeContext の値 thisServer と allServers は、以前から非推奨だったものがこの方針の下へ分類し直された1。動的クライアント登録の後継はClient ID Metadata Documentsで、それに対応しない認可サーバーのために後方互換として残る1。Careyは、新しい実装では非推奨であり、2027年の夏以降に除去される予定だと書いている3。
移行先も仕様が挙げている。Rootsについては、ディレクトリやファイルをツールのパラメータ、リソースURI、サーバーの設定で渡す。Samplingについては、LLMプロバイダのAPIと直接つなぐ。Loggingについては、stderr へ書くか、OpenTelemetryを使う1。
Careyは、切り替えの手順を書いている。従来のプロトコルのセッション、サーバー起点の要求、単独のストリームのどれかに頼っているサーバーには「より慎重な移行」が要る、というのが出発点である3。Careyが挙げる順序はこうである。厳密にステートレスな経路を、既存のセッションつき経路の隣に立てる。機能をそちらへ移す。動いているセッションが自然に終わるのを待つ。廃止期間のうちに旧経路を消す3。Equixlyは、古い版のクライアントが動き続けるのは旧経路を新しい経路と並べて動かす場合だけだと書いている4。Careyはまた、elicitationの型が変わったことを挙げている。以前のサーバー起点の要求が、開いたままのストリームに依存していたためである3。
設計文書は、移行で失われうるものを一つ、予想される反論として自分で挙げている。セッションが与えていた自動のライフサイクルの合図、つまり「セッションが終われば状態が解放される」という合図である2。ハンドルには、終わりの合図が付いてこない。文書はこの反論をすぐに退けている。「セッションは実際にはこれを確実には届けていない」2。サーバーはすでにTTLによる失効に頼っており、掃除をしているのはセッションの境界ではない、というのが根拠である2。文書はここから、寿命を書いた明示的なIDは「同じ仕組みを明示にしただけ」だと結論する2。ハンドルの寿命をツールの説明に書くのは、サーバーを書く側である2。
文書は運ぶ側の費用も挙げている。モデルがIDを少し違えて書くことと、会話の要約でIDがコンテキストから落ちることである。文書は、いまのモデルは識別子を確実に運んでおり、難しいのは要約のほうだとする2。ただし要約でツールの結果が落ちれば、セッションに預けたかごの中身も同じく見失う、とも文書は書く2。クライアントの主な責任は、要約をまたいでハンドルを残すことだと文書は書く2。文書は逆の面も挙げている。ハンドルはツールの結果に載って会話の記録に残るので、会話を保存するクライアントなら、アプリの再起動や別の端末のあとでも同じハンドルがモデルの前に戻り、別の会話や別のエージェントへも渡せる2。
出典4件
-
Model Context Protocol「Key Changes」仕様リビジョン 2026-07-28. https://modelcontextprotocol.io/specification/2026-07-28/changelog — 2025-11-25版からの変更。セッション・ハンドシェイク・ストリーム再開の除去、server/discover、MRTR、ttlMs・cacheScope、最低12か月の廃止期間を持つライフサイクルを並べる。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15 ↩16 ↩17 ↩18 ↩19 ↩20 ↩21 ↩22 ↩23
-
SEP-2567「Sessionless MCP」modelcontextprotocol リポジトリの設計文書. https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/seps/2567-sessionless-mcp.md — セッションを外す三つの理由、ハンドルの型と規範でない指針、予想される反論への答え、1,000リポジトリのLLM分類による自動調査(90.0%が未参照)を書く。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15 ↩16 ↩17 ↩18 ↩19 ↩20 ↩21 ↩22 ↩23 ↩24 ↩25 ↩26 ↩27 ↩28 ↩29 ↩30 ↩31 ↩32 ↩33 ↩34 ↩35 ↩36 ↩37 ↩38 ↩39 ↩40
-
Matt Carey「The next generation of MCP」Cloudflare, 2026年8月6日. https://blog.cloudflare.com/mcp-v2/ — ステートレス化で McpAgent やスティッキーセッションが不要になったという自社基盤での報告と移行の手順。著者は同社のシニアシステムエンジニアで、Worker を提供する立場から書かれている。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9
-
Equixly「Stateless MCP: What the 2026-07-28 specification changes for security」2026年8月5日. https://equixly.com/blog/2026/08/05/stateless-mcp/ — ハンドルがモデルの入力へ出る点と、requestState・ヘッダ照合の処方。侵入テストの販売元の、技術コンテンツ担当とCTO兼創業者による記事。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14
この記事はAIを使用して執筆されました。内容には誤りが含まれる可能性があります。事実や数字の出典は脚注から確かめられます。