はじめに
MCP(Model Context Protocol)の次期仕様「2026-07-28 Release Candidate」で、プロトコルからセッションの概念そのものが削除されることが決まった。Mcp-Session-Id ヘッダーも initialize ハンドシェイクも消える1。
「セッション廃止」と聞くと大改修が必要に思えるが、公式SEP(Standards Enhancement Proposal)には実は「1,000リポジトリを調査した結果、本当に作り直しが必要なサーバーは4%未満」という実測データが載っている。しかも、この変更が有効にする stateless_http モードは すでに現行の安定版Python SDKに実装済み だった。
本記事では、次期仕様を待たずに今すぐ試せるこの stateless_http モードを実際に動かし、「何が壊れて、何が壊れないのか」を手元のログで検証する。
この記事で分かること
- MCPが次期仕様でセッションを廃止する理由と、影響を受けるサーバーの実測割合
-
stateless_http=Trueを実際に動かして分かった「壊れる境界線」 - 自作MCPサーバーが移行対象かどうかのチェック方法
前提環境
- Python 3.11
-
mcp1.28.0(PyPI安定版。2026-07-08時点の最新は1.28.1) -
uvicorn/httpx(検証用)
TL;DR
- MCP次期仕様(2026年7月28日公開予定)は
Mcp-Session-Idとセッション概念をプロトコルから完全に削除する1 - 公式SEP-2567の1,000リポジトリ調査によると、セッションIDに何らかの言及があるサーバーは全体の10.0%だが、その大半(3.5%はSDKが自動除去・2.8%はコンストラクタ引数1行削除で対応可能)は無傷で移行でき、本当に設計し直しが必要なサーバーは2.5%+0.7%+0.5%=3.7%(4%未満)2
- 残り90.0%(アプリケーションコードがセッションIDを一切参照していないサーバー)は無変更で移行できる2
- この変更の実体である「セッションIDを発行しない」動作は、現行の安定版SDK(Python
mcp1.28.0、TypeScript SDKも同様)にstateless_http=Trueとしてすでに実装済み2 - 実際に動かすと、
stateless_http=False(現在のデフォルト)ではMcp-Session-Idを欠いたリクエストは400 Bad Request: Missing session IDで弾かれるが、stateless_http=Trueにすると同じリクエストがそのまま通る - ただし「グローバル変数で状態を持つ雑なツールは壊れない」という誤解には注意が必要。壊れるのはプロセスをまたいで水平スケールした瞬間
背景・課題
現行のMCP仕様(2025-11-25版)では、クライアントは initialize でハンドシェイクを行い、サーバーが発行した Mcp-Session-Id を以後すべてのリクエストに付け続ける必要がある。これはステートフルなロードバランシング(スティッキーセッション)を要求し、水平スケールを難しくしていた。
次期仕様のRelease Candidateはこの前提を覆す。公式ブログは次のように説明している。
The
initialize/initializedhandshake is removed ([SEP-2575]). TheMcp-Session-Idheader and the protocol-level session that came with it are also removed ([SEP-2567]).
— The 2026-07-28 MCP Specification Release Candidate(Model Context Protocol Blog)
正式仕様は 2026年7月28日 に公開予定で、SDK実装側には検証用の猶予期間(Tier 1 SDKへの対応が期待される期間)が設けられている1。
この変更を規定するSEP-2567(Status: Final)の実装状況について、仕様には次の一文がある。
All official SDKs except PHP already provide a stateless mode, implemented as not generating a session ID (e.g.
sessionIdGenerator: undefinedin the TypeScript SDK,stateless_http=Truein the Python SDK).
— SEP-2567: Sessionless MCP via Explicit State Handles
つまり「次期仕様で新しく登場する機能」ではなく、「すでに現行SDKにあるオプトイン設定を、次期仕様ではデフォルト(かつ唯一の選択肢)にする」というのが実態だ。それなら今の安定版で先取りして試せるはずだと考え、実際に動かしてみた。
やったこと
ステップ1: カウンターツールを持つ最小MCPサーバーを書く
stateless_http の起動フラグだけを変えられる最小サーバーを用意した。
# mcp_test_server.py
import sys
from mcp.server.fastmcp import FastMCP
STATELESS = sys.argv[1] == "stateless" if len(sys.argv) > 1 else False
PORT = int(sys.argv[2]) if len(sys.argv) > 2 else 8900
mcp = FastMCP("counter-demo", stateless_http=STATELESS, port=PORT)
_counter = {"n": 0}
@mcp.tool()
def increment() -> str:
"""Increment an in-memory counter and return the new value."""
_counter["n"] += 1
return f"counter={_counter['n']}"
if __name__ == "__main__":
mcp.run(transport="streamable-http")
pip install mcp==1.28.0 だけで動く。ベータ版や次期仕様専用のパッケージは一切不要だった。
ステップ2: 生のJSON-RPCでセッションIDの挙動を観測する
MCPクライアントSDK越しだとセッションIDの扱いが自動で吸収されてしまうため、httpx で initialize → tools/call を直接叩き、レスポンスヘッダーとステータスコードを見るクライアントを書いた(Mcp-Session-Id ヘッダーの有無を制御できるようにしてある)。
ステップ3: stateful(現在のデフォルト)で実行
python3 mcp_test_server.py stateful 8901 &
python3 mcp_test_client.py 8901
実際の出力(抜粋):
--- 1. initialize ---
status: 200
Mcp-Session-Id header: 4c5453116a4c4172a88b4c59dbf78329
--- 2. tools/call WITH session header ---
status: 200
body: {"result": {"content": [{"type": "text", "text": "counter=1"}], ...}}
--- 3. tools/call WITHOUT session header ---
status: 400
body: {"error":{"code":-32600,"message":"Bad Request: Missing session ID"}}
サーバー側ログにも Created new transport with session ID: ... が2回出ていた。ヘッダーなしのリクエストは新規セッション扱いになり、ハンドシェイクをやり直していないため即座に 400 で弾かれる。
ステップ4: stateless_http=True で実行
python3 mcp_test_server.py stateless 8902 &
python3 mcp_test_client.py 8902
実際の出力(抜粋):
--- 1. initialize ---
status: 200
Mcp-Session-Id header: None
--- 2. tools/call WITH session header (実際は空) ---
status: 200
body: {"result": {"content": [{"type": "text", "text": "counter=1"}], ...}}
--- 3. tools/call WITHOUT session header ---
status: 200
body: {"result": {"content": [{"type": "text", "text": "counter=2"}], ...}}
サーバーログは Terminating session: None を毎リクエスト出力する。initialize の時点で セッションIDが最初から発行されない。ヘッダーの有無に関わらず全リクエストが200で通り、カウンターも1→2と素直に増え続けた。
著者視点の発見ポイント
ここが一番の発見だった。「セッションを廃止したら、グローバル変数でカウンターを持つような雑なツールは壊れるはず」と予想していたが、実際には壊れなかった。1プロセス内で動かしている限り、_counter はただのPythonのモジュール変数であり、MCPのセッション概念とは無関係にプロセスの寿命だけに依存しているからだ。
つまり本当に壊れるのは、「セッションIDというプロトコル層の識別子そのものに依存しているコード」——例えばロードバランサーのスティッキールーティングや、リクエストヘッダーの Mcp-Session-Id を認可やロギングのキーに使っているコードだけである。SEP-2567の1,000リポジトリ調査でも、影響が大きいのは「セッションIDでルーティングするプロキシ・ゲートウェイ」(0.7%)と「認証情報をセッションIDに紐付けているサーバー」(0.5%)に限られており、単にプロセス内メモリで状態を持つだけのサーバーはそもそも対象外と明記されている2。
一方で、この「1プロセスなら壊れない」という結果に安心してはいけない。複数プロセス・複数レプリカで水平スケールした瞬間、プロセスごとに別々の _counter が存在することになり、どのリクエストがどのレプリカに届くかで結果が変わる。これは今回の検証環境(単一プロセス)では再現できない失敗モードであり、SEP-2567が推奨する「明示的ハンドル」パターン(create_basket() が返す basket_id を後続の呼び出しに引数として渡す)へ移行して初めて解決する問題だ。
自作MCPサーバーの移行チェックリスト
自分のMCPサーバー・クライアントコードに以下がないか確認する:
-
Mcp-Session-IdヘッダーやSDKのsession_idを、認可・ロギング・レート制限のキーとして直接参照していないか - ロードバランサー/ゲートウェイでセッションIDによるスティッキールーティングをしていないか
- OAuthのPKCE verifierやJWTクレームをセッションIDに紐付けていないか
- 複数プロセス/レプリカで動かす予定があるのに、状態をプロセス内グローバル変数だけで持っていないか(今は動いていても次期仕様移行時に顕在化する)
いずれも該当しなければ、SEP-2567の調査結果通り「無変更で移行できる90%」に入る可能性が高い。該当する場合は、状態を持つツールに create_*() → ID返却 → 後続ツールへID引き渡し、という明示的ハンドルパターンへの再設計を検討する。
まとめ
- MCP次期仕様(2026-07-28公開予定)はセッション概念を完全に削除する破壊的変更だが、実装の実体(
stateless_http=True)はすでに現行安定版SDKで試せる - 実際に動かすと、素朴なグローバル変数ベースの状態管理は単一プロセスなら壊れない。壊れるのはセッションIDそのものをキーに使っているコードだけ
- 公式調査ではセッションIDに何らかの言及があるサーバーは10.0%だが、うち6.3%はSDKの自動除去や1行削除で済み、本当に設計し直しが必要なのは3.7%(4%未満)。まずは自分のサーバーが該当するかをチェックリストで確認するのが移行の第一歩
- 水平スケール時の落とし穴(プロセスごとに状態が分裂する)は単一プロセス検証では見えないため、本番構成に近い形での検証を別途推奨する
参考リンク
- The 2026-07-28 MCP Specification Release Candidate — 次期仕様の公開日・変更点の一次情報
- SEP-2567: Sessionless MCP via Explicit State Handles — セッション廃止の提案本体。1,000リポジトリ調査・明示的ハンドルパターンの仕様
-
MCP Python SDK (PyPI) — 本記事の検証に使った
mcpパッケージ
-
The 2026-07-28 MCP Specification Release Candidate(Model Context Protocol Blog) ↩ ↩2 ↩3
-
SEP-2567: Sessionless MCP via Explicit State Handles(Status: Final) ↩ ↩2 ↩3 ↩4