MCPサーバーを作った時期によって、正しい実装例が変わっています。initializeを前提にするクライアント、Mcp-Session-Idを保持するサーバー、roots/list を投げていたクライアント、GETのSSE streamへ通知を流す構成は、2026-07-28仕様ではそのまま使えません。SDKだけ更新しても、protocol versionが旧世代のままなら移行も完了しません。
この記事は、MCP 2026-07-28へ移るための実装順を整理します。特に mcp roots / mcp root で調べている人が見たい roots/list 廃止後の受け皿もここで扱います。2025-eraのsessionを維持する判断は MCP Session 管理ガイド、HTTP+SSEからStreamable HTTPへの旧世代移行は MCP Streamable HTTP 移行ガイド が担当します。長時間処理は MCP Tasks extension 設計ガイド、変更通知は MCP subscriptions/listen 設計ガイド に分けます。途中の確認や追加入力はMCP MRTRの役割で、tool resultのcontent / structuredContent / outputSchemaの分離は MCP structuredContent 設計ガイド で扱います。
先に結論: SDK更新ではなく通信仕様を移す
移行の完了条件は、パッケージ版ではなく通信内容で決めます。
| 観点 | 2025-era | 2026-07-28 |
|---|---|---|
| 接続開始 | initialize / initialized | server/discover、または直接request |
| capability | connection単位 | requestごとの_meta |
| HTTP session | Mcp-Session-Idを利用可能 | protocol-level sessionなし |
| serverからの追加入力 | server-initiated request | MRTRのinput_required |
| change通知 | GET stream / resource subscription | subscriptions/listen |
| routing | JSON bodyの解析が必要 | Mcp-Method / Mcp-Name |
| list cache | client独自判断 | ttlMs / cacheScope |
私なら、移行を次の3段階に分けます。
- クライアントとサーバーが実際に話しているprotocol revisionを記録する
- statelessなリクエストへ変えても壊れない状態管理へ直す
- ゲートウェイ、cache、authorization、notificationを新しいwire contractへ合わせる
なぜ今移行するのか
2026-07-28は、小さなfield追加ではありません。protocol coreがconnection中心からrequest中心へ変わりました。
この変更で、remote MCPサーバーをsticky sessionや共有session storeなしで水平分散しやすくなります。ゲートウェイもJSON bodyを開かず、headerでtoolやmethodを識別できます。一方、connection内の暗黙状態に依存していたサーバーは、その状態を明示的なhandleかMRTRのrequestStateへ移す必要があります。
重要なのは、protocolがstatelessになっても、業務処理までstatelessにする必要はないことです。状態はtransportの裏へ隠さず、modelとクライアントが扱える明示的な識別子へ移します。
移行前に棚卸しする7項目
最初にcodeとtraffic logを調べます。
mcp_migration_inventory:
protocol_versions:
client_offers: []
server_accepts: []
initialize_handshake: unknown
session_header: unknown
state_store_key: unknown
get_stream: unknown
server_initiated_requests: []
sdk_and_host_support: []
見る項目は次の7つです。
initialize/notifications/initializedを送受信しているかMcp-Session-Idを保存、転送、削除しているか- session IDをtenantや作業状態のkeyにしているか
- GET endpointでlist changeやresource更新を待っているか
elicitation/createなどをサーバーから直接送っているか- reverse proxyがJSON bodyからmethodやtool名を抜いているか
- クライアント、host、SDKが2026-07-28を実際にnegotiationできるか
package.jsonだけでは6と7を判定できません。integration testでrequest headerとJSON-RPC envelopeを保存し、旧eraへfallbackしていないか確認します。
手順1: protocol negotiationを明示する
2026-07-28サーバーはserver/discoverを実装し、対応versionとcapabilitiesを返します。クライアントは事前にdiscoverしても、必要なmetadataを付けて直接リクエストしても構いません。
TypeScript SDK v2のdefaultは2025-eraです。packageをv2へ上げただけではwireは変わりません。クライアント側でmodern protocolを試し、旧サーバーへfallbackするmodeを明示します。
const client = new Client(
{ name: "internal-agent-host", version: "4.2.0" },
{ versionNegotiation: { mode: "auto" } },
);
await client.connect(transport);
本番移行では、まずautoで互換性を確認し、全サーバーの対応が終わってから2026-07-28へpinします。最初からpinすると、未移行サーバーを一括で切り落とします。逆にfallbackを永久に残すと、新仕様のつもりで旧handshakeを使い続けます。
monitoringにはnegotiated revisionを必ず残します。
{
"event": "mcp_protocol_negotiated",
"client": "internal-agent-host",
"server": "github-readonly",
"revision": "2026-07-28",
"fallback": false
}
手順2: リクエストを自己完結させる
2026-07-28では、各リクエストにprotocol versionとclient capabilitiesが必須です。client identityは推奨項目として各リクエストへ載せます。HTTPではversion headerも必要です。
POST /mcp HTTP/1.1
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: search_issues
Content-Type: application/json
{
"jsonrpc": "2.0",
"id": "req-1042",
"method": "tools/call",
"params": {
"name": "search_issues",
"arguments": { "query": "is:open label:security" },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {
"name": "internal-agent-host",
"version": "4.2.0"
},
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
ゲートウェイではheaderとbodyの整合も検証します。Mcp-Method: tools/callなのにbodyがresources/readなら、routingやauthorizationを通す前に拒否します。
手順3: session状態を明示的なhandleへ移す
旧実装で次のようなkeyを使っていたら、そのまま移行できません。
const state = await sessionStore.get(request.headers.get("Mcp-Session-Id"));
長く残る業務状態は、tool resultとしてhandleを返し、次のtool argumentで受け取ります。
{
"content": [{ "type": "text", "text": "Import was accepted." }],
"structuredContent": {
"operationHandle": "import_01JX8D4Q7V8A"
}
}
次回リクエストではhandleを明示します。
{
"name": "get_import_status",
"arguments": {
"operationHandle": "import_01JX8D4Q7V8A"
}
}
handleはopaque、推測困難、tenant-bound、期限付きにします。単にsession IDをfield名だけ変えて公開すると、cross-tenant参照やreplayの問題を持ち込みます。
同じ論理リクエストの途中だけで必要な状態は、MRTRのrequestStateへ置きます。数分以上続き、切断後も再開するjobはTasksへ分けます。
手順4: ゲートウェイpolicyをheaderへ寄せる
Mcp-MethodとMcp-Nameがあるため、ゲートウェイはbodyをparseせずにrouting、rate limit、audit labelを付けられます。
routes:
- match:
mcp_method: tools/call
mcp_name: search_issues
policy:
auth_scope: issues:read
rate_limit: 120/min
- match:
mcp_method: tools/call
mcp_name: delete_project
policy:
action: deny
ただしheaderだけを信用せず、MCP endpointでbodyとの一致を再検証します。ゲートウェイの判定とサーバー実行が別toolを指す状態を作ってはいけません。
手順5: list cacheを明示する
tools/list、prompts/list、resources/listなどのレスポンスは、ttlMsとcacheScopeを返します。tool一覧の順序も安定させます。
{
"resultType": "complete",
"tools": [],
"ttlMs": 300000,
"cacheScope": "private"
}
認可主体で内容が変わる一覧をpublicにすると、別userのtool catalogを共有cacheから返す危険があります。判断できない場合はprivate、短いTTLから始めます。
手順6: notificationをsubscriptions/listenへ移す
旧GET streamやresources/subscribeへ依存している場合、2026-07-28ではsubscriptions/listenへ移します。クライアントが必要な通知typeだけを選び、サーバーは受け入れたfilterを応答します。
リクエストに紐づくprogressやmessageは、そのリクエストのresponse streamへ残ります。全通知を1本のsubscriptionへ集める設計ではありません。
運用では次を監視します。
- クライアントが要求したfilterとサーバーが受け入れたfilter
- stream切断後の再接続回数
- 未対応filterを空で受け入れたように見せていないか
- extension通知をtarget SDKが処理できるか
手順7: authorizationとdeprecated featureを同時に棚卸しする
2026-07-28ではauthorizationも変わります。issuer validation、issuerごとのcredential分離、scope step-up、CIMDへの移行計画を確認します。DCRは後方互換用とし、新規標準には置きません。
Roots、Sampling、Logging、旧HTTP+SSEはdeprecation対象です。最低12か月の猶予があっても、新規実装へ追加する理由にはなりません。
| deprecated feature | 新規実装の代替 |
|---|---|
| Roots | tool argument、resource URI、server config |
| Sampling | model provider APIへの直接接続 |
| Logging | stdioのstderr、OpenTelemetry |
| HTTP+SSE | Streamable HTTP |
移行test matrix
本番切替前に、クライアントとサーバーの組み合わせを固定して試します。
| client | server | 期待結果 |
|---|---|---|
| 2026 client | 2026 server | server/discoverまたはmodern requestで接続 |
| 2026 client | 2025 server | 許可した場合だけlegacyへfallback |
| 2025 client | dual-era server | initializeで互換接続 |
| 2025 client | 2026-only server | 明確なversion error |
各caseで次をassertします。
- negotiated revisionがlogに残る
Mcp-Method/Mcp-Nameとbodyが一致する- user別のlist resultがcacheで混ざらない
- session headerなしで同じリクエストを別instanceが処理できる
- 状態handleのtenant、TTL、権限を検証する
- old notification pathへtrafficが戻らない
- OAuth issuerをまたいだcredential reuseを拒否する
よくある失敗
SDK v2へ上げただけで移行完了とする
TypeScript SDK v2は明示的にopt-inしない限り2025-eraを使います。wire logでrevisionとhandshakeを確認します。
session状態をglobal memoryへ移す
session headerを消してprocess memoryへ移しても、別instanceで処理できません。明示的なhandleか外部job storeへ移します。
状態handleを認可に使い回す
handleはobjectの識別子であり、authorization tokenではありません。主体とscopeはリクエストごとに検証します。
TasksとMRTRを同時に導入する
最初に短いtools/callをstateless化し、その後にMRTR、最後にTasksを追加します。extension対応が未確認の段階で長時間jobまで移すと、原因を切り分けにくくなります。
まとめ
MCP 2026-07-28への移行は、Mcp-Session-Idを削る作業ではありません。connectionに隠れていたversion、capability、状態、notification、routingをリクエスト単位へ出す作業です。
私なら、まず1つのread-onlyサーバーでserver/discover、リクエスト単位のmetadata、header validation、private cacheを通します。次に明示的なhandle、MRTR、Tasksの順で機能を戻します。この順なら、互換性と安全性のどちらで壊れたかを追えます。