MCPの2026-07-28仕様で何が変わった?セッション・initialize廃止への移行手順

MCPの2026-07-28仕様で何が変わった?セッション・initialize廃止への移行手順 AI開発

MCPのSDKを更新したら、これまで使えていたクライアントから接続できなくなった。あるいは、新しいクライアントから既存のMCPサーバーに接続すると「Bad Request: No valid session ID provided」が返ってくる。2026年の後半に入って、こうしたトラブルが起きやすくなっています。

原因の多くは、2026年7月28日に公開されたMCPの仕様改訂(以下、2026-07-28仕様)です。この改訂では、接続の最初に行っていたinitializeハンドシェイクと、Streamable HTTPのセッションを表すMcp-Session-Idヘッダーが廃止されました。MCPの通信が、1回ごとに完結するステートレスな形に変わったのです。

この記事では、2026-07-28仕様で何が変わったのかを整理したうえで、新旧のクライアントとサーバーがどの組み合わせでつながるのか、セッションに頼っていた状態をどう持ち直すのか、TypeScript SDKをv1からv2へ移す手順を解説します。記事中のレスポンスは、公式TypeScript SDK v2(@modelcontextprotocol/server 2.4.0)と、比較用のv1(@modelcontextprotocol/sdk 1.32.2)でサーバーを実際に起動し、curlでリクエストを送って確認したものです。

スポンサーリンク

2026-07-28仕様の主な変更点

公式のchangelogには多くの変更が並んでいますが、既存のサーバーやクライアントの動作に影響しやすいものは次のとおりです。

項目 2025-11-25まで 2026-07-28から
接続の開始 initializeで合意してから通信する ハンドシェイクなし。毎回のリクエストの_metaにバージョンと能力を書く
セッション Mcp-Session-Idヘッダーで識別する 廃止。必要な状態はツールの引数で渡すIDで扱う
サーバー情報の取得 initializeの応答に含まれる server/discoverで取得する(サーバーは実装必須)
HTTPヘッダー MCP-Protocol-Versionのみ Mcp-MethodとMcp-Nameも必須
サーバーからの通知 GETで開くSSEストリーム、resources/subscribe subscriptions/listenに一本化
サーバー発のリクエスト サーバーがクライアントへ送る 結果にinput_requiredを返し、クライアントが再送する(MRTR)
削除されたメソッド – ping、logging/setLevel、notifications/roots/list_changed
ストリームの再開 Last-Event-IDで再開できる 廃止。切れたら新しいIDでリクエストし直す

このほか、すべての結果にresultTypeが必須になった、tools/listなどの結果にキャッシュ用のttlMsとcacheScopeが付いた、Tasksが本体から公式の拡張(io.modelcontextprotocol/tasks)に移った、といった変更もあります。Roots・Sampling・Loggingの非推奨化はこの改訂の大きなテーマですが、別の記事のMCPのRoots・Sampling・Logging非推奨で何が変わる?代替実装を解説で詳しく扱っているので、ここでは通信方式の変更に絞ります。

セッションの廃止には猶予期間がありません
同じ改訂で、非推奨になった機能を最低12か月は残す方針が定められました。ただしこれは「非推奨」に分類された機能の話です。セッションの廃止を提案したSEP-2567では、猶予期間を設けずに切り替えると明記されています。セッションに頼るサーバーは、移行が終わるまで旧バージョンの仕様のまま運用することになります。

initializeの廃止で、毎回のリクエストに_metaを付ける

これまでのMCPでは、クライアントが最初にinitializeを送り、使うプロトコルのバージョンと、お互いの能力(capabilities)を決めてから、tools/listやtools/callを送っていました。サーバーは、その合意の内容を接続やセッションに結び付けて覚えておく必要がありました。

2026-07-28仕様では、このハンドシェイクがなくなりました。代わりに、すべてのリクエストがparams._metaに次の情報を持ちます。

  • io.modelcontextprotocol/protocolVersion:このリクエストで使うプロトコルのバージョン(必須)
  • io.modelcontextprotocol/clientCapabilities:クライアントの能力(必須)
  • io.modelcontextprotocol/clientInfo:クライアントの名前とバージョン(付けることが推奨)

Streamable HTTPでツールを呼び出すリクエストは、次のようになります。ヘッダーについては後で説明します。

tools/callのリクエスト(2026-07-28)
curl -s -X POST http://127.0.0.1:3000/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2026-07-28' \
  -H 'Mcp-Method: tools/call' \
  -H 'Mcp-Name: create_cart' \
  -d '{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "create_cart",
    "arguments": {},
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientInfo": { "name": "curl", "version": "1.0.0" },
      "io.modelcontextprotocol/clientCapabilities": {}
    }
  }
}'
レスポンス(実際の出力を整形)
{
  "result": {
    "content": [{ "type": "text", "text": "{\"cart_id\":\"85a47876-97ee-4b2a-868e-43c8139b22ed\"}" }],
    "resultType": "complete",
    "_meta": {
      "io.modelcontextprotocol/serverInfo": { "name": "cart-server", "version": "1.0.0" }
    }
  },
  "jsonrpc": "2.0",
  "id": 2
}

結果にはresultType: "complete"が付き、サーバー情報は_metaのserverInfoで返ってきます。1回のリクエストの中にバージョンも能力も書かれているので、サーバーは前のリクエストを覚えておく必要がありません。

対応していないバージョンを指定した場合

サーバーが対応していないバージョンを指定すると、UnsupportedProtocolVersionError(コード-32022)が返ります。存在しない2027-01-01を指定して試した結果です。

対応外のバージョンを送ったときの応答(HTTP 400)
{"jsonrpc":"2.0","error":{"code":-32022,"message":"Unsupported protocol version: 2027-01-01","data":{"supported":["2026-07-28"],"requested":"2027-01-01"}},"id":5}

data.supportedに、サーバーが対応しているバージョンの一覧が入ります。クライアントはこの中から使えるバージョンを選んで、リクエストを送り直します。ハンドシェイクで事前に交渉する代わりに、失敗したら一覧を見て選び直すという流れになったわけです。

server/discoverでサーバーの情報を取得する

initializeの応答で受け取っていたサーバーの名前や能力は、新しく追加されたserver/discoverで取得します。仕様上、サーバーはこのメソッドを必ず実装しなければなりません。一方、クライアントが呼ぶかどうかは任意です。いきなりtools/callを送り、バージョンが合わなければ前述のエラーを処理する、という使い方もできます。

server/discoverの応答(実際の出力)
{"result":{"supportedVersions":["2026-07-28"],"capabilities":{"tools":{"listChanged":true}},"resultType":"complete","ttlMs":0,"cacheScope":"private","_meta":{"io.modelcontextprotocol/serverInfo":{"name":"cart-server","version":"1.0.0"}}},"jsonrpc":"2.0","id":1}

ttlMsとcacheScopeは、今回の改訂で追加されたキャッシュ用の情報です。SDK v2の初期値は、ttlMs: 0(キャッシュしない)とcacheScope: "private"でした。server/discoverが特に役立つのはstdioです。HTTPのステータスコードがないstdioでは、クライアントが最初にserver/discoverを送り、新しい形式のエラー以外が返ってきたら旧仕様のサーバーとみなす、という判定の方法が仕様で定められています。

Mcp-Session-Idの廃止と、状態を持つツールの作り方

移行でいちばん手間がかかるのが、セッションに状態を結び付けていたサーバーです。これまでは、Mcp-Session-Idごとにサーバーのインスタンスを用意し、その中に作業中のデータを持たせる作り方がよく使われていました。2026-07-28仕様では、この仕組みそのものがありません。

仕様が推奨しているのは、サーバーが発行したIDを、ツールの普通の引数として受け渡す方法です。例えば、カートを作るツールがcart_idを返し、商品を追加するツールが引数でcart_idを受け取るようにします。SEP-2567では、セッションは1つの接続に1つしか持てず、「サブエージェント同士でカートは共有し、ブラウザの状態は分ける」といった使い分けができない点も、廃止の理由に挙げられています。IDを引数で渡す方法なら、共有するか分けるかを呼び出し側で選べます。

server.mjs(SDK v2・状態をIDで持つサーバー)
import { createServer } from 'node:http';
import { randomUUID } from 'node:crypto';
import { McpServer, createMcpHandler } from '@modelcontextprotocol/server';
import { toNodeHandler } from '@modelcontextprotocol/node';
import * as z from 'zod/v4';

// 状態はファクトリの外に置く(本番ではRedisやDBに置く)
const carts = new Map();

function buildServer() {
  const server = new McpServer({ name: 'cart-server', version: '1.0.0' });

  server.registerTool('create_cart', {
    description: 'カートを作成し、以降の操作で使うcart_idを返す',
    inputSchema: z.object({}),
  }, async () => {
    const cartId = randomUUID();
    carts.set(cartId, []);
    return { content: [{ type: 'text', text: JSON.stringify({ cart_id: cartId }) }] };
  });

  server.registerTool('add_item', {
    description: 'cart_idで指定したカートに商品を追加する',
    inputSchema: z.object({ cart_id: z.string(), item: z.string() }),
  }, async ({ cart_id, item }) => {
    const cart = carts.get(cart_id);
    if (!cart) {
      return { isError: true, content: [{ type: 'text', text: 'cart_idが見つかりません' }] };
    }
    cart.push(item);
    return { content: [{ type: 'text', text: JSON.stringify({ cart_id, items: cart }) }] };
  });

  return server;
}

const handler = createMcpHandler(buildServer);
const nodeHandler = toNodeHandler(handler);

createServer((req, res) => {
  if (req.url !== '/mcp') { res.writeHead(404).end(); return; }
  nodeHandler(req, res);
}).listen(3000, '127.0.0.1');

create_cartで受け取ったIDを使って、add_itemを2回呼び出しました。2回目のリクエストには、試しにMcp-Session-Id: dummy-session-123というヘッダーも付けています。

add_itemを2回呼んだ結果(実際の出力から抜粋)
{"result":{"content":[{"type":"text","text":"{\"cart_id\":\"b6b9532a-...\",\"items\":[\"apple\"]}"}], ...}
{"result":{"content":[{"type":"text","text":"{\"cart_id\":\"b6b9532a-...\",\"items\":[\"apple\",\"orange\"]}"}], ...}

リクエストは毎回独立していますが、cart_idを渡すことで、前の操作の続きとして処理できました。付けたMcp-Session-Idは無視され、応答のヘッダーにもセッションIDは含まれません。仕様でも、新しいサーバーはこのヘッダーを無視し、セッションIDを発行も返却もしないことになっています。

落とし穴:ファクトリの中に状態を置くと毎回消える

SDK v2のcreateMcpHandlerは、引数に渡した関数(ファクトリ)をリクエストのたびに呼び出して、新しいMcpServerを作ります。これを知らずに、状態を入れるMapをファクトリの中に書くと、どうなるかを試しました。

NG: 状態をファクトリの中に置いた例
let factoryCalls = 0;

function buildServer() {
  const carts = new Map(); // NG: リクエストごとに作り直される
  console.log(`factory called: ${++factoryCalls}`);
  const server = new McpServer({ name: 'cart-server', version: '1.0.0' });
  // ...ツールの登録は同じ...
  return server;
}
create_cartのあとにadd_itemを2回呼んだ結果
{"result":{"content":[{"type":"text","text":"cart_idが見つかりません"}],"isError":true, ...}
{"result":{"content":[{"type":"text","text":"cart_idが見つかりません"}],"isError":true, ...}

# サーバーのログ
factory called: 1
factory called: 2
factory called: 3

3回のリクエストで、ファクトリが3回呼ばれています。create_cartで作ったカートは、そのリクエストのMapと一緒に捨てられるため、次のadd_itemからは見えません。v1でセッションごとにサーバーを作っていた感覚のまま書くと、この不具合が起きます。

なお、上の正しい例でもプロセスのメモリに状態を置いているので、サーバーを複数台に増やしたり再起動したりすると状態が失われます。ステートレス化の利点は、どのサーバーにリクエストが届いても処理できることです。本番では状態をRedisやデータベースに置き、IDから引けるようにしてください。

IDを推測されないようにする
IDが漏れると、ほかの利用者がそのカートを操作できてしまうおそれがあります。SEP-2567の議論でも、この点は指摘されていました。IDには推測しにくいランダムな値(UUIDなど)を使い、さらにツールの中で「このIDは、認証された利用者のものか」を確認します。セッションがないので、認証の確認もリクエストごとに行います。考え方はMCPでOAuth認証を実装する方法|アクセストークンを安全に扱う設計とMCPツールに破壊的操作を持たせるときの設計|削除・更新処理を安全にするで解説しています。

Streamable HTTPではMcp-MethodとMcp-Nameが必須になった

Streamable HTTPでは、JSONの本文に書かれている一部の値を、HTTPヘッダーにも書くことが必須になりました。ロードバランサーやゲートウェイが、本文を解析しなくても、どのメソッドやツールへのリクエストかを判断できるようにするためです。

  • MCP-Protocol-Version:本文の_metaのバージョンと同じ値
  • Mcp-Method:本文のmethod(すべてのリクエスト)
  • Mcp-Name:本文のparams.nameまたはparams.uri(tools/call、resources/read、prompts/get)

ヘッダーと本文が食い違っていると、サーバーは400 Bad RequestとHeaderMismatch(コード-32020)を返します。ゲートウェイがヘッダーを見て振り分け、MCPサーバーが本文を見て実行する構成で、両者の判断がずれる事故を防ぐための決まりです。実際に、3つのパターンを試しました。

Mcp-Nameを付け忘れた場合
{"jsonrpc":"2.0","error":{"code":-32020,"message":"Bad Request: the request headers and body disagree: the body carries params.name=\"create_cart\" but the required Mcp-Name header is absent", ...},"id":3}
Mcp-Nameが本文のツール名と違う場合
{"jsonrpc":"2.0","error":{"code":-32020,"message":"Bad Request: the request headers and body disagree: the body carries params.name=\"create_cart\" but the Mcp-Name header names \"add_item\"", ...},"id":4}
MCP-Protocol-Versionが本文のバージョンと違う場合
{"jsonrpc":"2.0","error":{"code":-32020,"message":"Bad Request: the request headers and body disagree: the body envelope names protocol version 2026-07-28 but the MCP-Protocol-Version header names 2025-11-25", ...},"id":10}

また、MCP-Protocol-Version: 2026-07-28を付けたのに本文に_metaがない場合は、-32602(Invalid params)になりました。「_metaが足りない」とメッセージに出るので、自作のクライアントやテスト用のスクリプトを移行するときは、ここを確認してください。-32602のほかの原因はMCPサーバーで「Invalid Params」が出る原因|JSON Schemaと引数を確認にまとめています。

ゲートウェイでこれらのヘッダーを使う方法は、MCPサーバーをローカルではなくリモート公開する方法|認証・HTTPSの基本で紹介しています。

GETのストリームと再開の仕組みがなくなった

これまでのStreamable HTTPでは、クライアントがMCPのエンドポイントにGETを送ってSSEのストリームを開き、サーバーからの通知やリクエストを受け取れました。2026-07-28仕様ではこのGETが廃止され、エンドポイントはPOSTだけを受け付けます。SDK v2のサーバーにGETを送ると、405 Method Not Allowedとallow: POSTヘッダーが返りました。

ツール一覧の変更などの通知を受け取りたいクライアントは、subscriptions/listenをPOSTで送ります。その応答が長く続くSSEストリームになり、事前に選んだ種類の通知(toolsListChangedなど)だけが流れてきます。進捗(notifications/progress)のように特定のリクエストに関係する通知は、これまでどおり、そのリクエストの応答ストリームで届きます。

もう1つの変更は、Last-Event-IDによるストリームの再開がなくなったことです。応答の途中で接続が切れると、そのリクエストは失われます。クライアントは、新しいリクエストIDで送り直さなければなりません。途中で切れて再送されても困らないツールにしておくことが、これまで以上に大切になりました。冪等性の考え方はMCPツールがタイムアウトする原因|再試行・キャンセル・冪等性の設計で解説しています。

pingやlogging/setLevelを送るとMethod not foundになる

ping、logging/setLevel、notifications/roots/list_changedは削除されました。接続の死活監視にpingを使っていたクライアントは、新しい仕様のサーバーに送ると失敗します。実際に送ると、HTTPの404 Not Foundと次のエラーが返りました。

pingを送ったときの応答(HTTP 404)
{"jsonrpc":"2.0","id":11,"error":{"code":-32601,"message":"Method not found"}}

ログのレベルは、logging/setLevelで接続全体に設定するのではなく、リクエストごとに_metaのio.modelcontextprotocol/logLevelで指定するようになりました。このフィールドがないリクエストに対して、サーバーはログの通知を送ってはいけません。詳しくはMCPのProtocol Loggingが非推奨になった理由|stderrとOpenTelemetryでのログ設計を参照してください。

Method not foundが出たときの切り分けは、MCPサーバーが「Method not found」になる原因|tools/list・tools/callを確認で詳しく解説しています。

サーバーからの問い合わせはMRTRに変わった

ツールの実行中に、サーバーからクライアントへ「ユーザーに確認してほしい」(elicitation)、「LLMで文章を生成してほしい」(sampling)と依頼する機能があります。これまでは、サーバーがクライアントへ独立したリクエストを送っていました。

2026-07-28仕様では、これがMulti Round-Trip Requests(MRTR)という方式に変わりました。サーバーはresultType: "input_required"の結果を返し、inputRequestsで必要な情報を伝えます。クライアントは集めた回答をinputResponsesに入れて、元のリクエストを新しいIDで送り直します。サーバー側で途中の状態を持つ必要があれば、requestStateに入れて往復させます。セッションがなくても、確認を挟む処理を書けるようにした仕組みです。具体的な形はMCPのstdioとStreamable HTTPの違い|ローカル・リモート構成の選び方でも紹介しています。

エラーコードの変更

エラーの種類で処理を分けているクライアントは、コードの変更にも注意が必要です。今回の改訂で、-32020から-32099がMCPの仕様用に予約され、新しいエラーはこの範囲に割り当てられました。

エラー コード 主なHTTPステータス
HeaderMismatch(ヘッダーと本文の不一致) -32020 400
MissingRequiredClientCapability(必要な能力をクライアントが持っていない) -32021 400
UnsupportedProtocolVersion(対応外のバージョン) -32022 400
リソースが見つからない -32002から-32602に変更 –
メソッドが存在しない -32601(変更なし) 404

仕様の策定途中の版では-32001、-32003、-32004が使われていたため、ドラフトを見て実装したコードは番号がずれている可能性があります。

新旧のクライアントとサーバーはつながるのか

移行期間中に気になるのが、新旧の組み合わせです。公式の仕様では、2026-07-28以降の新しい方式を「Modern」、initializeを使う2025-11-25以前の方式を「Legacy」、両方に対応した実装を「Dual-era」と呼び、互換性を表にまとめています。要点は次のとおりです。

  • 新しいクライアント → 旧仕様だけのサーバー:つながりません。
  • 旧仕様だけのクライアント → 新仕様だけのサーバー:つながりません。旧クライアントには、新しい方式へ切り替える仕組みがないためです。
  • 両対応のクライアント → どちらのサーバー:つながります。新しい形式で送り、失敗したらinitializeにフォールバックします。
  • 旧クライアント → 両対応のサーバー:つながります。サーバーがinitializeに応答します。

つまり、片方だけを新しくすると、相手が両対応でない限り接続できません。冒頭で触れた「Bad Request: No valid session ID provided」は、新しい形式のリクエストが、v1 SDKでセッション必須に作ったサーバーに届いたときのエラーです。v1のサーバーを起動して試すと、次のようになりました。

v1のセッション必須サーバーに、新しい形式のtools/listを送った結果(HTTP 400)
{"jsonrpc":"2.0","error":{"code":-32000,"message":"Bad Request: No valid session ID provided"},"id":null}

新しい形式のリクエストにはMcp-Session-Idがないので、v1のサーバーは「セッションIDがない不正なリクエスト」として拒否します。-32000は新しい仕様のエラーコードではないため、両対応のクライアントであれば、ここで旧仕様のサーバーと判断してinitializeに切り替えます。切り替えに対応していないクライアントでは、このエラーがそのまま表示されます。

SDK v2のサーバーは、既定で旧クライアントも受け付ける

SDK v2のcreateMcpHandlerは、既定で両対応のサーバーとして動きます。旧クライアントのinitializeを送ると、2025-11-25として正常に応答しました。

SDK v2(既定)に旧クライアントのinitializeを送った結果
HTTP/1.1 200 OK
content-type: text/event-stream

event: message
data: {"result":{"protocolVersion":"2025-11-25","capabilities":{"tools":{"listChanged":true}},"serverInfo":{"name":"cart-server","version":"1.0.0"}},"jsonrpc":"2.0","id":6}

応答にはmcp-session-idヘッダーがありません。旧クライアントにも、セッションを使わないステートレスな形で応答しています。v1と同じファクトリからツールを提供するので、新旧で機能がずれる心配もありません。

新しい仕様だけを受け付けたい場合は、createMcpHandler(buildServer, { legacy: 'reject' })とします。この設定で同じinitializeを送ると、対応しているバージョンの一覧付きで拒否されました。

legacy: ‘reject’で旧クライアントのinitializeを送った結果(HTTP 400)
{"jsonrpc":"2.0","error":{"code":-32022,"message":"Unsupported protocol version: 2025-11-25","data":{"supported":["2026-07-28"],"requested":"2025-11-25"}},"id":6}

利用者がどのクライアントを使っているか分からない公開サーバーでは、当面は既定の両対応のままにしておくのが安全です。stdioのserveStdioも、新しいserver/discoverと旧来のinitializeの両方に応答することを確認しました。

TypeScript SDKをv1からv2へ移行する

TypeScriptの公式SDKは、v1の@modelcontextprotocol/sdkが2025-11-25までの仕様に対応し、2026-07-28仕様にはv2が対応しています。v2の最初の安定版(2.0.0)は、仕様の公開に合わせて2026年7月27日(UTC)に公開されました。v1も更新は続いていますが、2026年10月23日公開の1.32.2でSUPPORTED_PROTOCOL_VERSIONSを確認すると、最新は2025-11-25のままでした。v1を最新版に更新しても、2026-07-28仕様には対応しません。

パッケージを入れ替える

v2では、1つだった@modelcontextprotocol/sdkが、server、client、coreに分かれました。Expressなどを使う場合のアダプターも別のパッケージです。Node.js 20以上と、zod 4.2以上が必要です(zod v3は使えません)。

v2のインストール(サーバー側・素のNode.jsで動かす場合)
npm uninstall @modelcontextprotocol/sdk
npm install @modelcontextprotocol/server @modelcontextprotocol/node zod@4

@modelcontextprotocol/nodeは、Web標準のfetch形式で作られたハンドラーを、Node.jsのhttpサーバーで使うためのアダプターです。Express、Fastify、Hono向けのアダプターも用意されています。検証した時点では、serverが2.4.0、nodeが2.1.2で、バージョン番号はパッケージごとに異なっていました。

セッション管理のコードを削除する

v1でStreamable HTTPのサーバーを作ると、セッションIDごとにトランスポートを保存し、initializeのときだけ新しく作る、という定型のコードが必要でした。

移行前:v1のセッション管理(要点のみ)
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';

const transports = {}; // セッションIDごとにトランスポートを保持

app.post('/mcp', async (req, res) => {
  const sessionId = req.headers['mcp-session-id'];
  let transport = sessionId ? transports[sessionId] : undefined;

  if (!transport && isInitializeRequest(req.body)) {
    transport = new StreamableHTTPServerTransport({
      sessionIdGenerator: () => randomUUID(),
      onsessioninitialized: (id) => { transports[id] = transport; },
    });
    await buildServer().connect(transport);
  }
  if (!transport) {
    res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Bad Request: No valid session ID provided' }, id: null });
    return;
  }
  await transport.handleRequest(req, res, req.body);
});

v2では、この部分がまるごとcreateMcpHandlerに置き換わります。セッションの保存場所も、initializeの判定も、GETやDELETEの処理も不要です。

移行後:v2
import { createMcpHandler } from '@modelcontextprotocol/server';
import { toNodeHandler } from '@modelcontextprotocol/node';

const handler = createMcpHandler(buildServer);   // buildServerはリクエストごとに呼ばれる
const nodeHandler = toNodeHandler(handler);

createServer((req, res) => {
  if (req.url !== '/mcp') { res.writeHead(404).end(); return; }
  nodeHandler(req, res);
}).listen(3000, '127.0.0.1');

前述のとおり、buildServerはリクエストごとに呼ばれます。v1でセッションの中に持たせていたデータは、ファクトリの外のストアに移し、IDの引数で引けるように作り直してください。

ツール登録とエラー処理のAPIを書き換える

公式の移行ガイドに挙がっている、書き換えが必要になりやすい変更です。

  • server.tool()、server.prompt()、server.resource()は削除され、registerTool、registerPrompt、registerResourceだけになりました。
  • setRequestHandler(CallToolRequestSchema, ...)は、setRequestHandler('tools/call', ...)のようにメソッド名の文字列で書きます。
  • ハンドラーの第2引数はextraからctxに変わりました。例えばextra.signalはctx.mcpReq.signalになります。
  • McpErrorはProtocolErrorに、ErrorCodeはProtocolErrorCodeに名前が変わりました。
  • 旧HTTP+SSE方式のSSEServerTransportは削除されました。

registerToolのinputSchemaは、v2ではz.object({...})で渡します。この記事のサンプルもこの書き方で動作を確認しています。

ほかの言語のSDKを使っている場合

GitHubの発表(2026年7月23日)によると、主要な公式SDK(Tier 1)は後方互換を保ったまま2026-07-28仕様のベータ対応を済ませていました。ただし、対応の状況やAPIはSDKごとに異なります。PythonやGoなどを使っている場合は、各SDKのリリースノートで対応バージョンを確認してください。この記事で動作を確かめたのは、TypeScript SDKだけです。

移行前に確認するチェックリスト

既存のMCPサーバーを2026-07-28仕様に移すときに、確認しておきたい項目です。

  • Mcp-Session-Idやセッション単位のメモリに、作業中のデータを持たせていないか
  • 状態が必要なツールは、IDを返すツールと、IDを引数で受け取るツールに分けたか
  • IDは推測しにくい値になっているか。ツールの中で利用者の権限を確認しているか
  • 状態をプロセスのメモリではなく、RedisやDBに置いたか(複数台・再起動への備え)
  • tools/listの結果が、接続や直前のリクエストによって変わらないか(2026-07-28仕様で求められている)
  • pingやlogging/setLevelに頼った処理が残っていないか
  • 応答が途中で切れて再送されても、二重に実行されて困らないツールになっているか
  • エラーコードで分岐しているクライアントは、-32020〜-32022と-32602に対応したか
  • ゲートウェイやプロキシが、Mcp-MethodとMcp-Nameを削らずに転送するか
  • 旧クライアントの利用者がいる間は、両対応(SDK v2の既定)のままにしているか

移行後の動作確認には、MCP Inspectorが使えます。2026-07-28仕様での注意点を含めて、MCP Inspectorの使い方|ツールを単体テストして接続エラーを調べるで手順を解説しています。本番環境では、ヘッダーの不一致や対応外バージョンのエラーの件数を監視しておくと、古いクライアントの残り具合が分かります。監視の設計はMCPサーバーを本番運用する方法|メトリクス・アラート・トレースの設計を参考にしてください。

よくある質問

Q2026-07-28仕様に、すぐ移行しなければいけませんか?

A旧仕様のサーバーがすぐに動かなくなるわけではありません。ただし、新しい仕様だけに対応したクライアントからは接続できなくなります。利用者が使うクライアントの対応が進む前に、両対応のサーバーにしておくと安全です。SDK v2のcreateMcpHandlerは、既定で両対応になります。

Qstdioのサーバーにも影響はありますか?

Aあります。initializeの廃止、_metaの付与、pingなどの削除は、トランスポートに関係なく適用されます。HTTPのヘッダーに関する決まりは、Streamable HTTPだけのものです。stdioでは、クライアントがまずserver/discoverを送って新旧を判定します。

Qセッションの代わりにCookieや独自のヘッダーで状態を持ってもよいですか?

A仕様が推奨しているのは、サーバーが発行したIDをツールの引数で受け渡す方法です。引数であればモデルにも見えるので、複数の状態を使い分けたり、別のエージェントに引き継いだりできます。HTTPのCookieに頼る方法は、stdioでは使えず、クライアントの実装にも左右されます。

Qv1のSDKのまま、2026-07-28仕様に対応する方法はありますか?

A検証した1.32.2では、対応しているバージョンが2025-11-25までで、server/discoverも実装されていませんでした。2026-07-28仕様に対応するには、v2のパッケージに移行する必要があります。

QTasksは使えなくなったのですか?

A使えます。実験的な機能として本体にあったTasksは、公式の拡張(io.modelcontextprotocol/tasks)に移りました。結果を待つtasks/resultはなくなり、tasks/getで状態を確認する方式に変わっています。

まとめ

MCPの2026-07-28仕様では、initializeハンドシェイクとMcp-Session-Idがなくなり、すべてのリクエストが自分の中にバージョンと能力を持つ形になりました。一般的なHTTPのAPIと同じように、ロードバランサーの後ろに何台でもサーバーを並べられます。その代わり、サーバーは前のリクエストを覚えていないことを前提に作る必要があります。

移行で特に注意したいのは、次の3点です。

  • セッションに置いていた状態は、IDを引数で受け渡す設計に作り直し、外部のストアに置く
  • SDK v2では、ファクトリがリクエストごとに呼ばれるので、ファクトリの中に状態を置かない
  • 片方だけを新しくすると接続できないため、移行期間はサーバーを両対応にしておく

まずは手元でSDK v2のサーバーを起動し、この記事のcurlのリクエストを送って、ヘッダーや_metaが足りないときの応答を確かめてみてください。エラーの意味が分かっていれば、移行中に問題が起きたときも原因を見つけやすくなります。

参考資料