本文へスキップ
effidevFlutter・Cloudflareエッジ・クラウドコスト最適化
日本語

MCP 2026-07-28 仕様: セッションが消えた — ステートレス移行ガイド

MCP 2026-07-28 仕様 ステートレス移行ガイド

2026年7月28日、Model Context Protocol 仕様の新しいリビジョンがリリースされ、現行 (Current) の プロトコルバージョンになりました。直前のリビジョン 2025-11-25 から8か月ぶりです。MCP 共同創案者の David Soria Parra は公式発表で、今回のリリースを「remote MCP が登場してから1年あまりの中で、MCP に とって最も重要なもの」と表現しました。

一言でまとめるとこうです。MCP がステートレスになりました。 initialize ハンドシェイクが消え、 Mcp-Session-Id ヘッダーが消えました。いま MCP サーバーやクライアントを運用しているなら、これは 「いつか読めばいい」リリースノートではなく、コードを直す必要のあるブレイキングチェンジです。

影響はすでに表面化しています。8月6日、Cloudflare は Workers で MCP サーバーを作る際に使っていた McpAgentdeprecated・feature-frozen に切り替え、ステートレスハンドラーへ移行するよう案内 しました。セッションがなくなれば、セッションを保持するための Durable Object も不要になるからです。 この話は第12節で改めて扱います。

この記事は仕様変更の一覧をそのまま書き写すのではなく、実際にどのコードを直すべきかを中心に まとめます。各項目に SEP 番号を付けてあるので、根拠が必要なら該当の PR をそのまま開いてください。

1. セッションが消えた (SEP-2567)

最大の変更です。Streamable HTTP トランスポートから プロトコルレベルのセッションと Mcp-Session-Id ヘッダーが削除されました。

その帰結としてもう一つ条件が付きます。tools/listresources/listprompts/listもはやコネクションごとに異なる値を返せなくなりました。 以前は「このセッションは認証済みユーザー だから管理者向けツールまで見せる」といった実装が可能でしたが、これからは一覧のレスポンスがコネク ションに依存しません。

では、呼び出しをまたいで状態を保持する必要のあるサーバーはどうするのか。仕様が示す答えは明確です — サーバーが発行した明示的なハンドルを、通常のツール引数としてやり取りせよということです。

// Before — セッションヘッダーに依存した構造
app.post('/mcp', async (c) => {
  const sessionId = c.req.header('Mcp-Session-Id');
  const state = await sessions.get(sessionId);   // このヘッダーはもう届かない
  // ...
});

// After — 状態はサーバーが発行したハンドルとして、ツール引数に載せて明示的に受け渡す
server.tool('open_workspace', {}, async () => ({
  resultType: 'complete',
  content: [{ type: 'text', text: 'ワークスペースを開きました' }],
  structuredContent: { workspaceHandle: 'ws_01J8ZQ...' },   // サーバーが発行
}));

server.tool('write_file', {
  workspaceHandle: z.string(),   // クライアントが次の呼び出しにそのまま載せて送る
  path: z.string(),
  body: z.string(),
}, async ({ workspaceHandle, path, body }) => { /* ... */ });

暗黙的なコネクション状態が、明示的なデータに置き換わったわけです。ロードバランサーの背後にインス タンスを複数並べたり、サーバーレスで動かしたりするときに格段に扱いやすくなります。どのインスタンス がリクエストを受けても関係なくなるからです。

2. initialize ハンドシェイクが消えた (SEP-2575)

initializenotifications/initialized がまるごと削除されました。代わりに すべてのリクエストが、 自分自身に必要な情報を _meta に載せて持ち歩きます。

_meta キー 方向 必須 内容
io.modelcontextprotocol/protocolVersion クライアント → サーバー はい プロトコルバージョン ("2026-07-28")
io.modelcontextprotocol/clientCapabilities クライアント → サーバー はい このリクエストに関係するクライアントの能力
io.modelcontextprotocol/clientInfo クライアント → サーバー いいえ (SHOULD) クライアント名・バージョン
io.modelcontextprotocol/logLevel クライアント → サーバー いいえ このリクエストに対する最小ログレベル
io.modelcontextprotocol/serverInfo サーバー → クライアント (結果の _meta) いいえ (SHOULD) サーバー名・バージョン
// すべてのリクエストがこの形になる
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "write_file",
    "arguments": { "path": "a.txt", "body": "hi" },
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientCapabilities": { },
      "io.modelcontextprotocol/clientInfo": { "name": "my-client", "version": "1.0.0" }
    }
  }
}

必須フィールドが欠けたリクエストは malformed です。サーバーは必ず (MUST) JSON-RPC の -32602 (Invalid params) で拒否しなければならず、HTTP ならレスポンスステータスは 400 Bad Request でなければなりません。バージョンが合わない場合は UnsupportedProtocolVersionError (-32022) を返します。

能力の宣言にもルールが一つ加わりました。サーバーは、クライアントが宣言していない能力に依存しては なりません (MUST NOT)。 リクエストを処理するのに宣言されていない能力が必要なら MissingRequiredClientCapabilityError (-32021) を返さなければならず、data.requiredCapabilities に不足している能力を列挙しなければなりません。

仕様はステートレスの意味をかなり強く釘刺しています。サーバーは、同じコネクション上の以前の リクエストに依拠して文脈を組み立ててはなりません (MUST NOT)。 stdio プロセスですら会話やセッション ではないと明記されています — クライアントは同じトランスポートに互いに無関係なリクエストを混在させて 送ることができ、サーバーはコネクションやプロセスの同一性をセッション継続性の代用として扱ってはなり ません。

ハンドシェイクの往復が丸ごとなくなったので、コールドスタートが頻発する環境 — エッジランタイムや サーバーレス — では体感できる利点があります。逆に「初期化時に一度受け取って使い回していた」情報に 依存していたコードは、すべて手を入れる必要があります。

3. server/discover: これからは必須実装です (SEP-2575)

ハンドシェイクが消えた穴を埋める RPC です。サーバーは server/discover を必ず (MUST) 実装しなければ なりません。 サポートするプロトコルバージョン、能力、アイデンティティを知らせるためのものです。

クライアント側からは二通りの使い道があります。

DiscoverResult の形はこうです。フィールド名に注意してください — サポートバージョンの一覧は supportedVersions であり、サーバーのアイデンティティは結果のトップレベルではなく _meta の中に 入ります。

{
  "resultType": "complete",
  "supportedVersions": ["2026-07-28", "2025-11-25"],
  "capabilities": { "tools": {}, "resources": {} },
  "_meta": {
    "io.modelcontextprotocol/serverInfo": { "name": "my-mcp-server", "version": "2.0.0" }
  }
}

バージョンが合わないときにサーバーが返すエラーも決まっています。サポート一覧を一緒に載せることで、 クライアントが選び直せるようになります。

{
  "jsonrpc": "2.0", "id": 1,
  "error": {
    "code": -32022,
    "message": "Unsupported protocol version",
    "data": { "supported": ["2026-07-28", "2025-11-25"], "requested": "1900-01-01" }
  }
}

サーバーを作っているなら、今回の移行で 真っ先に追加すべきコード がこれです。なければ仕様違反です。

旧型・新型のクライアントを両方受けるには

仕様は実装を三つに分類して呼び分けます。

用語 意味
Modern バージョン・アイデンティティ・能力をリクエストごとに _meta で載せて送るバージョン (2026-07-28 以降)
Legacy initialize ハンドシェイクでセッションを確立するバージョン (2025-11-25 以前)
Dual-era 両方をサポートする実装

Dual-era サーバーは クライアントがどう話しかけてくるかで判断します。 リクエストがモダンな _meta を伴って来ればステートレスとして処理し、initialize が来ればレガシー方式で処理します。一つのエンド ポイントで両方を同時に提供しても構いません (MAY)。

クライアント側の検出はトランスポートごとに異なります。

判定結果は個々のリクエストではなく サーバーの属性 です。stdio ならプロセスの生存期間中、HTTP なら オリジン単位でキャッシュしておくのが適切です (SHOULD)。

注意すべき組み合わせがあります。レガシークライアント → モダンサーバーは失敗します。 レガシー クライアントには上位バージョンへ上がる手段がないからです。そのため、モダン専用サーバーなら initialize リクエストに対するエラーメッセージに サポートバージョンを書いておくのが望ましい (SHOULD) です — レガシークライアントの利用者が目にできる唯一の手がかりが、そのメッセージかもしれ ないからです。

4. 購読の方式が変わった (SEP-2575)

HTTP GET エンドポイントと resources/subscriberesources/unsubscribe が消え、 subscriptions/listen 一つに統合されました。単一の long-lived な POST レスポンスストリームです。

クライアントは受け取りたい通知の種類を 明示的にオプトイン します。

サーバーはそれを確認し、送る通知に io.modelcontextprotocol/subscriptionId タグを付けます。

ここで混同しやすい点が一つあります。notifications/progressnotifications/message のような リクエストに付随する通知は、subscriptions/listen ストリームには流れません。 これらの通知は、 元のリクエストのレスポンスストリームを引き続き流れます。進捗表示が急に出なくなったら、ここを疑うと いいでしょう。

5. MRTR — サーバーがクライアントに問い返す方式 (SEP-2322)

従来は、サーバーからクライアントへリクエストを投げる方向が別に用意されていました。roots/listsampling/createMessageelicitation/create などです。仕様の表現をそのまま移すと、サーバーは これからこの種のリクエストを 必ず MRTR パターンで送らなければならず (MUST)、従来のサーバー主導の リクエストパターンはもうサポートされません。ブレイキングチェンジです。

新しい流れはこうです。

  1. クライアントがリクエストを送る (id: 1)
  2. サーバーは情報がさらに必要なら、resultType: "input_required"InputRequiredResult を返す
  3. クライアントはユーザーに尋ねたうえで、元のリクエストを新しい id で送り直しながら 回答を載せる

ここで最も間違えやすい点があります。inputRequestsinputResponses は配列ではなくマップです。 キーはサーバーが付けた識別子で、値は ElicitRequestCreateMessageRequestListRootsRequest の いずれかである 完全なリクエストオブジェクト (method + params) です。

// ステップ2 — サーバーのレスポンス
{
  "jsonrpc": "2.0", "id": 1,
  "result": {
    "resultType": "input_required",
    "inputRequests": {
      "github_login": {
        "method": "elicitation/create",
        "params": {
          "mode": "form",
          "message": "GitHub のユーザー名を入力してください",
          "requestedSchema": {
            "type": "object",
            "properties": { "name": { "type": "string" } },
            "required": ["name"]
          }
        }
      }
    },
    "requestState": "AEAD-protected blob"
  }
}

レスポンスも同じキーを使うマップで、値は各リクエストの 結果オブジェクト です。

// ステップ3 — 新しい id で再試行し、回答と requestState をそのまま載せて送る
{
  "jsonrpc": "2.0", "id": 2,
  "method": "tools/call",
  "params": {
    "name": "delete_file",
    "arguments": { "path": "a.txt" },
    "inputResponses": {
      "github_login": { "action": "accept", "content": { "name": "octocat" } }
    },
    "requestState": "AEAD-protected blob"
  }
}

requestState — これが MRTR をステートレスにする仕掛けです

requestStateサーバーにとってのみ意味を持つ不透明な文字列 です。サーバーが必要な文脈をここに エンコードして送り返すと、クライアントがそのまま返してくれます。サーバーが状態ストアを持たなくて 済む理由が、まさにこれです。

ルールはかなり厳格です。

セキュリティ要件が特に重要です。この値は クライアントを経由して届くため、攻撃者が改ざんできる入力 です (MUST としてそう扱えと明記されています)。 認可・リソースアクセス・ビジネスロジックに影響を 与えるなら、サーバーは 完全性を必ず保護しなければならず (HMAC や AEAD など)、検証に失敗した状態は 拒否しなければなりません。 改ざんされてもリクエストが失敗する以外に何も起こらない場合に限り、 省略できます。

リプレイ防止のために、完全性保護されたペイロードの中に次を入れ、受信のたびに検証することが推奨され ます (SHOULD)。

ただしこれらの対策は リプレイの窓を狭めるだけで、単一使用を保証するものではありません。 一回限り の使用がどうしても必要なら、サーバー側で別途強制しなければなりません (MUST)。

MRTR を使えるリクエストは三つだけです

クライアントのリクエスト InputRequiredResult の可否
prompts/get
resources/read
tools/call

それ以外のリクエストに送ってはなりません (MUST NOT)。 また、クライアントが能力として宣言していない 種類のリクエストを inputRequests に入れてもなりません — クライアントが elicitation を宣言して いなければ、elicitation/create を入れることはできません。

双方向リクエストが、一方向の再試行に置き換わりました。トランスポートの観点ではずっと単純になりまし たが、クライアントは 再試行ループを自前で実装しなければなりません。 これまでサーバー主導の リクエストを処理していたコールバックは、すべてこの構造へ移す必要があります。

6. resultType がすべての結果に必須になった (SEP-2322)

すべての結果に resultType フィールドが 必須 で付きます。

後方互換に関するルールが明記されています。以前のプロトコルのサーバーがこのフィールドを欠いている 場合、クライアントは必ず (MUST) "complete" として扱わなければなりません。 クライアントを作るなら、 この分岐を必ず入れておきましょう。

7. なくなったもの

ping
logging/setLevel
notifications/roots/list_changed
tasks/result          (→ tasks/get のポーリングで代替)
tasks/list
notifications/elicitation/complete

ログレベルはこれから、リクエストごとに _metaio.modelcontextprotocol/logLevel で指定します。 そして サーバーは、このフィールドがないリクエストに対して notifications/message を送ってはなり ません (MUST NOT)。 無条件にログを流していたサーバーなら、条件を追加する必要があります。

SSE ストリームの再開もなくなりました。Last-Event-ID ヘッダーと SSE のイベント ID が Streamable HTTP から削除されたため、レスポンスストリームが切れると、進行中だったリクエストはそのまま失われます。 クライアントは新しいリクエスト ID で送り直さなければなりません (MUST)。再接続すれば続きから受け取れる ことを前提に書かれたコードがあるなら、ここで静かに壊れます。

8. Tasks がコアから拡張に外れた (SEP-2663)

実験的機能だった tasks がコアプロトコルから外れ、公式拡張 io.modelcontextprotocol/tasks へ移りま した。単なる引っ越しではなく、設計が組み直されています。

変更 内容
tasks/result の削除 ブロッキング方式でした。tasks/getポーリング で代替
tasks/update の新設 クライアント → サーバーの入力受け渡し用
tasks/list の削除
ハンドルの返却 サーバーが リクエストごとのオプトインなしに タスクハンドルを自発的に返せる

拡張は能力 (capabilities) の extensions マップで知らせます。識別子は _meta キーの命名規則に従い、 接頭辞が必須です。

{
  "capabilities": {
    "tools": {},
    "extensions": { "io.modelcontextprotocol/tasks": {} }
  }
}

片方だけが拡張をサポートする場合のルールも定められています。サポートしている側が コアの動作へ フォールバックするか、適切なエラーで拒否しなければなりません (MUST)。

9. Deprecated — Roots、Sampling、Logging (SEP-2577)

三つの機能が非推奨に指定されました。非推奨期間中は 完全に動作しますが、新規に作る実装では使うな、 という意味です。仕様が勧める代替手段はこうです。

非推奨の対象 代替
Roots ディレクトリ・ファイルをツールパラメータ、リソース URI、サーバー設定で渡す
Sampling LLM プロバイダーの API と直接連携
Logging stdio は stderr へ、それ以外は OpenTelemetry

併せて非推奨に再分類されたものもあります。

ここで重要なのは、今回同時に導入された 機能ライフサイクルポリシー です。Active / Deprecated / Removed の三状態を定義し、非推奨になった機能は削除対象になるまで 最低12か月 仕様に残ります。 ただし無条件ではありません — ポリシーの 迅速削除 (expedited-removal) 例外 が適用されると 最低90日 に短縮されます。活発なセキュリティリスクがある場合がそれにあたります。つまり上記の機能が すぐ次のリビジョンで消えるわけではありませんが、「1年は無条件に安全だ」と読んではいけません。

10. キャッシュとルーティング — 地味ですが実務に大きく効くもの

メジャーな変更一覧には載っていませんが、実装にはすぐ影響する項目です。

リスト結果がキャッシュ可能になりました (SEP-2549)。 tools/listprompts/listresources/listresources/readresources/templates/list の結果に ttlMscacheScope必須 で付きます。ttlMs は鮮度のヒント (ミリ秒) で、cacheScope"public" / "private" で 中間キャッシュが共有してよいかを決めます。ポーリングを減らすためのものです。

リクエストヘッダーが必須になりました (SEP-2243)。 Streamable HTTP の POST には Mcp-MethodMcp-Name を必ず載せなければなりません。本文をパースせずにルーティングできるようにするためです。 リバースプロキシや WAF の手前で MCP トラフィックを判別する必要があったなら、これからはヘッダーを 見るだけで済みます。

tools/list は決定的な順序で返すべきです (SHOULD)。 クライアントのキャッシュと LLM のプロンプト キャッシュのヒット率のためです。ツール一覧をマップから取り出してその都度違う順序で返していたなら、 ソートを入れましょう。プロンプトキャッシュが毎回壊れていたはずです。

OpenTelemetry のトレースコンテキストが規約化されました (SEP-414)。 _metatraceparenttracestatebaggage を載せて伝播します。

11. エラーコードが振り直された

エラーコードの割り当てポリシーができました。JSON-RPC のサーバーエラー範囲をこう分けます。

これに合わせて番号が変わったものがあります。

エラー 変更前 変更後
HeaderMismatch -32001 -32020
MissingRequiredClientCapability -32003 -32021
UnsupportedProtocolVersion -32004 -32022
リソースが存在しない -32002 -32602 (Invalid Params)

ここで誤解しやすい点があります。上の三つは今回のリビジョンのドラフト段階で初めて生まれたコードなので、 正式リリースされた以前のバージョンを実装したコードにはそもそも存在しません。 実際に既存コードへ 埋め込まれている可能性があるのは、最後の行の一つだけです。

リソースが存在しない場合が -32002 から -32602 (Invalid Params) に変わったのは、JSON-RPC 仕様に 合わせるための変更です。このバージョンを実装した側は -32002送出してはなりません (MUST NOT) が、クライアントは旧バージョンのサーバーが送る -32002引き続き受け入れるのが望ましい (SHOULD) です。

12. Cloudflare Workers を使っているなら — McpAgent が非推奨になった

今回の仕様変更が実装にどんな波紋を起こすかを最もよく示す事例が Cloudflare です。2026年8月6日、 Cloudflare は「The next generation of MCP」を発表し、McpAgent を取り除きました。

なぜこれが自然な帰結なのかは、前の第1・2節を読んでいれば察しがつくはずです。これまで Workers で MCP サーバーを動かすには、Durable Objects が事実上必須でした。Mcp-Session-Id で識別されるセッションと、 そのセッションに紐づく SSE ストリームを維持しなければならず、リクエストを同じインスタンスへ固定する 何かが必要だったからです。セッションが消えたことで、その前提そのものがなくなりました。

Cloudflare の表現はこうです。

This release of the MCP 2026-07-28 specification … removes the need for McpAgent. While Durable Objects remain the right primitive when an application itself needs state, MCP itself no longer requires a Durable Object to speak the protocol.

ドキュメント上の McpAgent の現状は 「deprecated and feature-frozen」 です。いますぐ使えなくなった わけではありませんが、新機能はもう追加されません。代替は createMcpHandler で、公式の説明は 「MCP SDK v2 のサーバーファクトリから、呼び出し可能な ステートレス な MCP リクエストハンドラーを 作る」というものです。

export default {
  fetch(request, env, ctx) {
    return createMcpHandler(createServer)(request, env, ctx);
  },
} satisfies ExportedHandler;

Durable Object クラスも、セッションルーティングもありません。リクエストごとにサーバーを新しく作って 処理し、それで終わりです。

ここで誤解しやすい点が一つあります。Durable Objects が不要になったわけではありません。 なくなった のは「MCP プロトコルを話すための」DO です。アプリ自体に状態が必要なら、DO は今でも正しい道具です。 公式ドキュメントの案内が、まさにその区別を突いています。

Store cross-request data behind an authenticated handle in a Durable Object, D1, KV, or R2 rather than an MCP session ID.

第1節で見た「サーバーが発行した明示的なハンドル」がまさにこの話です。状態を置く場所は従来どおり DO・D1・KV・R2 であり、その状態を指す鍵を MCP のセッション ID ではなく、認証されたハンドルに 置き換えよということです。

移行の負担も一度に集中しません。Cloudflare は /mcp エンドポイントが「新プロトコルと、2025年の Streamable HTTP クライアントによるステートレスなリクエストの両方を受け付ける」と述べています。旧型 クライアントを使う利用者が残っていても、サーバーを先に移せます。

コストについては注意が必要です。Cloudflare は「動く部品が減った分のコスト削減」という定性的な表現に とどめており、具体的な削減数値は示していません。 DO インスタンスが外れる分、料金が下がる方向で あることは確かですが、何パーセントという数字を期待して臨むべき話ではありません。

13. 移行チェックリスト

サーバーを運用中なら、順に確認しましょう。

クライアントなら、ここにさらに二つ加わります。

おわりに

Tier 1 の SDK 四つ — TypeScript、Python、Go、C# — はリリース時点ですでに 2026-07-28 を話します。 Rust SDK はベータでサポートしています。公式発表も「セッション識別子に依存していた開発者には、ある程度の 移行コストがある」と認めています。

とはいえ方向性そのものは明確です。暗黙的なコネクション状態を取り払い、すべてをリクエストの中に明示的に 収めていく方向です。エッジやサーバーレスのようにインスタンスが立ち上がっては消える環境で MCP サーバーを 動かしているなら、今回の変更は損ではなく得です。セッションストアを抱え込む理由がなくなったからです。

非推奨期間は最低12か月なので、今日すぐにすべてを作り直す必要はありません。ただし server/discover は 例外です。あれは非推奨ではなく 新規の必須要件 なので、新しい仕様に対応していると言うためには、 いま入れる必要があります。