In Silico

AIエージェント

MCPのステートレス化で状態はどこへ?セッション廃止の理由と移行

2026/9/9 (更新: 2026/11/3)

  • MCP
  • AIエージェント
  • ハンドル
  • セッション
  • ステートレス
  • Model Context Protocol
  • SEP-2567
  • MRTR
  • ツール引数
  • キャッシュ
  • スティッキーセッション
  • 廃止期間
  • ゲートウェイ
  • Streamable HTTP
  • Cloudflare
  • Durable Objects
  • サブエージェント
  • 買い物かご
  • OpenTelemetry
  • ChatGPT
  • JSON-RPC
目次
背景・問い・要点
背景

繰り返し届く要求に答えるプログラムは、前の要求で何が起きたかを覚えておく必要がある場面が多い。買い物かごに品物を足す処理を考えれば分かりやすい。二回目の呼び出しは、一回目でかごに何が入ったかを知らなければ答えを出せない。だから設計者は、この記憶をどこかに置く。

置き方の一つは、記憶を接続の中に持たせるやり方である。つながっている間だけ、両側が同じ記憶を暗黙のうちに共有する。呼び出す側は記憶の名前を書かなくてよいし、答える側は直前の要求の続きから処理を始められる。

このやり方はつながっている間は働くが、代償を伴う。一つ一つの要求が、特定のサーバープロセスに結びつく。接続が切れれば記憶は消え、プロセスが再起動しても消える。呼び出す側は、その記憶がまだ生きているかどうかを外から確かめられない。設計者は、記憶の置き場所を決めなければならない。

MCP(Model Context Protocol)は、AIエージェントのモデルに外部のツールやデータを渡すための共通の取り決めである。MCPは、記憶を接続の中に持たせるこのやり方を、プロトコル層のセッション(一つの接続に紐づく、複数の呼び出しにまたがる状態の入れ物)として持っていた1。

問い

MCPはセッションを外したあと、状態をどこに置いたのか。

要点

MCPの状態はサーバーに残っている。2026-07-28版の仕様がセッションを外したあと、その状態を指すハンドル(サーバーが発行し、呼び出しごとに添える識別子)は、モデルがツール引数として運ぶ1。

仕様がセッションを外した理由は、セッションの寿命と範囲がクライアントごとに違うこと、クライアントがツール一覧をセッションを跨いで取っておけないこと、一つのセッションが状態を一つしか持てないことだ2。セッションが外れたことで、サーバーを動かす基盤の側は、セッションを保持するための仕組みが要らなくなる3。その一方で、トランスポート(要求を運ぶ通信の層)に隠れていたセッションIDと違い、ハンドルはモデルの入力に現れる2。

モデル・例示

メモを覚えるサーバーを、記憶の寿命が違う二つのクライアントから呼ぶ

※ この節の例は、説明のために作った架空のものです。

状態を接続の記憶に持たせると、記憶の寿命がクライアントごとに違うので、同じサーバーでも返る答えが変わる。状態に名前を付けて引数で運ぶと、その差は消え、代わりに名前がモデルの読む入力に現れる。

メモに一行足すツールと、メモを読み出すツールを持つサーバーを考える。サーバーはメモを接続の記憶に結びつけている。このサーバーを、記憶の寿命が違う二つのクライアントから呼ぶ。

クライアントAは、ツールを一回呼び出すたびに新しい記憶を作る。「牛乳」を足してから読み出すと、二回目の呼び出しは新しい記憶の上で動くので、空のメモが返る。クライアントBは、アプリを起動したときに記憶を一つ作り、終了まで使い続ける。会話1で「牛乳」を足し、会話2で読み出すと、会話2には頼んでいない「牛乳」が返る。同じサーバーが、Aでは忘れすぎ、Bでは混ぜすぎる。

メモに名前を付ける形に変えると、この差は消える。メモを作るツールが memo-41 という名前を返し、モデルは足すときも読むときもその名前を引数に添える。Aでも二回目の呼び出しは memo-41 を指すので「牛乳」が返る。Bでも、会話2が自分で memo-42 を作れば別のメモを持てる。メモそのものは前と同じくサーバーにあり、サーバーはそれを接続の記憶ではなく自分の保存先に置いて、届いた名前で探す。変わったのは、それを指す名前を誰が運ぶかであり、その代わりに memo-41 という文字列が、モデルの読む入力の中に現れる。

ツールの一覧の取り直しも数えられる。サブエージェント3つが、それぞれサーバー4つにつなぐとする。一覧が記憶ごとに変わりうるなら、クライアントは 3×4=123 \times 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、SEPはSpecification Enhancement Proposalの略)1。

仕様は次に、initialize と notifications/initialized のハンドシェイクを取り除き、変更履歴はこの項目に、MCP(Model Context Protocol)を「ステートレスにする」と見出しを付けている。ステートレスになったのはプロトコル層であり、呼び出しを跨ぐ状態そのものが無くなったのではない。その状態は、上の段落の、サーバーが発行するハンドルで指される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(Server-Sent Events)ストリームの再開機能とメッセージの再送、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(Integrated Development Environment)の多くは、アプリを起動したときにセッションを一つ作り、終了するまで使い続ける。預けた状態は、起動中のすべての会話で共有され、再起動で消える。つまり同じ言葉が、一方では一回の呼び出しの間だけを、他方ではアプリを閉じるまでを指している。会話の単位でセッションを作るクライアントは少なく、切断や再起動のあとで前のセッションを再開するクライアントもほとんど無い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-2567は変動の源としてのセッションを取り除いて、キャッシュできる安定した対象を作る。SEP-2549は、それをどれだけ持ってよいか、いつ無効にするかを決める2。

文書は自分で調べた数も載せている。オープンソースのMCPサーバーから1,000リポジトリを無作為に抜き、リポジトリごとにLLM(Large Language Model)で分類した自動調査である2。90.0%は、アプリケーション水準でセッションIDを参照していなかった。セッションIDをアプリケーションの意味に使っていたのは三つの区分で、合わせて3.7%である2。内訳は「セッションをキーにしたアプリケーション状態」が2.5%、「プロキシ/ゲートウェイのスティッキールーティング」が0.7%、JWT(JSON Web Token)のクレームやPKCE(Proof Key for Code Exchange)の検証子をセッションに紐づける「認証の結び付け」が0.5%である2。最後の0.5%には、サーバーが生成するノンスかトークンの主体へ置き換える、という移行先が付いている2。

運用側が得たもの

状態を指す名前が接続の外へ出ると、サーバーを動かす基盤の側(以下、運用側)が用意すべきものも変わる。Cloudflareのブログ記事、2026年8月6日公開、著者Matt Careyは、自社の基盤で何が要らなくなったかを書いている3。

Careyの記述はこうである。「MCPサーバーはいまやWorkerだけで動かせる。ステートフルなインフラは要らない」3。セッションを保持するために使われてきた McpAgent が要らなくなる。ただしCareyは同じ段落で、アプリケーション自身が状態を必要とするならDurable Objectsは依然として適切な基本機構だと限定し、記事末尾でも繰り返している3。合わせて「スティッキーセッション、ストリームを開いたままにしておくこと、メッセージの再送」も要らなくなり、サーバーは要求の単位で確保するインフラの上に載る3。

この利得は、仕様が外したものと一つずつ対応している。ハンドシェイクとセッションが外れたから、状態を共有ストレージへ置いたサーバーの前では、要求を特定のプロセスへ固定するスティッキーセッションが要らない2。仕様がストリームの再開とメッセージの再送を外したから、再送のための保持が要らない。仕様がサーバー起点の要求をMRTRに置き換えたから、要求の途中で情報を問い合わせるためにストリームを開いておく必要もない3。仕様が外したものが、そのまま運用側で要らなくなったものになっている。

要らなくならなかったものもある。長命のストリームそのものは無くなっていない。変更の通知は、subscriptions/listen の一本の長命なストリームで届く1。また設計文書は、HTTPをstdioへ橋渡しして一つのセッションに一つの子プロセスを立てるゲートウェイを、機械的な書き換えでは済まない区分として残している2。

ハンドルを守る仕事は誰のものか

セッションIDからハンドルへの移動は、安全の側にも影響する。設計文書SEP-2567は、この変更が持ち込む主な安全上の考慮として、ハンドルがセッションIDの現れなかった場所に出ることを挙げる。会話の記録、サブエージェントへの指示、コピー&ペーストのバッファ、ときには他の利用者の画面である2。以前は、セッションIDがトランスポートの層にあった。モデルはそれを見なかった。ハンドルはツール引数なので、モデルの入力の中にある。トランスポートが隠していたものが、モデルの入力へ出てきた。

文書はこれを「露出面の変化であって、新しい種類の脆弱性ではない」と判断している2。根拠として文書は、Python SDK(Software Development Kit)のセッション管理を例に、漏れたセッションIDでも別の認証主体による乗っ取りが起きうると書く2。呼び出しごとに (handle, auth_context) を検証するサーバーは、ハンドルが露出しても影響を受けない、とも書く2。

ハンドルのほかにも、クライアントを往復するものがある。MRTRでサーバーが input_required の結果に付けてよい requestState である。仕様はこれをサーバーにだけ意味を持つ不透明な文字列とし、サーバーは状態をどんな形式で書き込んでもよい4。往復の間の状態を、要求の中身に書き込んで運べるということである。そのため仕様は、requestState を攻撃者が操作しうる入力として扱うことをサーバーに求めている(MUST)。認可や業務の判断に効くなら完全性を保護し、検証に通らないものは拒否しなければならない(MUST)4。再利用を防ぐために、認証された主体、短い有効期限、発行元の要求の識別子を中に含めて受け取るたびに確かめることも推奨される(SHOULD)4。

HTTPのヘッダにも規範が足された。仕様はStreamable HTTPのPOST要求に、標準のヘッダ Mcp-Method と Mcp-Name を必須にした。ツールのパラメータから独自のヘッダを作る x-mcp-header にも対応した(SEP-2243)1。JSON-RPCのエラー HeaderMismatch の番号は -32020 に改められ、それまでトランスポートの説明文にしか無かった HeaderMismatchError がスキーマに加わった1。

ハンドルそのものについては、SEP-2567が「サーバーへの指針」として、ハンドルを不透明にすること、認証のあるサーバーでは呼び出しごとに (handle, auth_context) を検証すること、認証の無いサーバーでは128ビット以上の暗号論的乱数で生成して寿命を区切ることを挙げている2。いずれも規範ではないので、守るかどうかはサーバーを書く側が決める。

移行の段取り

既存のサーバーは、この変更をいつどう受け取るか。

セッションの除去には廃止期間が無い。設計文書は、セッションが次の版で一斉に除去され、セッション単位の状態に頼るサーバーはハンドルへ移行するまで今の版にとどまって版の交渉で共存する、と書いている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。Careyはこの廃止期間がどの機能のものかを書いておらず、セッションの除去そのものには廃止期間が無い2。Careyはまた、elicitationの型が変わったことを挙げている。以前のサーバー起点の要求が、開いたままのストリームに依存していたためである3。

設計文書は、移行で失われうるものを一つ、予想される反論として自分で挙げている。セッションが与えていた自動のライフサイクルの合図、つまり「セッションが終われば状態が解放される」という合図である2。ハンドルには、終わりの合図が付いてこない。文書はこの反論をすぐに退けている。「セッションは実際にはこれを確実には届けていない」2。サーバーはすでにTTL(Time To Live)による失効に頼っており、状態を解放しているのはセッションの境界ではない、というのが根拠である2。文書はここから、寿命を書いた明示的なIDは「同じ仕組みを明示にしただけ」だと結論する2。ハンドルの寿命をツールの説明に書くのは、サーバーを書く側である2。

文書は運ぶ側の費用も挙げている。モデルがIDを少し違えて書くことと、会話の要約でIDがコンテキストから落ちることである。文書は、いまのモデルは識別子を確実に運んでおり、難しいのは要約のほうだとする2。ただし要約でツールの結果が落ちれば、セッションに預けたかごの中身も同じく見失う、とも文書は書く2。クライアントの主な責任は、要約をまたいでハンドルを残すことだと文書は書く2。文書は逆の面も挙げている。ハンドルはツールの結果に載って会話の記録に残るので、会話を保存するクライアントなら、アプリの再起動や別の端末のあとでも同じハンドルがモデルの前に戻り、別の会話や別のエージェントへも渡せる2。


出典4件
  1. 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 ↩24 ↩25

  2. SEP-2567「Sessionless MCP via Explicit State Handles」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 ↩41 ↩42 ↩43

  3. 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 ↩10

  4. Model Context Protocol「Multi Round-Trip Requests」仕様リビジョン 2026-07-28. https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr — requestState を攻撃者が操作しうる入力として扱い、完全性を保護し(MUST)、主体・期限・発行元の要求を照合する(SHOULD)よう求める。 ↩ ↩2 ↩3

この記事はAIを使用して執筆されました。内容には誤りが含まれる可能性があります。事実や数字の出典は脚注から確かめられます。