MCPのバージョニングと後方互換性はどう決まるか
MCP 2026-07-28版はハンドシェイクを廃止し、全リクエストにバージョンを載せる方式に変わりました。Modern/Legacy/Dual-eraの3分類と、Claude Codeが実際にどちらで動いているかを解説します。
MCPの2026-07-28版は、バージョンの決め方そのものを作り直しました。それまでの版はinitializeという最初のリクエストでクライアントとサーバーがハンドシェイクを行い、そこで確定した版をセッション全体で使い続ける方式でした。現行版にハンドシェイクはありません。すべてのリクエストが、自分がどの版のプロトコルで書かれているかを毎回申告します。サーバーはリクエストごとに、その版をサポートするかを独立に判断します。この変更が生む新旧混在の状態を、仕様は3つの用語と組み合わせごとの動作表で整理しています。
なぜハンドシェイクが無くなったのか
MCPは元々ステートレスなプロトコルを志向しています。サーバーは接続やセッションをまたいだ状態を仮定せず、リクエストごとに独立して処理すべきだという原則です。initializeハンドシェイクは、この原則と相性が悪い仕組みでした。一度確立した版をセッションの生存期間だけ覚えておく必要があり、その分だけ状態を持つからです。
現行版は版の情報をリクエストの_metaフィールドに載せ、HTTPではMCP-Protocol-Versionヘッダーにも重複させます。サーバーが対応していない版を要求されたらUnsupportedProtocolVersionError(コード-32022)を返し、対応する版の一覧をdata.supportedに含めます。クライアントはその一覧から使える版を選び直してリトライします。エラーコードの割り当て全体はMCPのエラーコードは-32000番台をどう割り当てるかで扱っています。
サーバーはserver/discoverという専用メソッドの実装が必須です。クライアントは他のリクエストを送る前にこれを呼んで対応版を先に知ることができますが、必須ではありません。対応版が分かっている前提でいきなり本題のリクエストを送り、UnsupportedProtocolVersionErrorが返ってきたときだけ対処する実装も仕様上は正当です。
Modern・Legacy・Dual-eraという3分類
仕様は版の新旧を扱うために3つの用語を定義しています。
| 用語 | 定義 |
|---|---|
| Modern | 定義版・識別情報・機能をリクエストごとのメタデータとして運ぶ(2026-07-28以降) |
| Legacy | 定義initializeハンドシェイクでセッションを確立する(2025-11-25以前) |
| Dual-era | 定義ModernとLegacyの両方に対応する実装 |
Dual-eraのサーバーは、クライアントの接続方法から自分がどちらの流儀で応じるかを決めます。Modern形式の_metaを伴うリクエストが来ればステートレスに現行版で処理し、initializeリクエストが来ればLegacyの意味論に切り替えます。同じエンドポイントやプロセスで両方の流儀を同時に扱うことも認められています。
era判定はどうやって行うか
Dual-eraのクライアントが「相手のサーバーがどちらのeraか」を見分ける方法は、トランスポートごとに異なります。stdioでは、他のリクエストより先にserver/discoverを送って応答を見ます。
stdioでのera判定(server/discoverのprobe)
- 1
DiscoverResultが返る
相手はModernサーバーです。返ってきた
supportedVersionsから共通の版を選び、そのまま進めます。 - 2
認識できるModernのエラーが返る
UnsupportedProtocolVersionErrorなどがこれに当たります。相手はModernですが、要求した版に対応していません。supportedの一覧にある版を選び直し、initializeへのフォールバックはしません。 - 3
それ以外のエラー、またはタイムアウト
相手はLegacyサーバーです。
initializeハンドシェイクへフォールバックします。フォールバックの条件を特定のエラーコードに結びつけてはならず、Legacyサーバーは未知のメソッドに-32601や-32602などの実装依存のエラーを返すか、何も返さないことがあります。
Streamable HTTPでは、まずModern形式のリクエストを試します。400 Bad Requestが返ったらボディを調べてから判断します。Modernのサーバーは、版の不一致だけでなくMissingRequiredClientCapabilityErrorやヘッダー検証の失敗にも400を使うためです。ボディに認識できるModernのJSON-RPCエラーがあればModernサーバーと確定し、版やリクエストを直して再送します。ボディが空か、そうでないエラーなら、Legacyと見なしてinitializeへ進みます。
この判定はサーバープロセスまたはHTTPのオリジンごとの性質であり、個々のリクエストの性質ではありません。クライアントはサーバープロセスの生存期間(stdio)またはオリジン(HTTP)の間、結果をキャッシュするのが望ましいとされています。同じサーバー構成なら再起動をまたいで保持してもよく、前提が外れたら再判定します。
Modern専用のクライアントは、probeを省いても動きます。それでも仕様はprobeを勧めています。initializeより前のリクエストを検証しないLegacyサーバーが、tools/callのようなera判別のつかないメソッドをLegacyの意味論で処理してしまうことがあるためです。probeを挟めば、その場合も決定的な失敗に変わります。
クライアント×サーバーの組み合わせでどう動くか
era判定を踏まえると、クライアントとサーバーの組み合わせは次の7パターンに分類できます。
| クライアント | サーバー | 結果 |
|---|---|---|
| Modern | サーバーModern | 結果成立。版が食い違えばUnsupportedProtocolVersionErrorでリトライ |
| Modern | サーバーLegacy | 結果失敗。stdioではserver/discoverを先に送り決定的に失敗させるべき |
| Dual-era | サーバーModern | 結果成立。probeがModernの結果を返し、クライアントはModernのまま進む |
| Dual-era | サーバーLegacy | 結果成立。probeが失敗しLegacyへフォールバック |
| Legacy | サーバーModern | 結果失敗。Legacyクライアントにフォールバック手段が無い |
| Legacy | サーバーDual-era | 結果成立。initializeに応じてLegacy版で処理される |
| Legacy | サーバーLegacy | 結果成立。従来のLegacy版どおりに動作する |
Modern→Legacyの失敗は、見え方が一定ではありません。Legacyサーバーが実装依存のエラーで拒否することもあれば、無言のままのこともあり、era判別のつかないメソッドをLegacyの意味論で処理してしまうこともあります。stdioでクライアントがserver/discoverを先に送るべきだとされるのは、この3通りを1つの決定的な失敗にそろえるためです。
Legacyクライアントが根本的に詰むのは、相手がModern専用サーバーの場合です。stdioではinitializeが未知のメソッドで、必須の_metaも欠けているため、JSON-RPCエラーで拒否されます。エラーコードは実装依存です。HTTPでは必須ヘッダーが欠けるため400 Bad Requestになり、古いHTTP+SSEトランスポートのクライアントは最初のGETの時点で失敗します。Legacyクライアントにはこのエラーから回復してModernへ乗り換える手段がありません。
この非対称性を踏まえ、仕様はModern専用サーバーに対して、initializeへのエラー応答に自分が対応する版を含めるよう求めています。トランスポートは問いません。Legacyクライアントが利用者に見せられる診断が、そのメッセージだけになりうるからです。
Modern専用のHTTPサーバーには、2025-11-25以前のStreamable HTTPの流儀で来るトラフィックへの応じ方も定められています。MCPエンドポイントへのGETやDELETEには405 Method Not Allowedを返します。Mcp-Session-Idヘッダーは無視し、セッションIDを発行も返送もしません。Last-Event-IDも無視します。ストリームの再開はこの版に無いためです。
拡張機能もこの仕組みでネゴシエーションする
コアプロトコルを超える追加機能は拡張(extensions)という別枠で扱われ、capabilitiesのextensionsフィールドに、識別子と設定オブジェクトのマップとして広告します。各拡張機能がどこまで対応しているかはMCP拡張機能のサポートマトリクスの読み方で確認できます。たとえばMCP Appsという拡張をio.modelcontextprotocol/uiという識別子で宣言する場合は次の形になります。
{
"capabilities": {
"roots": {},
"extensions": {
"io.modelcontextprotocol/ui": {
"mimeTypes": ["text/html;profile=mcp-app"]
}
}
}
}一方が拡張に対応し他方が対応していない場合、対応している側はコアプロトコルの挙動へ戻すか、適切なエラーで拒否しなければなりません。この扱いは拡張ごとにドキュメント化することが推奨されています。
Claude CodeはどちらのMCPクライアントランタイムを使っているか
ここまではMCP仕様一般の話です。では実際にClaude Codeは、Modern(2026-07-28)とLegacyのどちらでMCPサーバーに接続しているのか。Claude Codeは2つのクライアントランタイムを使い分けています。v1ランタイムはMCP TypeScript SDK 1.x上に構築され、v2ランタイムは同じコードをSDK 2.0上で動かしたもので、2026-07-28に対応します。ランタイムは起動のたびに決まり、終了まで変わりません。
v2が既定になる条件は、セッションがフィーチャーフラグを取得するかどうかで分かれます。
- フラグを取得するセッション: Claude Code v2.1.232以降でv2が既定です
- フラグを取得しないセッション: v2.1.274以降でv2が既定です。対象は、Amazon Bedrock・Claude Platform on AWS・Google Cloud's Agent Platform・Microsoft Foundryで実行するセッション(ホスト側が
CLAUDE_CODE_PROVIDER_MANAGED_BY_HOSTを設定していない場合)、Claude apps gateway経由でサインインしたセッション、DISABLE_TELEMETRYなどでテレメトリーやフラグ取得を切ったセッションです
つまり、フラグを取得しないセッションがv1に留まるのはv2.1.274より前のバージョンだけです。v2を使っていても、Claude Codeのバージョンによっては、Modern版を試さないサーバーが残ります。v2ランタイムが2026-07-28を尋ねる相手は、接続の種類と、Claude Codeのバージョンで決まります。
v2ランタイムがModern版を尋ねる相手
尋ねる
HTTPサーバーには常に尋ねます。stdioサーバーには、v2.1.292以降の全インストールで尋ねます。claude.aiコネクタのサーバーには、フラグ取得セッションで尋ね、v2.1.295以降はフラグを取得しないインストールでも尋ねます。
尋ねない
次の2つは、MCP_PROTOCOL_NEGOTIATION=autoを設定しない限りLegacyで接続します。v2.1.292より前のstdioと、v2.1.295より前でフラグを取得しないセッションのコネクタです。それ以外のトランスポートのサーバーもv1と同じ接続です。
Anthropicはフィーチャーフラグで、特定のサーバーだけをLegacyに留めたり、通知ストリームの対象から外したりもできます。ランタイムとprobeの挙動は、2つの環境変数で手元から上書きできます。
| 環境変数 | 役割 |
|---|---|
MCP_SDK_GENERATION | 役割使うランタイムをv1またはv2に固定する。v2.1.218以降 |
MCP_PROTOCOL_NEGOTIATION | 役割v2ランタイムだけで有効。autoでHTTP・コネクタ・stdioの全サーバーに尋ね(v2.1.295以降は全インストールで既定の動作)、legacyで全サーバーへの問い合わせを止める。v2.1.221以降 |
どちらも、認識できない値は無視されてデバッグログに警告が出ます。Claude Code v2.1.274のchangelogには、Bedrock・Vertex・Foundryとテレメトリー無効のインストールを既定でv2にした変更が、オプトアウト手段(MCP_SDK_GENERATION=v1かMCP_PROTOCOL_NEGOTIATION=legacy)つきで載っています。
結果として、v2ランタイムは接続の種類によって前提とするeraが違う非対称なDual-eraクライアントです。Modern版の接続は、Legacyにない振る舞いも運びます。
list_changed通知を、開いたままにするストリームで受け取る。ストリームが10秒以内に閉じる場合は最大3回まで再接続し、10秒より長く開いてから閉じる場合は、1時間に5回再接続すると次の再接続まで約6時間待つ- クライアント機能として
elicitation: {form: {}, url: {}}を宣言し、サーバーがフォーム形式とURL形式のどちらも要求できる - MCP OAuthの認可応答のissuerが想定と違えばサインインを失敗させる
Channelsにも影響があります。2026-07-28のModern版ではChannelsメッセージを配送できないため、その版で接続したChannelsサーバーは、Claude Codeにチャンネルとして登録されません。stdioでChannelsサーバーを自作している場合、v2.1.292以降は既定でModern版を試されます。2026-07-28に対応するサーバーはModern版で接続し、チャンネルとして登録されなくなります。対応しないサーバーは従来どおり登録されます。全サーバーをLegacyで接続させるにはMCP_PROTOCOL_NEGOTIATION=legacyを設定します。
この設計は何を優先しているか
ハンドシェイクの廃止は、MCPをよりステートレスなプロトコルに寄せる決定です。一方で移行は、Legacy実装を置き去りにしない形で進められています。era判定のキャッシュ、server/discoverの任意化、Modern専用サーバーがinitializeへのエラーに対応版を載せる求めは、いずれもその配慮です。
Claude Codeも、probeの対象をフラグ取得の有無を問わない形に広げてきました(v2.1.274でHTTP、v2.1.292でstdio、v2.1.295でclaude.aiコネクタ)。これも同じ方向の動きです。
まとめ
接続が成立しないときの切り分けは、相手のeraを先に決めるところから始まります。Legacyクライアントが詰むのはModern専用サーバーだけです。Modern専用サーバーを自作するなら、v2.1.292より前のClaude Codeでstdioトランスポートを使う場合、Legacyで呼ばれる前提で動作を確かめる、という判断が持ち帰れます。接続設定はClaude Code MCP設定ガイド、Agent SDKで独自ツールを組み込む方法はAgent SDKカスタムツールの作り方にあります。