Streamable HTTPは、MCP(Model Context Protocol)のクライアントとサーバーがHTTPでメッセージをやり取りするための通信方式(トランスポート)です。2025-03-26版の仕様で、それまでのHTTP+SSE方式の後継として導入されました。2026年7月28日に正式版となった最新仕様(2026-07-28版)では、セッションやGETストリームが廃止され、中身が大きく変わっています。本記事では、仕様書の原文と公式SDKを2026年10月23日時点で確認し、SSEとの違い、版ごとの変遷、SDK v2でサーバーを立てて測った挙動、運用で起きやすい失敗をまとめます。
まとめ:Streamable HTTPの要点(2026年10月時点)
- MCPの標準トランスポートはstdioとStreamable HTTPの2つ。リモートのMCPサーバーはStreamable HTTPで公開する。
- クライアントはメッセージを1件ずつ、1本のMCPエンドポイント(例:
https://example.com/mcp)へPOSTする。サーバーは1件のJSONで返すか、そのリクエスト専用のSSEストリームで返すかを選べる。 - 旧HTTP+SSE方式(2024-11-05版)は2本のエンドポイントと常時接続が必要だった。2026-07-28版では廃止候補(Deprecated)に分類され、猶予期間はすでに過ぎている。
- 2026-07-28版で
Mcp-Session-Id・GETストリーム・Last-Event-IDによる再開が廃止され、Mcp-Methodヘッダーがすべてのリクエストで、Mcp-Nameがツール呼び出し・リソース読み取り・プロンプト取得で必須になった。 - TypeScriptで2026-07-28版を使えるのは分割パッケージのv2(最新2.3.1)だけ。従来の
@modelcontextprotocol/sdk(最新1.32.1)は2025-11-25版までで、対応は予定されていない。 - TypeScript SDKの現行版では、ステートレスのトランスポートを使い回すと2本目のリクエストから、サーバーを使い回すと同時アクセスで500になる。どちらもリクエストごとに作るのが前提。
Streamable HTTPの役割:MCPで使う2つのトランスポート
MCPはJSON-RPC 2.0でメッセージを表現し、それを運ぶ層を「トランスポート」と呼びます。2026-07-28版の仕様が標準として定めるのは次の2つだけです。
| 項目 | stdio | Streamable HTTP |
|---|---|---|
| サーバーの動き方 | クライアントが子プロセスとして起動 | 独立したプロセスとして常駐 |
| 接続できるクライアント | 起動したクライアント1つ | 複数 |
| メッセージの区切り | 改行区切りのJSON | HTTP POST 1回に1件 |
| 主な用途 | ローカルのツール連携 | リモート公開・社内共有 |
| キャンセル | notifications/cancelled を送る |
応答ストリームを閉じる |
WebSocketやgRPCは標準トランスポートではありません。仕様は独自トランスポートを認めていますが、JSON-RPCの形式とメッセージの型を守ることが条件です。MCPそのものの仕組み(ツール・リソース・プロンプト)はMCP(Model Context Protocol)とは?AIと外部ツールをつなぐ標準規格の仕組みをわかりやすく解説で解説しています。
リクエストへの応答:JSONとSSEの選択
クライアントはPOSTのたびに Accept ヘッダーへ application/json と text/event-stream の両方を書く決まりです。サーバーはリクエストごとにJSONかSSEを選べます。途中経過を知らせたい処理ではSSEを選び、進捗通知(notifications/progress)を流したあと、最後に結果を送ってストリームを閉じます。途中の通知が要らなければ単一のJSONで返せます。どちらで返ってくるかはサーバー次第なので、クライアントは両方を受けられなければなりません。
通知(応答を求めないメッセージ)をPOSTした場合、サーバーは受理すれば本文なしの 202 Accepted を返します。
HTTP+SSE方式とStreamable HTTPの違い
「SSEとStreamable HTTPの違い」と言うとき、比べる相手はSSEという技術そのものではなく、2024-11-05版のMCP仕様にあった「HTTP+SSEトランスポート」です。
| 項目 | HTTP+SSE(2024-11-05版) | Streamable HTTP(2026-07-28版) |
|---|---|---|
| エンドポイント | SSE用とPOST用の2本 | MCPエンドポイント1本 |
| 接続の始め方 | GETでSSEを開き、POST先を受け取る | いきなりPOSTしてよい |
| 応答の返り道 | 常時接続のSSEストリーム | そのPOSTへのJSONか専用SSE |
| 常時接続 | 必須 | 不要(変更通知を受けるときだけ) |
| セッション | SSE接続とクライアントを対応付けて管理 | プロトコルのセッションなし |
| 仕様上の扱い | Deprecated(廃止候補) | 現行の標準 |
旧方式では応答が必ず長時間つなぎっぱなしのSSEを通って返るため、サーバーは接続とクライアントを結び付けて覚えておく必要がありました。接続情報を各サーバーのメモリに持つ構成では、スティッキーセッションでPOSTを対応するSSE接続へ届けるか、Redisなどのメッセージブローカーで応答を配送する必要があります。接続を長く保てないサーバーレス環境とも相性の悪い方式です。
「SSEが廃止された」は誤解
Streamable HTTPもSSEを使います。なくなったのは「接続全体で1本の常時SSE」であって、「1リクエストに1本のSSE」はむしろ標準の応答形式です。SSEの仕組み自体はSSE(Server-Sent Events)とは|仕組み・WebSocketとの違い・実装を解説、WebSocketとの比較はSSEとWebSocketの違いと使い分け|WebRTC・WebTransportまで性能比較【2026年10月】が詳しいです。
HTTP+SSEの削除時期:SEP-2596の猶予規定
2026-07-28版は、新しく定めた機能ライフサイクル方針(SEP-2596)でHTTP+SSEを「Deprecated」に分類し、新規の実装は採用すべきでない(SHOULD NOT)としました。通常の機能は最低12か月の猶予を置きますが、HTTP+SSEは以前から非推奨だったため、仕様の非推奨一覧では「SEP-2596がFinalになってから3か月後」が最短の削除時期です。SEP-2596は2026年6月3日のコミットでステータスがDraftからFinalに変わったので、3か月の猶予は9月上旬に過ぎています。
ただし削除は自動では起きません。SEP-2596の移行規定は、削除を次の版のリリース準備でコアメンテナーが決めると定めています。2026年10月23日時点で、次の版の草案(draft)の非推奨一覧にある「Removed」欄はまだ空です。次の版でHTTP+SSEが消えてもおかしくない状態と考え、旧方式で動いているサーバーは移行を急ぐべきです。
仕様バージョン別に見るStreamable HTTPの変遷
同じ「Streamable HTTP」でも、版によって中身が違います。2025-03-26版から2025-11-25版までを前提にした解説にはセッションID・GETストリーム・再開が登場しますが、いずれも2026-07-28版では廃止されています。解説を読むときは、対象の版を確かめてください。
| 仕様の版 | Streamable HTTPに関わる主な変更 |
|---|---|
| 2025-03-26 | HTTP+SSEに代えて導入。セッションID・GETストリーム・再開・バッチ送信を定義 |
| 2025-06-18 | JSON-RPCのバッチ送信を削除。MCP-Protocol-Version ヘッダーを必須化 |
| 2025-11-25 | 不正なOriginへの403応答を明記。サーバーが任意に切断するSSEポーリングを追加 |
| 2026-07-28 | セッション・GETストリーム・再開を廃止。Mcp-Method と Mcp-Name を必須化 |
2026-07-28版はGitHubで2026年5月29日にリリース候補(RC)、7月28日に正式版が公開されました。公開日は公式リポジトリのリリース一覧、通信の要件は2026-07-28版のStreamable HTTP仕様で確認できます。ハンドシェイクの廃止やMRTRなど、トランスポート以外も含めた版全体の変更点はMCP仕様2026-07-28版とは:ステートレス化の変更点と2025-11-25版からの移行手順にまとめています。
2026-07-28版で廃止された機能と追加された機能
2026-07-28版の変更は、MCPを「ステートレス(サーバーが接続ごとの状態を持たない)」なプロトコルへ作り替えるものです。初回の initialize ハンドシェイクもなくなり、各リクエストが本文の _meta に自分のプロトコルバージョンとクライアントの対応機能を載せて送られます。
Mcp-Session-IdとDELETEの廃止:ツール引数による状態の受け渡し
2025年版では、セッション管理を採用するサーバーが初期化時に Mcp-Session-Id ヘッダーでセッションIDを払い出すことができ、発行された場合はクライアントが以後のリクエストにそれを付けます。終了時のDELETE送信は推奨で、サーバーは405を返してクライアントからの終了を認めないこともできました。2026-07-28版ではこの仕組みが丸ごと削除されています。
複数回の呼び出しにまたがる状態が必要なサーバーは、サーバー側で発行したハンドル(識別子)を普通のツール引数として受け渡します。「カートを作成するツール」がカートIDを返し、「商品を追加するツール」がそのIDを引数に取る形です。状態の置き場所が、プロトコルからアプリケーションの設計へ移りました。
GETストリームの廃止とsubscriptions/listen
2025年版では、クライアントがGETで常時SSEを開き、ツール一覧の変更などサーバー発の通知を受け取れました。2026-07-28版ではGETが廃止され、代わりに subscriptions/listen というリクエストをPOSTし、その応答ストリームを開いたままにします。受け取る通知はクライアントが明示します。一覧の変更通知は toolsListChanged・promptsListChanged・resourcesListChanged をtrueにし、個別リソースの更新は resourceSubscriptions にURIを並べます。サーバーは要求されていない通知を送ってはいけません。
サーバーからクライアントへの問い合わせ(サンプリングや入力要求)も、SSEで別リクエストとして送る方式から、結果の中に「追加入力が必要」という形で埋め込み、クライアントが入力を添えて再送する方式(Multi Round-Trip Requests)に変わりました。
Last-Event-IDによる再開の廃止
2025年版では、イベントIDの付与と再送に対応するサーバーなら、切断後に Last-Event-ID ヘッダーでストリームの再開を要求できました。2026-07-28版ではこの再開がなくなり、応答ストリームが切れたら処理中のリクエストは失われます。2026-07-28版の変更履歴は、クライアントが新しいリクエストIDで出し直すこと(MUST)を定めています。一方でサーバーはストリームの切断をキャンセルとして扱うので、切断の時点でツールの処理がどこまで進んだかはクライアントから分かりません。決済やメール送信のように副作用のあるツールは、出し直しで二重に実行されても結果が変わらないよう、ツール側で冪等性を持たせておく必要があります。
必須になったMcp-MethodとMcp-Nameヘッダー
POSTには本文の内容をヘッダーへ写した Mcp-Method(メソッド名)が必須になり、tools/call・resources/read・prompts/get では Mcp-Name(ツール名やURI)も必須です。ロードバランサーやAPIゲートウェイ、WAFが本文のJSONを解析せずに振り分けや流量制限をかけられるようにするためです。
POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: add-note
{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"add-note","arguments":{"text":"hello"},
"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28",
"io.modelcontextprotocol/clientCapabilities":{}}}}
ヘッダーと本文が食い違う場合、サーバーは 400 Bad Request とエラーコード -32020(HeaderMismatch)で拒否しなければなりません。ゲートウェイがヘッダーを見て許可したのに、サーバーは本文の別のツールを実行する、というすり抜けを防ぐ規定です。ツールの引数を x-mcp-header 指定で Mcp-Param-名前 ヘッダーに写す仕組みも加わりました。
MCP-Protocol-Versionヘッダーによる新旧の判別
新旧のクライアントが同じURLに来るとき、サーバーが最初に見るのは MCP-Protocol-Version ヘッダーです。2026-07-28版はすべてのPOSTにこのヘッダーを求め、値は本文 _meta の版と一致しなければなりません。ヘッダーを定めていなかった2025-06-18版より前のクライアントも受けるサーバーは、ヘッダーのないリクエストを2025-03-26版として扱ってよい(MAY)とされています。
後述のSDK v2サーバーに server/discover をこのヘッダーなしで送ると、旧方式の経路に回されて -32601(Method not found)が返りました。ゲートウェイでヘッダーを削るとこの判別が狂うので、MCP-Protocol-Version と Mcp- で始まるヘッダーは素通しにします。
逆向き、つまり新しいクライアントが旧サーバーにつなぐ場合、仕様はまず新方式で送り、400の本文を見て判断する手順を定めています。本文が新方式のエラーなら相手は新方式のサーバーなので、示された対応版で送り直すか、ヘッダーの不一致などリクエスト側の誤りを直します。本文が空か新方式のエラーでなければ initialize へ戻ります。v1(1.32.1)のステートレスなサーバーに2026-07-28版のリクエストを送ると、400と「Unsupported protocol version: 2026-07-28」(-32000)が返りました。新方式のエラーではないので、クライアントは旧方式へ戻ります。
TypeScript SDK v2によるStreamable HTTPサーバーの実装
公式TypeScript SDKは2026年7月27日に分割パッケージのv2(@modelcontextprotocol/server・client・node ほか)を公開し、10月5日の2.3.1が最新です。従来の一体型パッケージ @modelcontextprotocol/sdk も1.32.1まで更新されていますが、v1.xのREADMEは2026-07-28版への対応を「予定していない」と明記し、v2公開から最低6か月はバグ修正とセキュリティ更新を続けるとしています。2026-07-28版を使うならv2が前提です。v1からのコードの書き換えはMCP TypeScript SDK v2とは?パッケージ分割とステートレス化の移行要点で扱っています。
以下はNode.js v26.5.0とSDK v2.3.1で動かした最小構成です。サーバーコードを server.mjs に保存し、依存パッケージをインストールしてから node server.mjs で起動します。add-note は通信確認用の疑似実装で、保存済みを示す文字列を返すだけです。
npm install @modelcontextprotocol/[email protected] @modelcontextprotocol/[email protected] @modelcontextprotocol/[email protected] [email protected]
import { createServer } from 'node:http';
import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';
import { toNodeHandler, localhostHostValidation, localhostOriginValidation } from '@modelcontextprotocol/node';
import * as z from 'zod/v4';
const handler = createMcpHandler(({ era }) => {
// リクエストごとに呼ばれ、毎回新しいサーバーを返す
const server = new McpServer({ name: 'notes', version: '1.0.0' });
server.registerTool(
'add-note',
{ description: 'メモを保存する', inputSchema: z.object({ text: z.string() }) },
async ({ text }) => ({ content: [{ type: 'text', text: `saved: ${text} (${era})` }] })
);
return server;
});
const nodeHandler = toNodeHandler(handler);
const validateHost = localhostHostValidation();
const validateOrigin = localhostOriginValidation();
createServer((req, res) => {
if (!validateHost(req, res) || !validateOrigin(req, res)) return;
void nodeHandler(req, res);
}).listen(3000, '127.0.0.1');
createMcpHandler に渡す関数は、HTTPリクエストが来るたびにサーバーのインスタンスを作ります。インスタンスに状態を持たせないので、そのまま複数台に並べてもスティッキーセッションは要りません。データベース接続のように使い回すものは、関数の外で1回だけ作ります。マウント方法の詳細はSDK公式のServe over HTTPにまとまっています。
curlで確かめたHTTPステータスとエラー応答
このサーバーにcurlでリクエストを送りました(2026年10月23日実測。Node.js v26.5.0、server・client 2.3.1、node 2.1.1)。Mcp-Name を本文と違う名前にしたときのコマンドと応答は次のとおりです(応答はヘッダーの大半と本文の一部を省略した抜粋)。
curl -s -i -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: other' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"add-note","arguments":{"text":"hello"},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'
HTTP/1.1 400 Bad Request
{"jsonrpc":"2.0","error":{"code":-32020,"message":"Bad Request: the request headers and body disagree: the body carries params.name=\"add-note\" but the Mcp-Name header names \"other\"", ...},"id":1}
ほかの条件の結果は次の表のとおりです。
| 送ったもの | HTTPステータス | 返ってきた内容 |
|---|---|---|
| 正しいヘッダーでtools/call | 200 | JSON。resultType: complete 付き |
| Mcp-Nameを別名にしたtools/call | 400 | -32020(ヘッダーと本文の不一致) |
| Mcp-Methodなしのtools/list | 400 | -32020(必須ヘッダーの欠落) |
| 未知の版 2027-01-01 を指定 | 400 | -32022。対応版 2026-07-28 を列挙 |
| 版ヘッダー付きのserver/discover | 200 | 対応版と機能の一覧 |
| GET・DELETE | 405 | Method Not Allowed |
| 不正なOrigin付き | 403 | Invalid Origin |
| 2025-06-18版のinitialize | 200 | SSEで初期化結果。セッションIDなし |
最後の行が示すとおり、createMcpHandler は既定(legacy: 'stateless')で2025年仕様のクライアントにも応答します。ただしセッションは発行しないので、2025年版のGETストリームやセッション終了のDELETEは405になります。2026-07-28版だけを受けたい場合は legacy: 'reject' を指定します。
SDK v2クライアントの既定値:2025年版の初期化方式
見落としやすいのがクライアント側の既定値です。v2の Client はオプションなしだと2025年版の initialize で接続し、新方式を試しません。2.3.1でも変わっておらず、上のサーバーにつないで getProtocolEra() を見ると、オプションなしでは legacy、versionNegotiation: { mode: 'auto' } を付けると modern になりました。
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
const client = new Client(
{ name: 'my-client', version: '1.0.0' },
{ versionNegotiation: { mode: 'auto' } }
);
await client.connect(new StreamableHTTPClientTransport(new URL('http://127.0.0.1:3000/mcp')));
console.log(client.getProtocolEra()); // modern
auto は最初に server/discover で相手の対応版を問い合わせ、2025年版しか話さないサーバーなら initialize へ戻ります。mode: { pin: '2026-07-28' } で版を固定すると、2025年版しか話さないサーバーには接続できません。
Streamable HTTPサーバーで起きやすい失敗:インスタンスの使い回しと巨大リクエスト
ステートレスなStreamable HTTPサーバーでは、リクエストごとにサーバーとトランスポートを作るのが前提です。この前提を外した書き方が2025年の解説記事に多く残っており、現行のSDKでは動かなくなっています。
サーバーとトランスポートの共有:CVE-2026-25536とCVE-2026-111770
2025年に広まったステートレス実装の典型は、サーバー起動時にトランスポートを1つだけ作って connect() し、すべてのPOSTをそれで処理する書き方です。
// 現行SDKでは2本目のリクエストから失敗する書き方
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
await server.connect(transport);
app.post('/mcp', (req, res) => transport.handleRequest(req, res, req.body));
この形では、複数のクライアントがほぼ同時に送ると応答が別のクライアントへ届くことがありました。SDKのクライアントはリクエストIDを0から振るので、IDが衝突するためです。v1ではGHSA-345p-7cg4-v4c7(CVE-2026-25536)として1.26.0で、v2ではGHSA-jm63-cq47-j5jp(CVE-2026-111770)として2.3.0で修正されています。修正の方法は「共有を拒否する」ことなので、アップデートするとこの書き方は失敗するようになります。共有する4通りの書き方と、比較用の正しい書き方を手元で試した結果が次の表です。
| 書き方 | SDK | 1本ずつ3本 | 同時に送った結果 |
|---|---|---|---|
| トランスポート1つを共有 | v1 1.32.1 | 2本目から500 | 未計測 |
| サーバー1つを共有・トランスポートは毎回 | v1 1.32.1 | すべて200 | 5本中4本が500 |
| トランスポート1つを共有 | v2 2.3.1 | 2本目から500 | 5本中4本が500 |
| ファクトリが同じサーバーを返す | v2 2.3.1 | すべて200 | 5本中4本が500 |
| ファクトリが毎回新しいサーバーを返す | v2 2.3.1 | すべて200 | 新旧10本すべて200 |
v1でサーバーを共有した場合のエラーは「Already connected to a transport」でした。厄介なのは2行目と4行目で、開発中に1本ずつ試す限りは成功し、本番で同時アクセスが来て初めて500が出ます。最終行は新旧のクライアントを5本ずつ、すべて同じリクエストID 1で同時に送ったもので、各リクエストに自分の応答が返りました。リクエストごとにサーバーを作るコストは、2.3.0でツール定義の変換を毎回しなくなったため小さくなっています。
リクエスト本文とツール引数の上限
もう1つの変更が受信サイズの制限です。GHSA-f897-62hf-6gxqにより、v1は1.30.1、v2はserver・node 2.1.0から、4MiBを超える本文に413、100件を超えるJSON-RPCバッチに400を返すようになりました。上のサーバーに5MiBの引数を送ると、413と「Request body must not exceed 4194304 bytes」が返りました。大きなファイルを引数で受けるツールは、オプション maxRequestBodySize で上限を引き上げます。このとき createMcpHandler と toNodeHandler の両方に設定が必要です。上のサンプルで createMcpHandler だけを8MiBにしても5MiBの本文は413のままで、toNodeHandler にも同じ値を渡すと200になりました。Nodeアダプターの上限が先に効くためです。Python SDKも2.3.0で同じ4MiBを既定とし、超えた本文には解析前に413を返します。
4MiB以内でも、要素数の多い配列やオブジェクトを検証させるとメモリを使い切る問題(GHSA-46jr-4wx8-v453)が別にあります。こちらはv1 1.32.0・v2 2.3.0で McpServer のオプション maxToolInputElements が追加されましたが、既定はオフです。アドバイザリ自身が「アップデートだけでは何も変わらない」と書いており、対策を2つ挙げています。1つは、必要な最大要素数より少し上の値を maxToolInputElements に明示すること。もう1つは、本文の上限をツールが実際に必要とする大きさまで下げることで、トランスポートか createMcpHandler の maxRequestBodySize で設定します。express.json() などで本文を自前で解析している場合は、そのパーサー側の上限を設定します。
SDKとクライアントごとのStreamable HTTPの指定方法
設定ファイルやコマンドでは、Streamable HTTPを streamable-http や http と書くのが一般的です。2026年10月23日時点の公式ドキュメントでの書き方を並べます。
| SDK・クライアント | 指定方法 | 補足 |
|---|---|---|
| Python SDK(mcp 2.x) | mcp.run(transport="streamable-http") |
既定は127.0.0.1:8000の /mcp |
| Claude Code | claude mcp add --transport http 名前 URL |
v2.1.265以降はSSEへ自動切替 |
| VS Code(mcp.json) | "type": "http" |
HTTPを試し、だめならSSE |
| TypeScript SDK v2 | createMcpHandler |
クライアントは StreamableHTTPClientTransport |
Claude CodeのMCPのドキュメントはSSEトランスポートを非推奨と明記したうえで、SSEしか持たないサービスも --transport http で登録すればよいとしています。HTTPで接続できなければSSEへ切り替える動作で、v2.1.265以降が必要です。VS CodeのMCP設定リファレンスも同様に、"type": "http" ならStreamable HTTPを先に試してSSEへ戻ります。サーバーが同じURLのまま新方式に移る場合は、クライアントの設定を sse から http に書き換えておけば移行後は新方式で接続します。移行でURLが変わる場合(例: /sse から /mcp)は、登録したURLも書き換えが必要です。
Python SDKは2026年7月28日公開のv2.0.0で2026-07-28版に対応し、同じサーバーで2025年版のクライアントにも応答します(PyPIの最新は10月2日公開の2.3.0)。旧クライアントへの対応を説明した公式ドキュメントによると、2025年版のクライアントには既定で initialize とセッションを使って応答し、2026-07-28版の接続は設定にかかわらずセッションなしになります。v2では高水準クラスの名前が FastMCP から MCPServer に変わりました。Pythonでサーバーを作る手順はMCPサーバーの作り方とAIエージェントへの接続手順|Python SDK 2.3でツール定義・認可を実装、FastMCPはFastMCPとは?PythonでMCPサーバーを作る手順とFastMCP 4.1の使い方【2026年10月】で扱っています。
2025年版から移行するときの判断基準
2026年10月時点でも、サーバーを2026-07-28版だけに絞る(legacy: 'reject')のは早すぎます。公式SDKのクライアントですら既定が旧方式で、Claude CodeやVS Codeの利用者が新方式で接続してくる保証もないからです。新旧の両方を受ける構成を基本にし、アクセスログで MCP-Protocol-Version の値を集計して、旧方式の割合が十分下がってから絞るのが安全です。
- セッションに状態を持たせているサーバー:状態をツール引数のハンドルへ移す設計変更が先に必要です。すぐに移せない場合、SDKの
isLegacyRequestで旧方式のリクエストだけを既存の実装へ振り分ける構成が公式ドキュメントに載っています。 - ロードバランサー:新方式のリクエストはスティッキーセッションなしで振り分けられます。旧方式のセッション付き通信が残る間は、その経路だけ現状の設定を維持します。
- リバースプロキシ:SSEの応答がnginxなどで溜め込まれると、進捗通知がまとめて届きます。仕様はSSE応答に
X-Accel-Buffering: noを付けるよう推奨しており、v2のサーバーは実測でもこのヘッダーを付けていました。 - 再送に弱いツール:ストリームが切れたら続きから受け取れないため、上で触れた冪等性の確保が移行の前提になります。
- v1のまま運用するサーバー:v1.xへの修正はv2公開(2026年7月27日)から最低6か月の約束なので、2027年1月下旬以降は保証がありません。それまでにv2へ移す計画を立てます。
社内データや業務システムをMCPサーバーとして生成AIにつなぐ構成を、移行も含めて設計から相談したい場合は生成AI導入支援で承っています。Cloudflare Workersへの配置はCloudflareでMCPサーバーを構築・公開する方法|Workersリモートデプロイと認証・ポータルで扱っています。
Streamable HTTPサーバーのセキュリティ要件
仕様はStreamable HTTPのサーバーに次の3点を求めています。
- すべての接続で Origin ヘッダーを検証し、不正なら 403 Forbidden を返す(MUST)。
- ローカルで動かすときは0.0.0.0でなく127.0.0.1だけで待ち受ける(SHOULD)。
- すべての接続に適切な認証をかける(SHOULD)。
前の2つはDNSリバインディング攻撃への対策です。悪意あるWebサイトが自分のドメイン名を127.0.0.1に解決させると、ブラウザがローカルのMCPサーバーを同一オリジンとして扱い、閲覧者のPC上のツールを呼べてしまいます。
SDK v2の createMcpHandler 自体はHostもOriginもトークンも検証しません。上のサンプルで localhostHostValidation と localhostOriginValidation を前段に置いているのはそのためです。Expressで使う場合は createMcpExpressApp がlocalhostで待ち受けるときにこの検証を既定で有効にしますが、0.0.0.0など外部向けに待ち受けると既定の検証が外れるため、許可するホスト名を自分で指定します。
リモートサーバーの認証はOAuthが基本です。2.3.0以降の requireBearerAuth には、自サーバー向けに発行されたトークンだけを受け付ける expectedResource が加わりました(既定はオフ)。2026-07-28版ではクライアント登録の方式として動的クライアント登録(DCR)が非推奨になり、Client ID Metadata Documents(CIMD)が推奨されています。詳しくはCIMD(Client ID Metadata Documents)とは?MCPでDCRに代わるOAuthクライアント登録の仕組みと実装【2026年10月】を参照してください。
よくある質問
Streamable HTTPとSSEはどちらを使うべきですか?
新しく作るならStreamable HTTPです。旧HTTP+SSE方式は2025-03-26版から非推奨で、2026-07-28版では廃止候補に分類され、削除までの猶予もすでに過ぎています。TypeScript SDK v2のサーバー本体はHTTP+SSE方式を提供せず、移行できない環境向けにv1から凍結コピーした別パッケージ(@modelcontextprotocol/server-legacy/sse)が残るだけです。
MCPのStreamable HTTPとWebSocketは何が違いますか?
Streamable HTTPは普通のHTTP POSTと、応答用のSSEだけで成り立ちます。WebSocketのような接続の切り替え(アップグレード)を使わないので、HTTPを通すプロキシやAPIゲートウェイの後ろに置けます。WebSocketはMCPの標準トランスポートではなく、使う場合は独自トランスポートの扱いになります。
エンドポイントのURLは /mcp でなければいけませんか?
いいえ。仕様が求めるのはPOSTを受ける単一のパスを用意することだけで、https://example.com/mcp は例として挙げられているものです。多くのSDKやクライアントの例が /mcp を使っているため、特に理由がなければ合わせておくと設定の食い違いが減ります。
2025年版のStreamable HTTPで作ったサーバーはもう使えませんか?
使えます。2026-07-28版の仕様は、新しいクライアントが旧版のサーバーを見分けて initialize へ戻る手順を定めています。TypeScript SDK v2のクライアントも mode: 'auto' なら旧サーバーに自動で合わせます。ただし新しい版だけに固定したクライアントからは接続できません。
セッションがなくなると、会話の文脈はどう保持しますか?
プロトコルは状態を持たないので、必要な状態はサーバーが発行するIDをツールの引数でやり取りして管理します。LLMとの会話履歴そのものはクライアント(ホストアプリ)側が持つもので、MCPのセッション廃止の影響は受けません。