ステートレスMCPサーバーで何が変わった?
MCP revision 2026-07-28でセッションとinitialize handshakeが削除されました。リバースプロキシ、ヘルスチェック、タイムアウト、認証への影響を解説します。
ステートレスな MCP サーバーとは
ステートレスな MCP サーバーは、リクエスト間でクライアントごとの状態を保持しません。各リクエストには、プロトコルのバージョン、クライアントの機能、サーバーが応答に必要とする認証情報が含まれます。そのため、どのマシン上のどのプロセスでも、任意のリクエストに応答できます。MCP(Model Context Protocol。エージェントがツールにアクセスするために使用する通信形式)は、revision 2026-07-28 でこれを規則としました。この変更により、initialize handshake と、その下で使用されていた HTTP セッションが削除されました。
これが運用上の要点です。クライアントごとの状態を保持しないサーバーは、セッション固定を必要とせず、通常のロードバランサーの背後に配置できます。デプロイ中に再起動してもクライアントとの接続を壊さずに済み、同一構成のプロセスを 1 つではなく 4 つ実行できます。セッション指向のサーバーでこれらを実現するには、追加の仕組みが必要です。
Model Context Protocol はステートレスなプロトコルです。リクエストの処理に必要な情報は、すべてリクエスト自体に含まれます。サーバーは各リクエストを独立して処理します。同じ接続またはストリーム上の以前のリクエストであっても、以前のリクエストから状態を推測してはいけません。
ステートレスであることは、サーバーが何も保存しないという意味ではありません。データベース、キュー、キャッシュは引き続き存在します。接続上でプロトコルの状態を保持しないという意味です。そのため、サーバーは接続、プロセス、オープンソケットを「会話の途中にあるこのクライアント」の代わりとして扱ってはいけません。
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ストリーム。これはsubscriptions/listenに置き換えられました。subscriptions/listenは、レスポンスが長時間存続するストリームになる通常の POST です。 - 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 で拒否する必要があります。この規則があるのは、ヘッダーでルーティングするロードバランサーと、ボディに基づいて処理するサーバーが、異なる情報源になるためです。これらのヘッダーでルーティングまたはレート制限を行う場合は、最初に MCP-Protocol-Version を確認してください。以前のリビジョンでは、ヘッダーとボディの値が検証されませんでした。そのため、そのバージョンではヘッダーの値を信頼できません。
バージョンの不一致は、ハンドシェイクの失敗ではなく、通常のリクエスト単位のエラーになりました。要求されたバージョンを実装していないサーバーは、400 Bad Request とエラー -32022、UnsupportedProtocolVersion を返し、対応しているバージョンを data.supported に列挙します。クライアントはその一覧から1つを選び、再試行します。
状態の保存先: トークン、カーソル、サブスクリプション
状態が消えたわけではありません。確認やログ出力が可能な場所へ移動しただけです。
認証情報はすべてのリクエストに含めます。 ID を紐付けるセッションがないため、アクセストークンを各 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 を受け付けるパスの 1 つです。通信の大半は短いリクエストと JSON レスポンスであり、どのプロキシでも処理できます。例外はストリーミングレスポンスです。この場合、プロキシのデフォルト設定が問題になります。これは、ノート PC 上のデモから 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 の location だけで延長し、サーバー全体には適用しないでください。サーバーは、無通信期間中のキープアライブとして SSE コメント行(コロンで始まる行)を送信することも推奨されています。これにより、中継プロキシがストリームをタイムアウトさせること自体を防げます。
Caddy では必要な設定が少なくて済みます。Caddy はデフォルトで通信効率のために部分的なバッファリングを行い、レスポンスに Content-Type: text/event-stream が含まれている場合は直ちにフラッシュします。そのため、追加のディレクティブなしでストリーミングが機能します。
mcp.example.com {
reverse_proxy 127.0.0.1:8080 {
health_uri /healthz
health_interval 10s
}
}このヘルスチェックが何を対象にしているかに注意してください。GET を指定して MCP エンドポイントにアクティブチェックを実行しないでください。このリビジョンだけを実装するサーバーは、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 は、プロセスは起動しているものの server/discover を提供していないことを示します。これはすべての 2026-07-28 サーバーが実装しなければならない機能です。-32022 を含む 400 は、チェッカーがこのビルドでサポートされていないバージョンを要求したことを示します。依存関係をアップグレードした後に確認したいのは、まさにこの状態です。オープンソース版 nginx にはアクティブヘルスチェックがないため、upstream でパッシブな max_fails と fail_timeout を使用し、プロトコルチェックは監視システムから実行してください。
ローリング再起動によって失われるのは、処理中のリクエストだけです。接続をドレインし、オープン中の POST を完了させてから新しいプロセスを起動します。失敗したリクエストはクライアントが再送します。ただし、オープン中の subscriptions/listen ストリームは切断されます。このストリームは、特定の 1 つのプロセスへのライブ接続だからです。ステートレス化によりセッションアフィニティは不要になりました。しかし、現在オープンしているストリームに対する接続アフィニティまでなくなったわけではありません。どのルーティングルールでも、この問題は解決できません。クライアントは、ストリームが正常に終了したかどうかを区別できます。空の subscriptions/listen 結果で終了したストリームは正常に閉じられています。これがないまま終了したストリームは切断されたものであり、クライアントは再接続の理由として扱えます。
初めてキャッシュが可能になります。list メソッドの結果には ttlMs と cacheScope が含まれるようになり、cacheScope: "public" により共有中継プロキシがレスポンスをキャッシュできることが示されます。これは、list の結果が接続ごとに変わらなくなったため安全です。セッションを削除したことが、その直接的な理由です。
セッションがない場合に認証が変わる理由
セッションがあると、initializeで 1 回だけ認証し、その後はセッション ID をすべての処理の証明として扱いたくなります。このように使うセッション ID は、対象サービスの指定、期限、失効手段を持たないベアラー認証情報であり、自分のサーバーが発行したものです。セッションを削除すると、この近道は使えなくなり、代わりにより厳格な方式が必要になります。
保護された MCP サーバーは、OAuth 2.1 のリソースサーバーとして動作します。クライアントからのすべての HTTP リクエストには Authorization: Bearer <access token> が必要で、サーバーは各リクエストでトークンを検証します。検証には対象者(audience)も含まれます。サーバーは RFC 8707(Resource Indicators for OAuth 2.0)に従い、そのトークンが自分自身向けに発行されたことを確認し、別の対象向けのトークンを受け入れたり転送したりしてはいけません。クライアントは、サーバーの正規 URI を resource パラメーターに指定して、正しい対象者を要求します。
Discovery はチャレンジを起点に実行されます。利用可能なトークンを付けずにリクエストが届くと、サーバーは 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 を読み取り、そのドキュメントを取得します。このドキュメントは RFC 9728(OAuth 2.0 Protected Resource Metadata)で定義されており、MCP サーバーは実装する必要があります。クライアントは認可サーバーを見つけてフローを実行します。権限が不足した有効なトークンには、403 Forbidden と error="insufficient_scope" が返され、その操作に必要なスコープが示されます。
運用上、2 つの点に注意が必要です。トークン検証はセッションごとに 1 回ではなく、リクエストごとに実行されます。そのため、呼び出しごとに introspection エンドポイントへのネットワーク往復が発生すると、遅延として現れます。署名、対象者、有効期限を使ってローカルで検証できるトークンを優先するか、トークンをキーとして検証結果を短時間キャッシュしてください。また、ID を保持するセッションがないため、認可は呼び出しごとにトークンから計算する必要があります。これはセッション方式よりも実態に即しており、認証情報を agent プロセスの外部に保持するという、より広い実践とも一致します。詳しくは AI agent の外部で Secret を保持する で説明しています。
このリビジョンで当てはまることと、当てはまらないこと
ここまでの説明はリビジョン 2026-07-28 についてのものです。MCP の今後すべてを説明するものではなく、昨年デプロイしたサーバーについて説明するものでもありません。
2025-11-25 以前のクライアントとサーバーは、引き続きハンドシェイクモデルを使用します。仕様では、これらのリビジョンをレガシーと呼び、リクエストごとのメタデータを使用するリビジョンをモダンと呼びます。このリビジョンのみをサポートするサーバーが古いクライアントと通信する場合、MCP エンドポイントで GET または DELETE に対して 405 Method Not Allowed を返し、Mcp-Session-Id ヘッダーは発行もエコーもせずに無視し、ストリームは再開できないため Last-Event-ID も無視します。両方の世代に対応するサーバーは、1 つのエンドポイントで両方を提供できます。モダンな _meta を含むリクエストはステートレスに処理し、initialize リクエストでは旧来のセッションセマンティクスを選択します。
そのため、これらを信頼する前にリビジョン文字列を確認してください。SDK がまだ initialize を送信している場合、デプロイ環境ではセッションが依然として実体を持ち、上記のセッション関連の問題にも引き続き対処する必要があります。クライアント側でも同じです。VPS 上でコーディングエージェントを実行する の構成のように、自分のサーバー上で動作するエージェントプロセスは、使用するライブラリがモダンなリビジョンに対応している場合にのみ、この意味でステートレスになります。ランタイムがネゴシエートするバージョンを確認し、対応する仕様のリビジョンを読んでください。このページはプロトコル全般ではなく、名前付きの1つのリビジョンについて説明しているものとして扱ってください。
FAQ
ステートレスな MCP サーバーでは、何も保存できないという意味ですか?
いいえ。ステートレスとはプロトコルの性質であり、アプリケーションの性質ではありません。データベース、キュー、キャッシュは、これまでどおり利用できます。変わるのは、複数の呼び出しにまたがる状態を、クライアントが各リクエストで渡す明示的な識別子で参照する必要がある点です。たとえば、ツール引数にサーバーが発行したハンドルを含めます。接続からコンテキストを推測することはできません。仕様では、サーバーは同じ接続上の過去のリクエストに依存して、機能、プロトコルバージョン、クライアント ID を確立してはならないと定めています。これらはすべてのリクエストで _meta に指定されるためです。
ロードバランサーでスティッキーセッションを引き続き使用する必要はありますか?
通常のリクエストでは必要ありません。リビジョン 2026-07-28 では、各 POST にプロトコルバージョン、機能、認証情報が含まれるため、どのレプリカでも任意のリクエストに応答でき、ラウンドロビンで問題ありません。残る長時間維持されるものは subscriptions/listen のレスポンスストリームです。これは 1 つのプロセスへの単一のオープン接続です。そのプロセスが終了するとストリームも終了し、クライアントは subscriptions/listen を再送して再確立します。これはセッションアフィニティではなく接続の存続期間の問題であり、これを妨げるルーティング規則はありません。
Mcp-Session-Id と HTTP GET ストリームはどうなりましたか?
どちらも SEP-2567 と SEP-2575 に基づくリビジョン 2026-07-28 で削除されました。このリビジョンだけを実装するサーバーは、MCP エンドポイントへの GET と DELETE に対して 405 Method Not Allowed を返し、Mcp-Session-Id ヘッダーはエコーせず無視する必要があります。サーバーが開始する変更通知は、独立した GET ストリームではなく、subscriptions/listen リクエストのレスポンスストリームで送信されるようになりました。古いクライアントにも対応する必要があるサーバーは、以前のリビジョンの動作をこのリビジョンと併せて実装します。
ハンドシェイクなしの MCP サーバーをどのようにヘルスチェックすればよいですか?
2 段階で確認します。プロキシのアクティブチェックは、アプリケーションが提供する通常の HTTP パスに向けてください。MCP エンドポイントへの GET は正しく 405 を返すため、正常なバックエンドを停止と判定する可能性があるためです。次に、server/discover を POST してプロトコル自体を確認します。これはすべての 2026-07-28 サーバーが実装する必要があります。応答が HTTP 200 であり、クライアントが使用するプロトコルバージョンを列挙していることを検証します。JSON-RPC エラー -32601 を伴う 404 は、プロセスは稼働しているものの、そのメソッドを提供していないことを示します。-32022 を伴う 400 は、要求したバージョンがそのビルドでサポートされていないことを示します。