本文へ移動
🔄

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-era2026-07-28
接続開始initialize / initializedserver/discover、または直接request
capabilityconnection単位requestごとの_meta
HTTP sessionMcp-Session-Idを利用可能protocol-level sessionなし
serverからの追加入力server-initiated requestMRTRのinput_required
change通知GET stream / resource subscriptionsubscriptions/listen
routingJSON bodyの解析が必要Mcp-Method / Mcp-Name
list cacheclient独自判断ttlMs / cacheScope

私なら、移行を次の3段階に分けます。

  1. クライアントとサーバーが実際に話しているprotocol revisionを記録する
  2. statelessなリクエストへ変えても壊れない状態管理へ直す
  3. ゲートウェイ、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つです。

  1. initialize / notifications/initializedを送受信しているか
  2. Mcp-Session-Idを保存、転送、削除しているか
  3. session IDをtenantや作業状態のkeyにしているか
  4. GET endpointでlist changeやresource更新を待っているか
  5. elicitation/createなどをサーバーから直接送っているか
  6. reverse proxyがJSON bodyからmethodやtool名を抜いているか
  7. クライアント、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新規実装の代替
Rootstool argument、resource URI、server config
Samplingmodel provider APIへの直接接続
Loggingstdioのstderr、OpenTelemetry
HTTP+SSEStreamable HTTP

移行test matrix

本番切替前に、クライアントとサーバーの組み合わせを固定して試します。

clientserver期待結果
2026 client2026 serverserver/discoverまたはmodern requestで接続
2026 client2025 server許可した場合だけlegacyへfallback
2025 clientdual-era serverinitializeで互換接続
2025 client2026-only server明確なversion error

各caseで次をassertします。

  1. negotiated revisionがlogに残る
  2. Mcp-Method / Mcp-Nameとbodyが一致する
  3. user別のlist resultがcacheで混ざらない
  4. session headerなしで同じリクエストを別instanceが処理できる
  5. 状態handleのtenant、TTL、権限を検証する
  6. old notification pathへtrafficが戻らない
  7. 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の順で機能を戻します。この順なら、互換性と安全性のどちらで壊れたかを追えます。

参考

WRITTEN BY nidoneko

Full-stack engineer with 8+ years of experience in TypeScript, React, Node.js, and cloud-native development across healthcare, finance, HR, and IoT domains.

View Profile →