メインコンテンツまでスキップ

トランスポート - 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 つです。

  1. stdio: クライアントが起動するサブプロセスの標準ストリームで、改行区切りのメッセージをやり取りする
  2. 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 種類です。

  1. クライアントのリクエストへのレスポンス (JSON-RPC の id で対応づける)
  2. 処理中のリクエストに関係する通知 (notifications/progress や notifications/message)
  3. 有効な 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 つです。

  1. サーバーは、すべての接続で Origin ヘッダーを検証しなければならない (DNS rebinding の対策)。Origin があって無効なら、403 Forbidden を返す
  2. ローカルで動かすサーバーは、すべてのインターフェース (0.0.0.0) ではなく、localhost (127.0.0.1) だけで待ち受けることが推奨される
  3. サーバーは、すべての接続に、適切な認証を実装することが推奨される。HTTP の認可は MCPの認可 - OAuth 2.1とメタデータ探索 を参照

必須の HTTP ヘッダー​

Streamable HTTP は、本文の一部のフィールドを HTTP ヘッダーに複製します。ロードバランサー、ゲートウェイ、可観測性のツールが、本文を解析せずにリクエストをルーティングし、検査できるようにするためです。正は本文で、ヘッダーは複製です。

ヘッダー元になる項目必須となるリクエスト
MCP-Protocol-Version_meta の io.modelcontextprotocol/protocolVersionすべての POST
Mcp-Methodmethodすべてのリクエスト
Mcp-Nameparams.name または params.uritools/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 で指定します。サーバーは、要求されていない種類を送ってはいけません。

フィールド型受け取る通知
toolsListChangedbooleannotifications/tools/list_changed
promptsListChangedbooleannotifications/prompts/list_changed
resourcesListChangedbooleannotifications/resources/list_changed
resourceSubscriptionsstring[]指定した 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 の比較​

観点stdioStreamable 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通常の ASCIIMcp-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 への束縛、認証が要る

関連ドキュメント​

参考資料​