トランスポート - stdioとStreamable HTTP
MCP のトランスポート層 (stdio と Streamable HTTP) の仕様と、実運用で気をつける点を解説します。メッセージの中身 (_meta、resultType など) は MCPプロトコル基礎 で扱い、ここでは、そのメッセージを運ぶ層に絞ります。2026-07-28 での変更の一覧と移行の進め方は MCP最新仕様 2026-07-28 の変更点と移行、ローカルサーバーの安全な運用は ローカルMCPサーバーの安全な運用 を参照してください。
1. このドキュメントで扱うこと
- stdio トランスポート
- Streamable HTTP トランスポート
- 必須の HTTP ヘッダー
- ゲートウェイ / WAF との連携
- 購読 (subscriptions/listen)
- ストリーム切断時の挙動
- ステートレスな水平スケール
- HTTP+SSE からの移行
- ブラウザーからの接続
トランスポートは「束縛 (binding)」
プロトコルの意味は、どのトランスポートでも同じです。トランスポートは、メッセージの区切り方と届け方、リクエストのメタデータの運び方、キャンセルと終了の伝え方を定める「束縛」であり、メッセージの意味は定めません。標準のトランスポートは次の 2 つです。
- stdio: クライアントが起動するサブプロセスの標準ストリームで、改行区切りのメッセージをやり取りする
- Streamable HTTP: すべてのメッセージを、単一の MCP エンドポイントへの HTTP POST で送り、応答は JSON オブジェクトか、リクエスト専用の SSE ストリームで受ける
どちらでも、メッセージの向きは同じです。クライアントのリクエストと通知がサーバーへ、サーバーのレスポンスと通知がクライアントへ流れます。サーバーが JSON-RPC リクエストを送ることも、クライアントが JSON-RPC レスポンスを送ることもありません。独自のトランスポートを作ることもできますが、JSON-RPC のメッセージ形式、メッセージパターン、リクエストごとのメタデータの方式を保つ必要があります。信頼できる双方向のバイトストリーム (Unix ドメインソケットや TCP) の上に作るなら、stdio と同じ区切り方を使うことが推奨されます。
stdio トランスポート
stdio では、クライアントが MCP サーバーをサブプロセスとして起動します。
- サーバーは
stdinから JSON-RPC メッセージを読み、stdoutに書く - 1 メッセージは 1 行で、メッセージの中に改行を含めてはいけない
- サーバーは
stdoutに、有効な MCP メッセージ以外を書いてはいけない。ログはstderrに UTF-8 の文字列で出してよい - クライアントは
stderrを、取り込んでも、転送しても、無視してもよく、stderrの出力をエラーの表れと見なしてはいけない - クライアントは
stdinに、有効な MCP メッセージ以外を書いてはいけない
標準ストリームの上には、リクエストごとのストリームはなく、すべてのメッセージが 1 つのチャネルを共有します。サーバーが stdout に書くのは、次の 3 種類です。
- クライアントのリクエストへのレスポンス (JSON-RPC の
idで対応づける) - 処理中のリクエストに関係する通知 (
notifications/progressやnotifications/message) - 有効な
subscriptions/listenに対して配信する通知。クライアントは、_metaのio.modelcontextprotocol/subscriptionIdで購読と対応づける
リクエストのメタデータは、すべてメッセージの本文 (_meta) に入ります。ヘッダーの層はありません。
キャンセルは、クライアントが notifications/cancelled に、リクエストの ID を入れて送ります。サーバーは、できるだけ早く作業をやめ、そのリクエストについて、これ以上メッセージを送ってはいけません。
終了は、クライアントが、(1) サーバーへの入力ストリームを閉じ、(2) サーバーの終了を待ち、(3) 妥当な時間で終わらなければ、OS に応じた方法で強制終了します。POSIX では、通常 SIGTERM から SIGKILL へ段階を上げます。サーバーは、標準入力が閉じられた (EOF を読んだ) ら、速やかに終了することが推奨されます。これが、唯一の移植性のある、終了の合図です。
サーバーのプロセスが予期せず終了したら、クライアントは再起動することが推奨されます。プロトコルがステートレスなので、処理中のリクエストは失われるだけで、新しいプロセスに再試行できます。有効だった subscriptions/listen は、再起動後に、開き直す必要があります。
stdio の認証は、OAuth の仕様には従わず、環境から認証情報を取得します。stdio のサーバーは、起動した利用者の権限で動く、通常の OS のプロセスです。その脅威モデルは ローカルMCPサーバーの安全な運用 で扱います。
Streamable HTTP トランスポート
Streamable HTTP では、サーバーは、複数のクライアントの接続を扱える独立したプロセスです。
- サーバーは、POST を受け付ける単一の HTTP エンドポイント (MCP エンドポイント。例:
https://example.com/mcp) を提供しなければならない - クライアントは、JSON-RPC のリクエストと通知を、それぞれ別の HTTP POST で送る
- サーバーは、各リクエストに、単一の JSON オブジェクト (
Content-Type: application/json) か、そのリクエスト専用の SSE ストリーム (Content-Type: text/event-stream) で応答する - サーバー起点の要求 (Sampling、Elicitation、Roots) は、結果の中の入力要求として運ぶ (MRTR)
- 長く開く変更通知は、
subscriptions/listenの応答ストリームで配信する
クライアントの規則は次のとおりです。
- すべての JSON-RPC メッセージを、新しい HTTP POST で送る
Acceptヘッダーに、application/jsonとtext/event-streamの両方を入れる。応答は、どちらの形も扱えなければならない- POST の本文は、1 つの JSON-RPC リクエストか通知で、レスポンスは送らない
- 通知をサーバーが受け付けたら
202 Accepted(本文なし)、受け付けられなければ HTTP のエラー (たとえば400)
SSE ストリームで返す場合、サーバーは、最終のレスポンスの前に、元のリクエストに関係する通知 (進捗など) を送れます。そのストリームで、独立した JSON-RPC リクエストを送ってはいけません。最終のレスポンスが、ストリームを終えることが推奨されます。
キャンセルは、Streamable HTTP では、SSE の応答ストリームを閉じることです。リクエストごとにストリームがあるため、切断が、どのリクエストの取り消しかは明確です。サーバーは、切断を、そのリクエストのキャンセルとして扱い、できるだけ早く作業をやめ、それ以上メッセージを送ってはいけません。この版のコアのプロトコルでは、HTTP 上に notifications/cancelled は期待されません。
セキュリティの要件は、次の 3 つです。
- サーバーは、すべての接続で
Originヘッダーを検証しなければならない (DNS rebinding の対策)。Originがあって無効なら、403 Forbiddenを返す - ローカルで動かすサーバーは、すべてのインターフェース (0.0.0.0) ではなく、localhost (127.0.0.1) だけで待ち受けることが推奨される
- サーバーは、すべての接続に、適切な認証を実装することが推奨される。HTTP の認可は MCPの認可 - OAuth 2.1とメタデータ探索 を参照
必須の HTTP ヘッダー
Streamable HTTP は、本文の一部のフィールドを HTTP ヘッダーに複製します。ロードバランサー、ゲートウェイ、可観測性のツールが、本文を解析せずにリクエストをルーティングし、検査できるようにするためです。正は本文で、ヘッダーは複製です。
| ヘッダー | 元になる項目 | 必須となるリクエスト |
|---|---|---|
MCP-Protocol-Version | _meta の io.modelcontextprotocol/protocolVersion | すべての POST |
Mcp-Method | method | すべてのリクエスト |
Mcp-Name | params.name または params.uri | tools/call、resources/read、prompts/get |
Mcp-Param-{Name} | ツール引数 (x-mcp-header で指定したもの) | 指定されたパラメーターに値があるとき |
ヘッダーの名前は大文字小文字を区別しません。値 (メソッド名など) は区別します。
MCP-Protocol-Version の値は、本文の _meta の値と一致しなければなりません。一致しなければ、サーバーは 400 Bad Request と HeaderMismatch で拒否します。サーバーが、要求されたバージョンに対応しない場合も 400 Bad Request で、UnsupportedProtocolVersionError に対応するバージョンを列挙します。サーバーが、要求された RPC メソッドを実装していない場合は、404 Not Found と、JSON-RPC のエラーコード -32601 (Method not found) を返します。この本文が、モダンな MCP のエンドポイントを持たない旧 HTTP+SSE サーバーの 404 と、区別するための手がかりです。
Mcp-Name の値が、安全な ASCII の文字列で表せない場合 (非 ASCII、制御文字、前後の空白など) は、Base64 の形式 =?base64?{Base64 の値}?= で符号化します。
x-mcp-header は、ツールの inputSchema のプロパティに付ける拡張です。値が Region なら、そのパラメーターの値が Mcp-Param-Region ヘッダーにも入ります。サーバーが使うかどうかは任意ですが、クライアントはこの機能に対応しなければなりません。制約は次のとおりです。
- 値は空でなく、HTTP のフィールド名のトークンとして有効で、制御文字を含まず、スキーマの中で大文字小文字を区別せず一意
- プリミティブ型 (整数、文字列、真偽値) のプロパティにだけ付けられる。
number型は不可。整数は JavaScript の安全な範囲 - スキーマのルートから、
propertiesのキーだけをたどって到達できるプロパティにだけ付けられる。items、oneOf/anyOf/allOf、if/then/else、$refを経由するものは不可 - 制約に違反するツール定義は、HTTP のクライアントが、
tools/listの結果から除外し、警告をログに出す。1 つの不正な定義が、他のツールを使えなくしないため
値の符号化は、文字列はそのまま、整数は 10 進、真偽値は小文字の true / false です。非 ASCII や制御文字を含む、前後に空白がある、あるいは =?base64? で始まり ?= で終わる値は、UTF-8 の Base64 で符号化します。値が null や未指定なら、ヘッダーは省略します。
サーバーの検証: 本文を処理するサーバーは、ヘッダーの値 (必要なら復号したもの) が、本文の値と一致することを検証しなければなりません。不一致、必須ヘッダーの欠落、不正な文字、復号できない Base64 は、400 Bad Request と JSON-RPC のエラー -32020 (HeaderMismatch) で拒否します。ロードバランサーがヘッダーで経路を決め、サーバーが本文で実行する、という「真実の源が食い違う」状況を、悪用させないための要件です。クライアントが、Mcp-Param-* の欠落で拒否された場合は、tools/list で inputSchema の変更を確認し、正しいヘッダーで再試行することが推奨されます。
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32020,
"message": "Header mismatch: Mcp-Name header value 'foo' does not match body value 'bar'"
}
}
ゲートウェイ / WAF との連携
Mcp-Method と Mcp-Name があるため、JSON の本文を解析せずに、メソッドやツール単位で制御できます。
- ルーティング:
Mcp-Methodがtools/callのものだけを、ツール実行用のバックエンドへ送る - レート制限:
Mcp-Name(ツール名) ごとに上限を設ける。重いツールだけ厳しくする - 可観測性: メソッドとツール名を、アクセスログやメトリクスのラベルにする
- 認可の補助: 書き込み系のツールを、特定のネットワークからだけ許可する
- 地域やテナントによる振り分け:
x-mcp-headerで指定したMcp-Param-RegionやMcp-Param-TenantIdで、本文を解析せずに、ルーティングする
ただし、ヘッダーの値は、ツールの引数に由来します。LLM や悪意あるクライアントに影響される値なので、セキュリティ上の判断の唯一の根拠にしてはいけません。
- テナント ID やリージョンのように、特定のリソースへのアクセスを意味する値は、認証されたユーザーの権限と、別に照合する
- ヘッダーだけを根拠に、特権を与えたり、レート制限や割り当てを省いたりしない
- パスワード、API キー、トークン、個人情報を、
x-mcp-headerで指定しない。ヘッダーは、ロードバランサーやログなど、中継装置から見える。Base64 は暗号化ではない - ヘッダーの値で方針を決める中継装置は、
MCP-Protocol-Versionが、ヘッダーと本文の検証を要求する版を示すことを確認する。古い版や、ヘッダーがない場合は、検証されていない値を信用せず、拒否することが推奨される - 過大なヘッダーや、多すぎるヘッダーは、Base64 の復号や本文の解析の前に、早い段階で拒否する。ヘッダーサイズの上限は仕様にはなく、環境に応じて運用側が決める
購読 (subscriptions/listen)
subscriptions/listen は、サーバーからクライアントへの通知を受ける、長く開いたストリームです。HTTP の GET エンドポイントと、resources/subscribe / resources/unsubscribe を置き換えます。
クライアントは、受け取りたい通知の種類を notifications で指定します。サーバーは、要求されていない種類を送ってはいけません。
| フィールド | 型 | 受け取る通知 |
|---|---|---|
toolsListChanged | boolean | notifications/tools/list_changed |
promptsListChanged | boolean | notifications/prompts/list_changed |
resourcesListChanged | boolean | notifications/resources/list_changed |
resourceSubscriptions | string[] | 指定した URI の notifications/resources/updated |
すべて任意で、省略は、その種類を購読しないことと同じです。
- 確認応答: サーバーは、最初のメッセージとして、
notifications/subscriptions/acknowledgedを送らなければならず、それより前に通知を送ってはいけない。確認応答のnotificationsは、サーバーが実際に対応する部分で、対応しない種類は省かれる。クライアントは、要求と照合し、対応しない種類を穏当に扱うことが推奨される - 識別: すべての通知の
_metaにio.modelcontextprotocol/subscriptionIdが入る。値は、subscriptions/listenリクエストの JSON-RPC の ID。クライアントは、複数の購読を同時に持て、この値で振り分ける。stdio では、全メッセージが 1 つのチャネルを共有するため、必須 - 範囲:
notifications/progressとnotifications/messageのような、リクエストに属する通知は、このストリームではなく、そのリクエストの応答ストリームで届く - 終了: クライアントは、HTTP では SSE ストリームを閉じ、stdio では、
subscriptions/listenの ID を指定したnotifications/cancelledで終える。サーバーが自分から終えるときは、元のリクエストへの成功のレスポンス (resultType: "complete") を返してから閉じることが推奨される。このレスポンスがなく、ストリームが閉じたら、予期しない切断である - 保活: 長いストリームでは、サーバーが SSE のコメント行 (コロンで始まる行) を定期的に送ることが推奨される。中継装置や、クライアントのアイドルのタイムアウトで切られるのを避けるため。クライアントは、コメント行を無視する
通知は、最善努力の配信で、再接続をまたぐと欠落し得ます。クライアントは、ttlMs による再取得と併用します。
ストリーム切断時の挙動
SSE ストリームの再開 (Last-Event-ID) と、メッセージの再送は、廃止されました。ストリームが切れたら、処理中のリクエストは失われます。
- クライアントは、リクエストを新しいリクエスト ID で再発行しなければならない
- サーバーは、2025-11-25 以前のクライアントが送る
Last-Event-IDを無視する。ストリームは再開できない - ツールは、再実行されても安全 (冪等) になるように設計する。長い処理は、処理を進めたまま、結果を、明示的なハンドルや Tasks 拡張で後から取得できるようにする
subscriptions/listenは、切れたらクライアントが開き直す。サーバーは、再接続をまたいで購読の状態を持たない。再接続のあいだに起きた変更は、*/listの再取得で補う- stdio では、プロセスが終了したら、クライアントがプロセスを再起動し、リクエストを再試行し、購読を開き直す
ステートレスな水平スケール
セッションがないため、リクエストを同じサーバーに戻す必要がありません。
- スティッキーセッション (セッション ID やクッキーによる固定) は不要。任意のロードバランサーの背後に、同じサーバーを並べられる
- どのインスタンスも、
_metaとヘッダーだけでリクエストを処理できる - インスタンス間で共有すべき状態は、ツールが発行する、明示的なハンドルと、共有のストレージ (データベースなど) で扱う
- 複数のリクエストにまたがる処理の状態は、MRTR の
requestStateや、Tasks 拡張のハンドルに載せる - SSE のストリームは、1 つのリクエストの間だけ、1 つのインスタンスに結びつく。
subscriptions/listenは、開いている間、1 つのインスタンスが保持する。デプロイでインスタンスを入れ替えるときは、サーバーが、成功のレスポンスで購読を閉じ、クライアントが開き直す流れを前提にする - 逆プロキシ (nginx など) がバッファリングすると、SSE のイベントが遅れる。SSE ストリームを返すサーバーは、
X-Accel-Buffering: noを付けることが推奨される
HTTP+SSE からの移行
2024-11-05 版の HTTP+SSE トランスポートは、2025-03-26 から非推奨で、SEP-2596 で、フィーチャーライフサイクル上も Deprecated に分類されました。新しい実装は採用せず、Streamable HTTP に移行します。移行措置により、削除の最短時期は SEP-2596 が Final になってから 3 か月後です (削除は Core Maintainers の判断です)。最新の時期は、非推奨機能のレジストリ で確認してください。
2025-03-26 から 2025-11-25 の Streamable HTTP も、2026-07-28 とは形が違います。
| 項目 | 2025-11-25 まで | 2026-07-28 |
|---|---|---|
| セッション | Mcp-Session-Id (HTTP DELETE で終了) | なし |
| GET のストリーム | サーバー起点のメッセージ用に、クライアントが開ける | なし (subscriptions/listen) |
| サーバー起点のリクエスト | SSE ストリームで送れる | なし (MRTR) |
| 再開 | Last-Event-ID | なし |
2026-07-28 だけに対応するサーバーが、旧クライアントのトラフィックを受けたときの応答は、次のとおりです。
- MCP エンドポイントへの HTTP GET または DELETE:
405 Method Not Allowed Mcp-Session-Idヘッダー: 無視する。セッション ID を発行も、返しもしないLast-Event-IDヘッダー: 無視する
旧サーバーに対応したいクライアントは、まず新しい Streamable HTTP の POST を試します。400、404、405 のどれかで失敗し、かつ、本文が、認識できる Modern の JSON-RPC エラーでないときは、サーバー URL に GET を送り、SSE ストリームの最初のイベントが endpoint なら、旧 HTTP+SSE のサーバーと判断します。本文に、認識できる Modern のエラーがあれば、Modern のサーバーなので、広告された supported のバージョンで再試行するか、リクエストを直します。
ブラウザーからの接続
ブラウザーの Web ページや、ブラウザー上のクライアントが、MCP の HTTP エンドポイントへ接続する場合の要点です。
- Origin の検証: サーバーは、
Originヘッダーを検証し、存在して無効なら403 Forbiddenで拒否しなければならない。これが、ブラウザーを経由した、DNS rebinding への仕様上の対策 - localhost のサーバー: すべてのインターフェースではなく、127.0.0.1 だけで待ち受ける。認証も実装する。保護がないと、攻撃者が DNS rebinding で、リモートの Web サイトから、ローカルの MCP サーバーを操作できる。詳細と実装例は ローカルMCPサーバーの安全な運用 を参照
- CORS: 仕様は CORS の設定を規定しません。ブラウザーから、別のオリジンのサーバーに接続するなら、サーバーかゲートウェイで、許可するオリジンを明示した CORS の応答ヘッダーを設定します。CORS のヘッダーは認証ではないので、OAuth などの認証を併用します。
MCP-Protocol-Version、Mcp-Method、Mcp-Nameなどのカスタムヘッダーを付ける POST は、プリフライト (OPTIONS) の対象になるため、これらをAccess-Control-Allow-Headersに含めます
2. stdio と Streamable HTTP の比較
| 観点 | stdio | Streamable HTTP |
|---|---|---|
| 主な用途 | ローカルツール、開発者のマシン上の統合 | リモートサーバー、組織共通のサービス |
| 認証 | 環境変数など実行環境から取得 (OAuth 仕様には従わない) | OAuth 2.1 ベースの認可に従う |
| 配布・起動 | クライアントがプロセスを起動・管理 | URL を登録するだけ。サーバー側で運用 |
| スケール | クライアントごとに 1 プロセス | 水平スケール可能 |
| 可観測性 | stderr のログが中心 | HTTP の既存の監視基盤を流用できる |
3. 開発者がつまずきやすいポイント
stdout にログを書いてしまう
stdio のサーバーが、stdout に print やログを書くと、プロトコルのメッセージが壊れ、クライアントが解析に失敗します。stdout は、有効な MCP メッセージ専用で、ログは stderr へ出します。依存するライブラリが stdout に書かないかも確認します。
メッセージに改行を入れる
stdio では、1 メッセージが 1 行です。整形した (改行入りの) JSON をそのまま書くと、区切りが壊れます。JSON は、1 行にシリアライズします。文字列中の改行は、\n にエスケープされるので問題ありません。
Accept ヘッダーの片方だけを送る
クライアントは、Accept に application/json と text/event-stream の両方を入れる必要があります。サーバーは、リクエストごとに、どちらの形で応答するかを選べます。curl で試すとき、片方だけにすると、応答の形によって失敗します。
ヘッダーと本文の食い違い
MCP-Protocol-Version と _meta の値、Mcp-Method と method、Mcp-Name と params.name が、一致しないと、400 と HeaderMismatch (-32020) で拒否されます。ゲートウェイや独自のクライアントが、ヘッダーを書き換えたり落としたりしていないか確認します。ヘッダーの欠落も、HeaderMismatch の対象です。
Mcp-Name の非 ASCII
ツール名やリソース URI に、非 ASCII や制御文字があると、Mcp-Name は Base64 の形式で運ばれます。サーバーとゲートウェイは、復号してから、本文の値と比較します。ゲートウェイでツール名で制御するなら、復号に対応しているか確認します。ツール名は、ヘッダーに安全な文字だけにするのが無難です。
切断されたら再開できると思い込む
Last-Event-ID での再開はありません。切断後に、同じストリームを続きから受け取る実装を書かず、新しいリクエスト ID で再発行します。冪等でないツール (たとえば決済や送信) は、再発行で二重に実行されないよう、冪等キーなどを設計に含めます。
スティッキーセッションを前提にしたインフラ
ロードバランサーのセッション固定や、セッション ID でのルーティングは、2026-07-28 では不要です。固定していても動きますが、デプロイやスケールの自由度を損ないます。旧クライアントを同じエンドポイントで受けるあいだだけ、旧ルートに必要です。
SSE がバッファされて届かない
リバースプロキシや CDN が、レスポンスをバッファすると、進捗や購読の通知が、まとまって遅れて届きます。X-Accel-Buffering: no を付け、プロキシの設定でもバッファリングを切ります。長く開く購読では、保活のコメント行を送ります。
購読が静かに終わる
subscriptions/listen は、中継装置のタイムアウトや、サーバーの入れ替えで終わります。クライアントは、ストリームが閉じたことを検知し、開き直し、*/list を再取得します。成功のレスポンスで閉じたか、レスポンスなしで切れたかで、正常な終了と、予期しない切断を区別できます。
ローカルの HTTP サーバーを 0.0.0.0 で公開する
開発用のサーバーを、すべてのインターフェースで待ち受けると、同じネットワークの他の端末から、認証なしで操作されます。127.0.0.1 に絞り、Origin を検証し、認証を実装します。
4. コード例
JSON で応答するリクエスト (curl)
curl -sS -X POST https://example.com/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: get_weather' \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": { "location": "Seattle, WA" },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": { "name": "ExampleClient", "version": "1.0.0" },
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}'
x-mcp-header を使ったツール定義と、リクエスト
region の値を Mcp-Param-Region に複製するツールです。ロードバランサーは、本文を解析せずに、地域ごとのクラスターに振り分けられます。
{
"name": "execute_sql",
"description": "Execute SQL on Google Cloud Spanner",
"inputSchema": {
"type": "object",
"properties": {
"region": {
"type": "string",
"description": "The region to execute the query in",
"x-mcp-header": "Region"
},
"query": {
"type": "string",
"description": "The SQL query to execute"
}
},
"required": ["region", "query"]
}
}
POST /mcp HTTP/1.1
Content-Type: application/json
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: execute_sql
Mcp-Param-Region: us-west1
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "execute_sql",
"arguments": { "region": "us-west1", "query": "SELECT * FROM users" },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
非 ASCII の値の符号化
| 元の値 | 理由 | ヘッダーの値 |
|---|---|---|
us-west1 | 通常の ASCII | Mcp-Param-Region: us-west1 |
Hello, 世界 | 非 ASCII を含む | Mcp-Param-Greeting: =?base64?SGVsbG8sIOS4lueVjA==?= |
padded | 前後に空白 | Mcp-Param-Text: =?base64?IHBhZGRlZCA=?= |
購読の開始 (subscriptions/listen)
{
"jsonrpc": "2.0",
"id": 1,
"method": "subscriptions/listen",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {}
},
"notifications": {
"toolsListChanged": true,
"resourceSubscriptions": ["file:///project/config.json"]
}
}
}
最初に届く確認応答と、その後の通知です。subscriptionId は、リクエストの id と同じ値です。
{
"jsonrpc": "2.0",
"method": "notifications/subscriptions/acknowledged",
"params": {
"_meta": { "io.modelcontextprotocol/subscriptionId": 1 },
"notifications": {
"toolsListChanged": true,
"resourceSubscriptions": ["file:///project/config.json"]
}
}
}
{
"jsonrpc": "2.0",
"method": "notifications/resources/updated",
"params": {
"_meta": { "io.modelcontextprotocol/subscriptionId": 1 },
"uri": "file:///project/config.json"
}
}
ゲートウェイでのメソッド単位の制御 (nginx の例)
Mcp-Method ヘッダーを、変数として使い、ツール実行だけを別のバックエンドへ送り、SSE のバッファリングを切る例です。変数名やディレクティブは、使用している nginx のバージョンと環境で確認してください。
map $http_mcp_method $mcp_backend {
default mcp_general;
"tools/call" mcp_tools;
}
server {
listen 443 ssl;
server_name example.com;
location /mcp {
# ヘッダーは本文の複製。サーバー側でも、本文との一致を検証する
proxy_pass http://$mcp_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off; # SSE のイベントをすぐに届ける
proxy_read_timeout 1h; # subscriptions/listen のために長めにする
}
}
切断後の再発行 (TypeScript)
切断で失われたリクエストを、新しいリクエスト ID で再発行する骨格です。
let nextId = 1;
async function callWithRetry(
send: (id: number) => Promise<unknown>,
maxAttempts = 3,
): Promise<unknown> {
let lastError: unknown;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const id = nextId++; // 再発行のたびに新しい ID を使う
try {
return await send(id);
} catch (err) {
// ストリームの切断など。同じ内容を、新しい ID で再発行する
// (冪等でないツールでは、呼び出し側で二重実行を防ぐ)
lastError = err;
}
}
throw lastError;
}
stdio のサーバーで、ログを stderr に出す (Go)
import (
"log/slog"
"os"
)
// stdout は MCP のメッセージ専用。ログは stderr に出す
var logger = slog.New(slog.NewTextHandler(os.Stderr, nil))
5. まとめ
- トランスポートはメッセージの運び方を決める束縛で、プロトコルの意味はどの束縛でも同じ
- stdio は、ローカルのサブプロセスで、改行区切りのメッセージを交換する。
stdoutは MCP のメッセージ専用で、ログはstderr - Streamable HTTP は、単一のエンドポイントへの POST で、応答は JSON か、リクエスト専用の SSE ストリーム
MCP-Protocol-Version、Mcp-Method、Mcp-Nameが必須で、x-mcp-headerでツール引数もMcp-Param-*にできる。本文が正で、サーバーは一致を検証し、不一致は400と-32020- ヘッダーのおかげで、ゲートウェイや WAF が本文を解析せずに、ルーティング、レート制限、メトリクスを扱える。ただし、ヘッダーの値は信頼しない
subscriptions/listenが、GET のエンドポイントとresources/subscribeを置き換える。Last-Event-IDによる再開はなく、切断したら新しいリクエスト ID で再発行する- セッションがないので、スティッキーセッションは不要で、水平スケールしやすい
- HTTP+SSE は非推奨で、Streamable HTTP に移行する。ブラウザーやローカルの HTTP サーバーでは、Origin の検証、localhost への束縛、認証が要る
関連ドキュメント
- MCP入門 - Model Context Protocolとは
- MCPプロトコル基礎 - JSON-RPCとステートレス設計
- MCP最新仕様 2026-07-28 の変更点と移行
- ローカルMCPサーバーの安全な運用
- MCPの認可 - OAuth 2.1とメタデータ探索