TL;DR
- MCP の 2026-07-28 仕様は Roots / Sampling / Logging を deprecated にし、公式ブログは「12か月は動き続ける」と書いている
- しかし
@modelcontextprotocol/server@2.0.0を実際に動かすと、2026-07-28 era のリクエストを処理している最中はroots/list・sampling/createMessage・pingが即座に例外(METHOD_NOT_SUPPORTED_BY_PROTOCOL_VERSION)になる - 一方で
sendLoggingMessageだけは例外にならない。3 つまとめて deprecated と読むと、移行時に壊れる箇所を読み違える - v2.0.0 の
LATEST_PROTOCOL_VERSIONは2025-11-25のまま。2026-07-28 は initialize のバージョン交渉ではなく、リクエストごとの_metaエンベロープで判定される
対象読者は、Roots / Sampling / Logging を使った MCP サーバーを運用していて、v1(@modelcontextprotocol/sdk)から v2 への移行を検討している開発者です。
何を確かめたかったか
MCP の 2026-07-28 リリース告知 は、Roots・Sampling・Logging の 3 つを deprecated にしたうえで、こう書いています。
They still work, and they'll keep working for at least twelve months. New implementations shouldn't adopt them.
「12 か月は動く」と読めば、移行は急がなくてよさそうに見えます。ただ「動く」の主語が仕様なのか SDK 実装なのかは、この一文からは決まりません。そこで、実際に npm から取得した SDK で 3 つの機能が本当に同じ壊れ方をするのか を確かめました。
検証環境は Node.js v22.22.2 / npm 10.9.7、パッケージは @modelcontextprotocol/server@2.0.0・@modelcontextprotocol/client@2.0.0・比較用に @modelcontextprotocol/sdk@1.30.0 です。
前提: v2 はパッケージが分かれ、バージョン番号も別系統
まず npm 上の状態を確認します。
$ npm view @modelcontextprotocol/sdk dist-tags
{ latest: '1.30.0' }
$ npm view @modelcontextprotocol/server version
2.0.0
v1 の @modelcontextprotocol/sdk は 1.30.0 として今も更新が続いており、v2 は @modelcontextprotocol/server / @modelcontextprotocol/client という別パッケージの 2.0.0 です。@modelcontextprotocol/sdk@2.x を探しても存在しません。README にも「v2 is the stable release line, implementing the 2026-07-28 MCP spec」と明記されています。
ところが、その v2 が公開している定数はこうなっていました。
import * as S from '@modelcontextprotocol/server';
console.log(S.LATEST_PROTOCOL_VERSION);
console.log(JSON.stringify(S.SUPPORTED_PROTOCOL_VERSIONS));
2025-11-25
["2025-11-25","2025-06-18","2025-03-26","2024-11-05","2024-10-07"]
2026-07-28 は交渉可能なバージョン一覧に入っていません。ではどこで era が決まるのか。ランタイム(dist/*.mjs)を grep すると 2026-07-28 は 113 箇所に現れ、そのうちの一つがこう言っています。
Request is missing the required _meta envelope for protocol revision 2026-07-28
(io.modelcontextprotocol/protocolVersion, ...)
つまり 2026-07-28 は initialize のバージョン交渉ではなく、リクエストごとの _meta エンベロープで分類される。ステートレス化した仕様に合わせて、era 判定もリクエスト単位に移っています。SDK 内部の型もこれを type ProtocolEra = 'legacy' | 'modern' として持っていました。
対照実験: legacy era と modern era で同じ 4 つを叩く
同じサーバー実装に対して、era だけを変えて listRoots() / createMessage() / sendLoggingMessage() / ping() を呼びます。
条件A: legacy era(InMemoryTransport で 2025-11-25 を交渉)
import { Server, InMemoryTransport } from '@modelcontextprotocol/server';
import { Client } from '@modelcontextprotocol/client';
const [st, ct] = InMemoryTransport.createLinkedPair();
const server = new Server({ name: 'dep-probe', version: '0.0.1' },
{ capabilities: { logging: {}, tools: {} } });
const client = new Client({ name: 'probe-client', version: '0.0.1' },
{ capabilities: { roots: {}, sampling: {} } });
client.setRequestHandler('roots/list', async () => ({
roots: [{ uri: 'file:///tmp', name: 'tmp' }] }));
client.setRequestHandler('sampling/createMessage', async () => ({
model: 'stub', role: 'assistant', content: { type: 'text', text: 'ok' } }));
await Promise.all([server.connect(st), client.connect(ct)]);
結果です。
negotiated = 2025-11-25
OK roots/list (listRoots) -> {"roots":[{"uri":"file:///tmp","name":"tmp"}]}
OK sampling (createMessage) -> {"model":"stub","role":"assistant",...}
OK logging (sendLoggingMessage) -> undefined
OK ping -> {}
logging notifications received = 1
4 つとも通ります。deprecated ではあるが動く、という告知どおりの挙動です。
条件B: modern era(_meta エンベロープに 2026-07-28 を載せる)
同じ 4 つを、createMcpHandler 経由の HTTP リクエストとして、_meta に 2026-07-28 を明示した状態で呼びます。
const envelope = {
[S.PROTOCOL_VERSION_META_KEY]: '2026-07-28', // io.modelcontextprotocol/protocolVersion
[S.CLIENT_INFO_META_KEY]: { name: 'probe-client', version: '0.0.1' },
[S.CLIENT_CAPABILITIES_META_KEY]: {},
};
const req = new Request('http://localhost/mcp', {
method: 'POST',
headers: {
'content-type': 'application/json',
'Mcp-Method': 'tools/call',
'Mcp-Name': 'probe',
},
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call',
params: { name: 'probe', arguments: {}, _meta: envelope } }),
});
const res = await handler.fetch(req);
結果はこうなりました。
HTTP 200
roots/list SdkError[METHOD_NOT_SUPPORTED_BY_PROTOCOL_VERSION]
sampling/createMessage SdkError[METHOD_NOT_SUPPORTED_BY_PROTOCOL_VERSION]
ping SdkError[METHOD_NOT_SUPPORTED_BY_PROTOCOL_VERSION]
notifications/message (logging) OK
例外メッセージは次のとおりで、代替手段まで書かれています。
Server-to-client requests are not available on protocol revision 2026-07-28:
'roots/list' cannot be sent while serving a request on that revision.
Return inputRequired({ ... }) from the handler instead — the client fulfils
the embedded requests and retries the original request
(multi round-trip requests).
| API | legacy era(2025-11-25) | modern era(2026-07-28) |
|---|---|---|
listRoots() |
成功 | 即例外 |
createMessage()(Sampling) |
成功 | 即例外 |
ping() |
成功 | 即例外 |
sendLoggingMessage() |
成功 | 例外にならない |
Logging だけ扱いが違う理由
ランタイム側には _assertPushApiInServedEra(method) というガードがあり、その呼び出し箇所は 4 つだけ でした。
- ping
- sampling/createMessage
- elicitation/create
- roots/list
いずれも「サーバーがクライアントへリクエストを送る」方向の API です。2026-07-28 がステートレスコアへ寄せた結果、常時開いた双方向ストリームを前提とするこれらは era 内で成立しなくなり、代わりに Multi Round-Trip Requests(inputRequired(...))へ誘導されます。
対して Logging は notifications/message という 通知 であり、リクエスト/レスポンスの往復を必要としません。だからガードの対象外で、例外にもならない。ここが「3 つまとめて deprecated」という読み方では抜け落ちる部分です。
型定義側も確認しました。.d.mts から @deprecated ブロックを機械的に抽出すると、2026-07-28 起因が 40 箇所、2025-11-25 の wire vocabulary 起因が 18 箇所。メソッドでは createMessage(3 オーバーロード)・listRoots・sendLoggingMessage(2 箇所)・elicitInput・ping・getClientCapabilities が該当します。sendLoggingMessage も型では deprecated 扱い なので、IDE の取り消し線だけを見て「同じ壊れ方をする」と判断すると実挙動を取り違えます。
なお、今回の最小構成では modern era で sendLoggingMessage() を呼んでもレスポンスストリームに notifications/message は現れませんでした(io.modelcontextprotocol/logLevel を _meta に載せた場合も同じ)。SDK のソース上は modern era のとき _meta の log level をしきい値として読み、未設定なら送出せず戻る分岐になっていますが、この点は今回のハーネスでは「例外にならない」ところまでしか観測できていません。
移行時に効いた副作用: ヘッダーと本文の一致チェック
検証中に一度 HTTP 400 で弾かれました。
{"jsonrpc":"2.0","error":{"code":-32020,
"message":"Bad Request: the request headers and body disagree: the body carries
params.name=\"probe\" but the required Mcp-Name header is absent"}}
2026-07-28 era のリクエストは Mcp-Method に加えて Mcp-Name ヘッダーを要求し、本文と食い違うと処理前に落とします。ステートレス化の目的が「普通の HTTP インフラでルーティングできること」なので、ロードバランサやプロキシがボディを開かずに振り分けられるよう、ヘッダー側にも同じ情報を要求する設計です。自前のクライアントや中継を書いている場合は、ここが移行時の最初のつまずきになります。
実際に移行するときの判断材料
今回の実測から言えることを整理します。
-
v2 に上げただけでは壊れない。v2.0.0 の交渉ラダーは 2025-11-25 までで、クライアントが
_metaに 2026-07-28 を載せてこない限り legacy era のままです。移行の第一歩としてのパッケージ差し替えは、Roots / Sampling / Logging を使っていても即死しません -
壊れるのはクライアントが 2026-07-28 で話しかけてきたとき。そのときサーバー起点リクエストは例外になるので、
listRoots()/createMessage()に依存したツールハンドラはinputRequired(...)への書き換えが必要です - Logging は移行の優先度が下がる。例外にはならないため、Roots / Sampling の置き換えを先に片付けてよい。ただし modern era では送出条件が変わるので、ログが届いているかは別途確認したほうが安全です
-
pingの扱いに注意。公式ブログの deprecated 3 点セットには入っていませんが、実装上は同じガードで弾かれます。ヘルスチェックにpingを使っている監視系があるなら、そこも移行対象です
「12 か月動き続ける」は仕様レベルの約束であって、SDK が era を切り替えた瞬間の挙動とは別の話でした。移行計画を立てるときは、告知の文面ではなく どの era のリクエストを受けるのか を基準にするのが確実です。
関連記事
- mcp-cliに時刻取得を頼んだら、Geminiは無視しOpenAIは例外で落ちた
- MCP SDK v2 betaのcodemod、requireは0行変換で移行完了と出た
- can_use_toolはallowed_toolsの丸ごと許可で呼ばれなくなっていた