ステートレスなMCPサーバーへの変更点と移行の注意点
MCPの2026-07-28リビジョンでセッションとinitializeハンドシェイクが廃止されました。本記事では、ステートレス化に伴うリバースプロキシの構成変更、ヘルスチェックの最適化、タイムアウト処理、認証の実装方法について技術的な詳細を解説します。
ステートレスな MCP サーバーとは
ステートレスな MCP サーバーは、リクエスト間でクライアントごとの状態を保持しません。すべてのリクエストには、プロトコルバージョン、クライアントの機能、およびサーバーが応答するために必要な認証情報が含まれているため、どのマシンのどのプロセスでもリクエストに応答できます。MCP (Model Context Protocol、エージェントがツールにアクセスするためのワイヤフォーマット) は、リビジョン 2026-07-28 でこれをルール化し、その下層にあった initialize ハンドシェイクと HTTP セッションを廃止しました。ここではそのワイヤのサーバー側について解説します。もしエージェント側がまだ不慣れであれば、AI エージェント学習の段階的パスで、HTTP の詳細が重要になる前にツールを呼び出す判断を行うループについて説明しています。
これが運用の要点です。クライアントごとの状態を保持しないサーバーは、セッションアフィニティのない一般的なロードバランサーの背後に配置でき、デプロイ中に再起動してもクライアントを中断させず、1 つではなく 4 つの同一プロセスとして実行できます。セッション指向のサーバーは、追加の仕組みなしではこれらを実行できません。
Model Context Protocol はステートレスなプロトコルです。リクエストを処理するために必要なすべての情報は、リクエスト自体に含まれています。サーバーは各リクエストを独立して処理します。同じ接続やストリーム上のリクエストであっても、以前のリクエストから状態を推測してはなりません。
ステートレスとは、サーバーが何も保存しないという意味ではありません。データベース、キュー、キャッシュはすべて引き続き存在します。これは、接続上で状態を保持する情報をプロトコルが持たないという意味です。そのため、サーバーは接続、プロセス、オープンソケットを「会話の途中にいるこのクライアント」の代わりとして扱ってはいけません。すでにデータを管理しているアプリケーションを見ると、この違いが分かりやすくなります。openGym の読み取り専用 MCP server は、アプリケーション自身のデータベースに保存されたトレーニング履歴について質問に回答します。そのデータの保存方法は、リクエストがどの接続から到着したかに依存しません。
2026-07-28 のリビジョンで削除されたもの
2026-07-28 は 2026 年 8 月時点の仕様の最新リビジョンです。2025-11-25 と比較すると、セッションをサポートするために存在していた 5 つの要素が削除されています。
initializeリクエストおよびnotifications/initialized通知。ハンドシェイクは完全に廃止されました (SEP-2575)。Mcp-Session-Idヘッダー、および HTTPDELETEを使用したセッション終了 (SEP-2567)。- サーバーが通知をプッシュしていたスタンドアロンの HTTP
GETストリーム。これは、レスポンスが長時間継続するストリームとなる通常の POST であるsubscriptions/listenに置き換えられました。 - SSE (server-sent events) ストリームの再開機能。
Last-Event-IDヘッダーとイベントごとの ID は削除されました。そのため、ストリームが切断されると処理中のリクエストは失われ、クライアントは新しいリクエスト ID を付与して新規リクエストとして再発行する必要があります。 ping、logging/setLevel、およびnotifications/roots/list_changed。ログレベルは現在、_meta内のio.modelcontextprotocol/logLevelというリクエストごとのフィールドとして定義されています。
1 つのメソッドが追加され、すべてのサーバーで実装が必須となりました。server/discover は、サーバーがサポートするプロトコルバージョン、機能、および識別情報を 1 回の呼び出しで返します。これは残されたハンドシェイクに最も近いものですが、クライアントによる呼び出しは任意です。
セッション転送の運用が困難だった理由
2025-11-25以前のバージョンでは、サーバーは初期化時にセッションIDを発行し、InitializeResult上のMcp-Session-Idヘッダーで返していました。クライアントは、その後のすべてのリクエストでそのヘッダーを送信する必要がありました。ネゴシエーションされたプロトコルバージョンやクライアントの機能は、そのIDをキーとしてサーバーのメモリ上に保持されていました。これらの仕様には、運用上のコストが伴います。
- 再起動によりセッションテーブルが破棄されます。仕様上、サーバーは無効なセッションIDを含むリクエストに対して
404 Not Foundで応答する必要があり、クライアントは新しいInitializeRequestでやり直す必要がありました。デプロイのたびに、接続中の全クライアントで再接続が発生していました。 - 2台目のレプリカは、1台目のレプリカのセッション情報を認識できません。スケールアウトするには、ロードバランサーでのスティッキーセッション設定か、すべてのレプリカがリクエストごとに読み取る共有セッションストアが必要でした。
- セッションテーブルは、アイドル状態のクライアントが増えるほど肥大化するメモリ領域でした。
DELETEはオプションであり、送信せずに切断したクライアントのデータが残存していました。 - リスト結果が接続ごとに異なる可能性があるため、サーバーの前段でのキャッシュは安全ではありませんでした。
セッションを廃止することで、これら4つの問題が同時に解消されます。設定を変更する前に、この変更の意義を理解しておくことが重要です。
すべてのリクエストに含まれる情報
MCP エンドポイントへの各 POST リクエストは独立しています。プロトコルバージョンとクライアントの機能はリクエストボディの _meta に含まれ、選択されたフィールドは HTTP ヘッダーにもミラーリングされます。これにより、仲介サーバーは JSON を解析することなくルーティングを行えます。
POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: get_weather
Authorization: Bearer <access token>
{
"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": {}
}
}
}io.modelcontextprotocol/protocolVersion および io.modelcontextprotocol/clientCapabilities はすべてのリクエストで必須です。clientInfo は必須ではありませんが、クライアントは送信すべきです。必須フィールドが欠落しているリクエストは不正な形式とみなされるため、サーバーは JSON-RPC エラー -32602 および HTTP 400 Bad Request を返して拒否する必要があります。
Mcp-Method ヘッダーはすべてのリクエストで必須です。Mcp-Name は tools/call、resources/read、および prompts/get で必須となります。ヘッダーの値はボディと一致している必要があり、ボディを処理するサーバーは不一致を検知した場合、400 Bad Request およびエラーコード -32020、HeaderMismatch を返して拒否しなければなりません。このルールが存在する理由は、ヘッダーに基づいてルーティングを行うロードバランサーと、ボディに基づいて実行を行うサーバーという、2 つの異なる信頼の源泉が存在するためです。これらのヘッダーに基づいてルーティングやレート制限を行う場合は、まず MCP-Protocol-Version を確認してください。以前のリビジョンではヘッダーとボディの検証が行われていなかったため、それらのバージョンではヘッダーの値は信頼できません。
バージョンの不一致は、ハンドシェイクの失敗ではなく、リクエストごとの一般的なエラーとして扱われるようになりました。要求されたバージョンを実装していないサーバーは 400 Bad Request を返し、エラー -32022、UnsupportedProtocolVersion を提示した上で、サポートしているバージョンを data.supported にリストアップします。クライアントはそのリストから一つを選択して再試行します。
状態の所在:トークン、カーソル、サブスクリプション
状態は消失したわけではありません。可視化およびログ記録が可能な場所へ移行しました。
認証情報はすべてのリクエストに付随します。 アイデンティティを紐付けるセッションは存在しないため、アクセストークンは各 HTTP コールに乗せて送信され、その都度検証されます。詳細は以下の認証セクションを参照してください。
カーソルは自身の位置情報を保持する必要があります。 tools/list、resources/list、prompts/list、resources/templates/list におけるページネーションでは不透明なカーソル文字列を使用します。クライアントはこれを解析したり変更したりしてはなりません。単一プロセスのサーバーでは、セッションをキーとしてオフセットをメモリ内に保持するのが一般的でした。セッションがない環境では、どのレプリカでもリストの再開ができるよう、カーソル自体に位置情報をエンコードして署名するか、すべてのレプリカが共有するストレージに保持する必要があります。無効なカーソルに対しては -32602 を返してください。不透明なカーソルであってもクライアントから提供される入力であり、コードがデコードして信頼する対象となるため、必ず署名を行ってください。
サブスクリプションは接続ではなくリクエストに属します。 変更通知を希望するクライアントは、必要なタイプを指定したフィルタと共に subscriptions/listen を送信します(例:toolsListChanged、promptsListChanged、resourcesListChanged、resourceSubscriptions)。サーバーは notifications/subscriptions/acknowledged で応答し、そのレスポンスストリームを開いたまま維持します。ストリームが切断された場合、サーバー側では何も保持されないため、クライアントは再度 subscriptions/listen を送信して再開します。
コールを跨ぐアプリケーション状態は明示的なハンドルとなります。 サーバーがコール間で何らかの情報を保持する必要がある場合、仕様上の回答は、サーバーが発行した識別子を通常のツール引数として受け渡すことです。これはツールスキーマに現れ、ログ記録が可能であり、接続によって暗黙的に決定されることはありません。セルフホスト型の MCP メールサーバーのように、ユーザーごとのデータを扱うサーバーでは、セッションの代わりにこのパターンを使用します。メールボックスや下書きの識別子がツール引数となるため、どのレプリカでも次のコールを処理できます。一方で、ハンドルを一切必要としないツールも多数存在します。独自の SearXNG インスタンスを利用した検索ツールは、クエリを受け取って結果を返すだけであり、次のコールで再開すべき状態はなく、どのレプリカが応答したかを気にする必要もありません。
デプロイメント: リバースプロキシ、タイムアウト、ヘルスチェック
MCP エンドポイントは POST を受け付ける単一のパスです。トラフィックの大半は短いリクエストと JSON レスポンスであり、どのようなプロキシでも処理可能です。例外はストリーミングレスポンスであり、プロキシのデフォルト設定が障害となる場合があります。これは、ラップトップでのデモ環境から VPS 上で動作する MCP サーバー へ移行する際に変更が必要となる部分です。
location /mcp {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_read_timeout 1h;
proxy_send_timeout 1h;
}proxy_buffering off は重要です。Nginx はデフォルトでプロキシされたレスポンスをバッファリングするため、バッファが満杯になるかレスポンスが終了するまで SSE イベントを保持してしまうからです。仕様ではサーバーが SSE レスポンスで X-Accel-Buffering: no を送信することが求められており、Nginx はこのヘッダーを尊重します。そのため、適切なサーバーであればプロキシに対して正しい指示を伝えます。このディレクティブは自身で制御できる部分であるため、設定しておいてください。
proxy_read_timeout のデフォルト値は 60 秒です。これより長く静かな状態が続く subscriptions/listen ストリームは、サーバーではなく Nginx によって切断されます。その結果、ログ上ではプロセスが正常に見えても、クライアント側ではストリームが切断された状態になります。この設定はサーバー全体ではなく、MCP のロケーションに対してのみ引き上げてください。また、サーバーは静かな期間にキープアライブとして SSE コメント行(コロンで始まる行)を送信することが推奨されています。これにより、中間ノードによるストリームのタイムアウトを防ぐことができます。
Caddy の設定はより簡潔です。通信効率のためにデフォルトで部分的なバッファリングを行いますが、レスポンスに Content-Type: text/event-stream が含まれる場合は即座にフラッシュするため、追加のディレクティブなしでストリーミングが機能します。
mcp.example.com {
reverse_proxy 127.0.0.1:8080 {
health_uri /healthz
health_interval 10s
}
}ヘルスチェックの参照先には注意してください。MCP エンドポイントに対して GET を用いたアクティブチェックを行わないでください。このリビジョンのみを実装するサーバーは GET や DELETE に対して 405 Method Not Allowed を返すためであり、Caddy のデフォルトのヘルスチェックメソッドは GET であるためです。これを行うと、プロキシは正常なバックエンドをダウンしていると誤判定します。プロキシ用には /healthz のような単純なパスを用意し、プロトコルのチェックは別途 POST で行ってください。
curl -sS https://mcp.example.com/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: server/discover' \
-d '{"jsonrpc":"2.0","id":"health-1","method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'supportedVersions リストを含む 200 は、プロセスが起動しておりプロトコルが疎通していることを示します。JSON-RPC エラー -32601 を伴う 404 は、プロセスは起動しているものの、すべての 2026-07-28 サーバーで実装が必須である server/discover を提供していないことを意味します。-32022 を伴う 400 は、チェッカーが現在のビルドでサポートされていないバージョンを要求したことを示しており、これは依存関係のアップグレード後に検出すべき事象そのものです。オープンソース版の Nginx にはアクティブヘルスチェック機能がないため、アップストリームに対してパッシブな max_fails および fail_timeout を使用し、プロトコルチェックは監視システム側から実行してください。
ローリング再起動において失われるのは、実行中のリクエストのみです。ドレイン(排水)を行い、既存の POST リクエストの完了を待ってから新しいプロセスを起動すれば、失敗したリクエストはクライアント側で再送されます。唯一切断されるのは、現在開いている subscriptions/listen ストリームです。これは特定のプロセスに対するライブ接続であるためです。ステートレス化によってセッションアフィニティは排除されましたが、現在開いているストリームの接続アフィニティまでは排除されておらず、ルーティングルールでこれを解決することはできません。クライアントは、空の subscriptions/listen 結果で終了したストリームは正常終了、そうでないものは切断と判断できます。後者の場合、クライアントは再接続のトリガーとして扱う可能性があります。
これにより、初めてキャッシュが可能になります。リストメソッドの結果には ttlMs および cacheScope が含まれるようになり、cacheScope: "public" によって共有中間ノードがレスポンスをキャッシュ可能であることが示されます。リストの結果が接続ごとに変化しなくなったため、これは安全に行えます。これはセッションを排除したことによる直接的な結果です。
セッションがない場合に認証が変更される理由
セッションがある場合、initialize で一度認証を行い、その後のすべてをセッション ID で証明する手法が一般的でした。そのように使用されるセッション ID は、宛先(audience)や有効期限、失効経路を持たないベアラ資格情報であり、自前のサーバーが発行するものです。セッションを廃止するとこの近道は使えなくなり、代替手段はより厳格になります。
保護された MCP サーバーは、OAuth 2.1 のリソースサーバーとして動作します。クライアントからのすべての HTTP リクエストには Authorization: Bearer <access token> を含める必要があり、サーバーはリクエストごとにトークンを検証します。検証には宛先の確認が含まれます。サーバーは RFC 8707 (Resource Indicators for OAuth 2.0) に従い、トークンがそのサーバー専用に発行されたことを確認しなければならず、他の目的で発行されたトークンを受け入れたり転送したりしてはなりません。クライアントは、サーバーの正規 URI を指定した resource パラメータを送信することで、適切な宛先を要求します。
検出はチャレンジに基づいて実行されます。有効なトークンを含まないリクエストが到着すると、サーバーは 401 Unauthorized を返します。
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
scope="files:read"クライアントは resource_metadata を読み取り、そのドキュメント(MCP サーバーが実装しなければならない RFC 9728, OAuth 2.0 Protected Resource Metadata)を取得し、認可サーバーを見つけてフローを実行します。権限が不足している有効なトークンに対しては、error="insufficient_scope" とその操作に必要なスコープを伴う 403 Forbidden が返されます。
運用上の影響が2点あります。トークンの検証はセッションごとの1回ではなくすべてのリクエストで行われるため、呼び出しごとにイントロスペクションエンドポイントへのネットワークラウンドトリップが発生するとレイテンシに影響します。署名、宛先、有効期限に基づいてローカルで検証可能なトークを使用するか、トークンをキーとして検証結果を短時間キャッシュすることを推奨します。また、ID を保持するセッションが存在しないため、認可は呼び出しごとにトークンから計算する必要があります。これはセッションモデルよりも誠実な設計であり、AI エージェントからシークレットを排除するで解説されている、エージェントプロセス内に資格情報を保持しないという広範な慣習とも合致しています。スコープはリクエストが到達した後のトークンの権限を制限するに過ぎません。エージェントが実行されるマシン上では、ツール権限ルールと予算上限を追加するハーネスプラグインが、どの呼び出しを許可するかを決定します。
このリビジョンで正しいこと、正しくないこと
上記の内容はすべてリビジョン 2026-07-28 について記述したものです。これは MCP の永続的な仕様ではなく、昨年デプロイしたサーバーの仕様でもありません。
2025-11-25 以前のクライアントとサーバーは、依然としてハンドシェイクモデルを使用します。仕様書ではこれらのリビジョンをレガシーと呼び、リクエストごとのメタデータを使用するリビジョンをモダンと呼びます。このリビジョンのみをサポートするサーバーが古いクライアントと通信する場合、MCP エンドポイントで 405 Method Not Allowed に対して GET または DELETE を応答し、Mcp-Session-Id ヘッダーは生成やエコーを行わずに無視し、ストリームの再開ができないため Last-Event-ID も無視する必要があります。デュアルエラ(新旧両対応)サーバーは、1 つのエンドポイントで両方の方式を提供できます。モダンな _meta を含むリクエストはステートレスとして処理され、initialize を含むリクエストは古いセッションセマンティクスを選択します。
したがって、この内容を信頼する前にリビジョン文字列を確認してください。使用している SDK が依然として initialize を送信している場合、デプロイ環境ではセッションが有効であり、前述のセッションに関連する問題は依然として管理対象となります。これはクライアント側でも同様です。VPS でコーディングエージェントを実行する のような、自身のマシン上のエージェントプロセスは、使用しているライブラリがモダンなリビジョンに対応している場合にのみ、この意味でステートレスとなります。ランタイムがネゴシエーションしたバージョンを確認し、対応するリビジョンの仕様書を読み、このページはプロトコル全般ではなく特定の名前付きリビジョンについて記述しているものとして扱ってください。
FAQ
ステートレスな MCP サーバーでは何も保存できないのでしょうか?
いいえ。ステートレスとはプロトコルに関する定義であり、アプリケーションの動作を制限するものではありません。データベース、キュー、キャッシュはこれまで通り利用可能です。変更点は、複数の呼び出しにまたがる状態を管理する場合、クライアントがリクエストごとに渡す明示的な識別子(ツール引数に含まれるサーバー発行のハンドルなど)を使用する必要があるという点です。接続からコンテキストを推測することはできません。仕様では、サーバーは同一接続上の過去のリクエストに依存して機能、プロトコルバージョン、クライアントの識別を行ってはならないと定められています。これらはすべて、_meta に含まれる各リクエストで提供される必要があるためです。
ロードバランサーでスティッキーセッションはまだ必要ですか?
通常のリクエストには不要です。リビジョン 2026-07-28 以降、各 POST リクエストにはプロトコルバージョン、機能、認証情報が含まれるため、どのレプリカでもリクエストに応答でき、ラウンドロビンで問題ありません。唯一の長寿命な要素は subscriptions/listen レスポンスストリームであり、これは単一プロセスへの単一のオープン接続です。プロセスが終了するとストリームも終了し、クライアントは subscriptions/listen を再送して再確立します。これはセッションアフィニティではなく接続のライフタイムの問題であり、ルーティングルールで回避できるものではありません。
Mcp-Session-Id と HTTP GET ストリームはどうなりましたか?
両者とも SEP-2567 および SEP-2575 に基づき、リビジョン 2026-07-28 で削除されました。このリビジョンのみを実装するサーバーは、MCP エンドポイントへの 405 Method Not Allowed から GET および DELETE に対して応答し、Mcp-Session-Id ヘッダーはエコーバックせず無視する必要があります。サーバー主導の変更通知は、独立した GET ストリームではなく、subscriptions/listen リクエストのレスポンスストリーム経由で送信されます。古いクライアントへの対応が必要なサーバーは、以前のリビジョンの動作を本リビジョンと併存させて実装してください。
ハンドシェイクのない MCP サーバーのヘルスチェックはどうすればよいですか?
2段階で実施してください。プロキシのアクティブチェックには、アプリケーションが提供する通常の HTTP パスを指定してください。MCP エンドポイントへの GET は正しく 405 を返しますが、これでは正常なバックエンドをダウンと判定してしまうためです。次に、すべての 2026-07-28 サーバーが実装すべき server/discover を POST してプロトコル自体をチェックします。その応答が HTTP 200 であり、クライアントが使用するプロトコルバージョンが含まれていることを確認してください。JSON-RPC エラー -32601 を含む 404 が返された場合は、プロセスは稼働しているがそのメソッドを提供していないことを意味し、-32022 を含む 400 が返された場合は、要求したバージョンがそのビルドでサポートされていないことを意味します。