initialize ハンドシェイクが消える。MCP(Model Context Protocol)の2026-07-28仕様は、プロトコルからセッションの概念を外し、各リクエストを自己記述的にする。前回の当サイト記事でステートレス化の全体像を追った続報として、今回はBeta SDKが出揃った段階を使い、自作MCPサーバの移行が実際に何を壊すのかを手を動かして確かめる。
目次
何が起きたのか
公式ブログの投稿は6月29日。SDK Betas for the 2026-07-28 spec で、Tier-1の4言語すべてのBeta SDKが公開された。Python mcp v2.0.0b1、TypeScript v2、Go v1.7.0-pre.1、C# 2.0.0-preview.1。最終仕様の公開は2026年7月28日を予定していて、現時点ではRC相当の位置づけになる。確定リリースではない点は最初に押さえておきたい。
自作のMCPサーバを本番で回している人ほど、7月28日という日付が重く見えるはず。ハンドシェイクが消えると聞いて、まず気になるのは「うちのサーバは当日いきなり動かなくなるのか」だろう。結論を先に置くと、後方互換があるので当日いきなり全滅する設計にはなっていない。何もしないで済む話でもなく、Betaで壊れる箇所を今のうちに一つずつ潰せるかどうかで、当日の落ち着き方が変わる。
柱はステートレス化だ。旧仕様では、クライアントとサーバが最初に initialize をやり取りしてプロトコルレベルのセッションを張り、そのセッションの上で以降のリクエストが流れていた。新仕様はこのハンドシェイクとセッションを廃止する。各リクエストが自分の文脈を丸ごと抱える形になり、サーバの capabilities は server/discover で取りに行く。
これは破壊的変更だ。
効いてくるのは運用面だ。セッション状態を持たないので、どのサーバインスタンスが任意のリクエストを処理してもよい。ロードバランサの背後に同じMCPサーバを何台も並べ、素直なラウンドロビンで捌ける。特定クライアントを特定インスタンスへ固定するスティッキーセッションの設定が要らなくなる。この一点だけでも、コンテナで水平スケールさせている環境には効く。
旧仕様と新仕様はどう違うのか
移行の設計をする前に、差分を一枚で見渡しておく。
| 観点 | 旧(ステートフル:initialize+セッション) | 新(ステートレス:自己記述) |
|---|---|---|
| 接続開始 | initialize ハンドシェイクでセッション確立 |
ハンドシェイクなし。各リクエストが自己完結 |
| capabilities取得 | initialize の応答に同梱 | server/discover で個別取得 |
| リクエストの性質 | セッション文脈に依存 | 自己記述的(単体で処理可能) |
| 負荷分散 | スティッキーセッション前提 | ラウンドロビン可。どのインスタンスでも処理可 |
| ルーティング | セッションIDで対応付け | ヘッダ Mcp-Method / Mcp-Name で振り分け |
| リソース未検出のエラー | JSON-RPC -32002 |
JSON-RPC -32602 に標準化 |
エラーコードの変更は地味だが見落としやすい。missing resource が旧 -32002 から -32602(invalid params 系)へ寄せられた。クライアント側でエラーコードを分岐に使っている実装は、ここでリソース未検出の分岐が死ぬ。ハンドリングを数値決め打ちで書いていた箇所は洗い出しておく。
ルーティングの発想も変わる。セッションIDでインスタンスに結びつけていた対応付けが消え、代わりに Mcp-Method と Mcp-Name のヘッダでリクエストを振り分ける。プロキシやAPIゲートウェイでメソッド単位・ツール単位の振り分けを書けるので、重いツールだけ別プールへ逃がすといった制御がインフラ層で完結する。セッション固定の設定を外せるぶん、ここの設計はむしろ楽になる。
対話系の追加も入った。Multi Round-Trip Requests で、ツールが処理の途中に InputRequiredResult を返し、ユーザーへ追加入力を問い返せる。認可の追加確認や、不足パラメータの補完を一往復で挟めるようになる。
問い返しの形はこうなる。ツールが最終結果の代わりに InputRequiredResult を投げ、クライアントへ追加入力を促す。
# Multi Round-Trip:処理の途中で追加入力を求める(形のイメージ)
from mcp.types import InputRequiredResult
@app.tool()
def deploy(target: str):
if target == "production":
return InputRequiredResult(
prompt="本番へ反映します。確認コードを入力してください。"
)
return run_deploy(target)
接続を切って張り直す往復を挟まず、単発リクエストの中で対話の一往復を完結させられる。ステートレス化と噛み合う仕組みで、認可の再確認をツールの内側に閉じ込められる。
移行で何が壊れて、何が楽になるか
Pythonから見ると、破壊の実体は改名に集約される。エントリポイントの FastMCP が MCPServer になった。ツール定義のデコレータまわりの書き味は近いので、まずはimportとクラス名の置換から入れる。
# v1(2025-11-25 仕様)
from mcp.server.fastmcp import FastMCP
app = FastMCP("weather")
@app.tool()
def get_forecast(city: str) -> str:
return fetch(city)
# v2.0.0b1(2026-07-28 仕様)
from mcp.server import MCPServer
app = MCPServer("weather")
@app.tool()
def get_forecast(city: str) -> str:
return fetch(city)
Beta は明示的にピン留めして入れる。安定版の依存に混ざって勝手に上がると事故になる。
# Python: Beta を明示ピン留め
pip install "mcp==2.0.0b1"
重いのはTypeScriptだ。単一パッケージだった @modelcontextprotocol/sdk が廃止され、機能ごとの分割パッケージに再編された。ESM専用になり、対応ランタイムはNode 20以上。スキーマ検証はStandard Schemaに寄せられた。CommonJSのまま動かしているプロジェクトや、Node 18で止めている環境は、SDKの入れ替えだけで済まず、モジュール形式とランタイムの移行がセットで乗ってくる。require ベースのビルド設定、__dirname の扱い、テストランナーのESM対応まで芋づるで確認が要る。
GoはBeta前段の v1.7.0-pre.1、C#は 2.0.0-preview.1。認可まわりは6本のSEPでOAuth/OIDCが強化された。筆者は以前、EVEダッシュボードでOAuth2/PKCEのトークン自動リフレッシュを実装したとき、リフレッシュの失敗時に無限リトライへ落ちる罠を踏んだことがある。認可の仕様が動く更新では、正常系より失敗系のテストを先に置くほうが安全だと考えている。
後方互換があるので、一気に切らなくていい
ここが今回いちばんの安心材料だ。v2サーバはレガシーの initialize にも応答する。2025-11-25 仕様のクライアントは、v2サーバに対して従来通り接続を維持できる。サーバを先に新仕様へ上げ、クライアント群を後から順に移す段階移行が成立する。
「サーバとクライアントを同時に切り替える」ビッグバン移行を強いられないので、本番の停止ウィンドウを取らずに進められる。まずサーバをv2化してレガシー応答で既存クライアントを生かし、疎通を確認しながらクライアントを一つずつ新仕様に寄せていく。この順番なら、どこかで壊れても切り戻す先が残る。
自作サーバ2本に、実際に通してみた
当サイトの運用で使っている自作の MCP サーバは2本ある。文字起こしの whisper-mcp と、X検索・YouTubeチャット収集の scraper-mcp。どちらも Python の mcp パッケージで書いて stdio で動かしている。この2本に何が起きるかを手元で確かめた。
まず、いま何を話しているのか
両サーバを起動して、クライアント側から protocolVersion: "2026-07-28" を要求する initialize を投げた。
// 送ったもの
{"method":"initialize","params":{"protocolVersion":"2026-07-28"}}
// 返ってきたもの(whisper-mcp / scraper-mcp とも同じ形)
{"protocolVersion":"2025-11-25",
"serverInfo":{"name":"whisper-mcp","version":"1.28.1"},
"capabilities":{"experimental":{},
"prompts":{"listChanged":false},
"resources":{"subscribe":false,"listChanged":false},
"tools":{"listChanged":false}}}
新仕様を要求しても 2025-11-25 が返る。入っている SDK が mcp 1.28.1 で、まだ 2026-07-28 を話さないからだ。ネゴシエーションが働いて旧仕様へ落ちている。
serverInfo.version が 1.28.1 なのは想定していなかった。pyproject.toml に書いた版は 0.1.0 で、そちらは出ていない。FastMCP は SDK の版を serverInfo に入れる。クライアント側でサーバの版を見て分岐しているなら、見ているのは自分のサーバの版ではない。
capabilities を見れば、壊れる範囲が絞れる
返ってきた capabilities に、移行の見積もりがそのまま出ていた。
| 項目 | 実測値 | 移行への影響 |
|---|---|---|
resources.subscribe |
false | 購読を使っていない。セッション廃止の影響を受けない |
listChanged(3箇所とも) |
false | 動的なリスト変更通知を出していない |
| ツール構成既に同型 | start / status / result / cancel | 新仕様の Tasks と同じジョブ方式 |
2本とも5ツールで、時間のかかる処理を *_start でジョブ化し、*_status で進捗を返し、*_result で取りに行く形にしてある。文字起こしが数分かかるので、接続を張り続けない設計を先に選んでいた。新仕様の Tasks は、この形を標準化したものになる。手元にある仕組みを仕様の名前へ置き換えるだけで足りそうだ。
改名で触る行は4行だった
ここまでで移行の影響が小さいことは見えたので、実際に書き換える行を数えた。2本のソースを FastMCP で grep する。
whisper_mcp/server.py:100 from mcp.server.fastmcp import FastMCP
whisper_mcp/server.py:103 mcp = FastMCP("whisper-mcp")
scraper_mcp/server.py:73 from mcp.server.fastmcp import FastMCP
scraper_mcp/server.py:76 mcp = FastMCP("scraper-mcp")
実コードは4行だった。ツール定義のデコレータはどちらも @mcp.tool() のままで、触る必要がない。ステートレス化で消える機能を一つも使っていなかったので、破壊の実体はこの4行に収まっている。
この結果はサーバの作りに依存する。購読やサンプリングを使っていれば話は変わる。capabilities を1回取れば、自分がどちら側かは数秒で分かる。移行計画を立てる前に、まずこれを取るのが早い。
ピン留めする版が、もう安定版になっていた
上で mcp==2.0.0b1 のピン留めを書いた。2026年7月31日に PyPI を見に行ったら、2.0.0 が安定版として出ていた。
2.0.0a1 → 2.0.0a2 → 2.0.0a3 → 2.0.0b1 → 2.0.0b2 → 2.0.0rc1 → 2.0.0
いま入れるなら 2.0.0b1 を指定する理由はない。未検証: 筆者の2本はまだ 1.28.1 のままで、2.0.0 へ上げて動かすところまでは確かめていない。上の4行を置換した後に何が出るかは、実際にやってから書く。
開発者としての実践ポイント
- 最終仕様の7月28日を待たず、いまBeta SDKで自作サーバを新仕様に通し、単発リクエストで疎通だけ先に取る。仕様確定前でも壊れる箇所は今の実装で洗い出せる。
- Pythonは
FastMCP→MCPServerの改名を起点に、import・クラス名・ツール定義の順で置換する。バージョンはmcp==2.0.0b1でピン留め。 - TypeScriptはSDK入れ替えを、ESM移行・Node 20以上・Standard Schema対応と一つの作業単位として計画する。CommonJS前提の設定は先に棚卸しする。
- クライアントのエラーハンドリングで、リソース未検出を
-32002決め打ちにしている箇所を-32602に直す。数値で分岐している場所を全部grepする。 - サーバを先にv2化し、レガシー
initialize応答で既存クライアントを生かしたまま、クライアントを段階移行する。実績のある旧経路をフォールバックとして残す。
仕様そのものの全体像は前回まとめた。MCP 2026-07-28ステートレス仕様の詳細(続報の元記事)を先に読むと、今回の移行手順が追いやすい。
移行前によく出る疑問
いま移行すべきか。最終仕様が7月28日から動く可能性は残るが、破壊箇所の洗い出しはBetaで先にやっておく価値がある。仕様確定を待つほど、まとめて直す作業が本番直前へ寄っていく。筆者なら、コードを書き換えるかは仕様確定後に決め、疎通と壊れ方の確認だけ今のBetaで済ませておく。
既存クライアントは壊れるのか。v2サーバがレガシー initialize に応答するかぎり、2025-11-25 仕様のクライアントはそのまま生き残る。壊れるのは、サーバとクライアントを同時に新仕様へ振り切ったとき。段階移行の順番を守れば、どこかの時点で必ず旧経路が残っている。
ESM専用化はどこに効くのか。TypeScript SDKだけの話で収まらない。require で読み込んでいたビルド設定、__dirname を前提にしたパス解決、ESM未対応のテストランナーまで巻き込む。Node 18で止めている環境は、SDK差し替えにランタイム更新が同時に乗ってくると見積もっておきたい。
まとめ
破壊的変更を本番前に確かめるなら、筆者は7月28日の最終仕様を待たず、Beta SDKで自作MCPサーバを新仕様に通し、単発で疎通確認してからフォールバックを用意する。
この順番を徹底しているのには理由がある。筆者は過去に、廃止予定のAPIを検証しないまま全経路へ入れて、切り替え当日に全滅させた。以後は「単発の疎通確認を先に取り、実績のある経路をフォールバックに残す」を固定の手順にしている。今回のステートレス化は運用側に効く良い変更だが、initialize廃止もTypeScriptのESM移行も、当日に初めて触ると事故る種類の変更だ。後方互換が段階移行を許してくれる今のうちに、壊れる箇所を一つずつ本番外で潰しておく。