Claude Media
MCPのカスタムトランスポートを設計する — Unixソケット/TCP指針

MCPのカスタムトランスポートを設計する — Unixソケット/TCP指針

MCPは標準のstdio・Streamable HTTP以外にカスタムトランスポートの実装を許容します。MUST/SHOULDの要件とstdioフレーミング再利用の設計指針を仕様原文から確認します。

カスタムトランスポートとは何か — 標準2種以外を選ぶ場面

MCPの仕様は、クライアントとサーバーが標準のstdio・Streamable HTTP以外に独自のトランスポート機構を実装することを明示的に許容しています。プロトコル自体はトランスポートに依存しない設計で、双方向のメッセージ交換ができる通信チャネルであれば、どんな経路の上にも実装できます。

標準の2トランスポートでは足りない場面は具体的です。同一ホスト内のプロセス間通信でTCPのオーバーヘッドを避けたいならUnixドメインソケット、プライベートネットワーク内でサブプロセス起動を避けたいならTCP、社内標準のメッセージングインフラに乗せたいなら他の双方向チャネルが候補になります。仕様がこの余地をわざわざ残しているのは、MCPを特定の配線方法に縛らないためです。

身近な実例もあります。Claude CodeのMCPクライアントは、claude mcp add --transportでstdio・http(Streamable HTTP)・sse(非推奨)の3方式を選べるほか、type: "ws"のJSON設定でWebSocketサーバーにも接続できます。WebSocketはMCP仕様が定める標準2トランスポートには含まれておらず、Claude Code側が独自に実装を追加した経路です。サーバーがクライアントへ予告なくイベントをpushしたい常時接続の用途に向くとされ、この記事で扱うMUST/SHOULDの要件がホスト実装側でどう扱われているかを見る具体例になります。

実装がMUSTで守るべき3つの要件

トランスポートを自作する自由度は高い一方、仕様は逸脱してはいけない境界線をMUSTで3つ定めています。

  1. JSON-RPCのメッセージ形式を保つこと。フレーミング方法は変えてよくても、メッセージそのものはJSON-RPCのまま
  2. メッセージパターンを保つこと。リクエスト・通知・レスポンスの往復ルールはコアプロトコルの一部であり、どのバインディングでも同一。サーバーがクライアントにJSON-RPCリクエストを送らない、といった役割分担をトランスポート側で崩さない
  3. リクエストごとのメタデータモデルを保つこと。プロトコルバージョンやクライアント機能情報を運ぶ仕組みは、メッセージ本文の_metaフィールドが正であり続ける

この3つを守っていれば、フレーミング方式や接続の張り方は実装者の裁量に委ねられます。逆に言えば、独自の応答フォーマットを発明したり、サーバー側からクライアントへ一方的にリクエストを送る経路を作ったりすると、MCPクライアント側の共通実装と噛み合わなくなります。

SHOULDで求められる3点のドキュメント化

MUSTの3要件に加えて、仕様はカスタムトランスポートの実装者に3点の文書化をSHOULDとして求めています。接続確立の手順、メッセージのフレーミング方法、キャンセルパターンです。この3つを書いていないカスタムトランスポートは、実装者本人以外が相互運用性を検証する手段を持てません。

キャンセルパターンの文書化がとくに見落とされやすい箇所です。stdioではnotifications/cancelled通知を送るだけで済みますが、これは「単一の共有双方向チャネルなので専用のストリームを閉じる操作が無い」という前提あってのルールです。独自チャネルでリクエストごとに個別のコネクションを張る設計なら、そのコネクションを閉じることをキャンセルとみなすのか、それとも通知メッセージを送るのかを自分で決めて明文化する必要があります。

Unixドメインソケット/TCPで作るならstdioフレーミングを再利用する

信頼できる双方向バイトストリームの上にカスタムトランスポートを作る場合、仕様は新しいフレーミング方式を発明するのではなく、stdioのフレーミングを再利用することをSHOULDとして推奨しています。stdioのバインディングは、実質的には「改行区切りのJSON-RPCメッセージを、信頼できるバイトストリームの上に流す」だけの仕組みで、プロセス固有のルールはごく一部にとどまります。

再利用できる部分と、チャネルごとに作り直す必要がある部分は明確に分かれます。

要素stdioと共通で再利用できるチャネル固有に作り直す必要がある
メッセージのフレーミングstdioと共通で再利用できる改行区切りのJSON-RPC、1行1メッセージチャネル固有に作り直す必要がある
メタデータの運び方stdioと共通で再利用できる_metaフィールドがすべてを運ぶ、ヘッダー層は無いチャネル固有に作り直す必要がある
メッセージの方向性ルールstdioと共通で再利用できるクライアントがrequest/notification送信、サーバーがresponse/notification送信チャネル固有に作り直す必要がある
接続の確立stdioと共通で再利用できるチャネル固有に作り直す必要があるUnixソケットのbind/connect、TCPのlisten/accept
標準エラー出力相当のログ経路stdioと共通で再利用できるチャネル固有に作り直す必要があるstderrは子プロセス専用の概念なので、ログ用の別チャネルか別フィールドを設計する
シャットダウンstdioと共通で再利用できるチャネル固有に作り直す必要がある「入力ストリームを閉じてプロセス終了を待つ」に相当する、チャネルごとの終了合図
プロセス異常終了時の再起動stdioと共通で再利用できるチャネル固有に作り直す必要がある子プロセスの概念が無いチャネルでは、再接続の判断基準を自分で定義する

上の表からも分かる通り、作り直しが必要なのはプロセスのライフサイクルに紐づく部分だけです。メッセージのやり取り自体はstdioのルールをそのまま持ってこられます。Unixドメインソケット越しに流すメッセージも、フレーミングとしては改行区切りのJSON-RPCそのままです。

{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}\n
{"jsonrpc":"2.0","id":1,"result":{"tools":[]}}\n

ソケットのbind/connectとプロセス起動の違いを除けば、フレーム自体はstdioと同じ形をしています。

メタデータをヘッダー相当の層に写してよいか

プロトコルのメタデータ(バージョンやクライアント機能など)は、すべてメッセージ本文の_meta.io.modelcontextprotocol/*フィールドに乗ります。カスタムトランスポートを設計するときに迷いやすいのが、このメタデータをチャネル固有の「ヘッダー相当の層」にも複製してよいかという点です。

仕様の答えは「複製してもよいが、本文が正であり続けること」です。Streamable HTTPは実際にこの方式を採っており、本文のフィールドの一部をHTTPヘッダーにも反映させることで、中継装置がボディをパースせずにルーティングや検査を行えるようにしています。ただし本文が唯一の正であることに変わりはなく、ヘッダー相当の層と本文が食い違った場合にどちらを優先しどう拒否するかは、そのバインディングが自分で定義する責任を負います。Unixソケットのような素のバイトストリームでこの層を作る場合も、同じ原則(本文が正、複製層との不一致は明示的に拒否する)を踏襲するのが安全です。

後方互換性の検知はバインディングごとに個別実装になる

MCPの古いプロトコル改訂には、接続単位のセッションとinitializeハンドシェイクがあり、サーバーからクライアントへJSON-RPCリクエストを送ることも許されていました。現行の仕様と相互運用するクライアント・サーバーは、相手がどちらの世代かを検知してフォールバックする必要があります。この検知の具体的な手順は、バインディングのページごとに個別に記述する決まりです。

stdioバインディングでは、server/discoverというリクエストを最初に送ってプローブする方式を採っています。モダンなサーバーならDiscoverResultか既知のエラーコードを返し、レガシーなサーバーならタイムアウトするか未知のエラーを返す、という3パターンで世代を判定します。カスタムトランスポートを設計する際も、この「まず軽いプローブを送り、応答の形で世代を判定する」という考え方はそのまま流用できますが、具体的なプローブの手順書化は自分のトランスポート専用に書く必要があります。相互運用性を検討するチームが多いカスタムトランスポートほど、この後方互換性の章を省略すると混乱の元になります。

標準トランスポートとの使い分け

3つの選択肢を並べると、どこにカスタムトランスポートの居場所があるかが見えます。

トランスポート主な用途特徴
stdio主な用途クライアントが起動するローカルサーバー特徴サブプロセスの標準入出力を使う、設定が最も単純
Streamable HTTP主な用途独立プロセスとして複数クライアントを相手にするリモートサーバー特徴単一のPOSTエンドポイント、Origin検証などWeb向けのセキュリティ要件を持つ
カスタム(Unixソケット/TCP等)主な用途同一ホスト内の高頻度IPC、社内標準インフラへの統合特徴stdioフレーミングを再利用しつつ、接続確立とライフサイクルだけを独自定義

stdioとStreamable HTTPの個別仕様はMCP stdioトランスポートの仕様を実装レベルで確認するMCP Streamable HTTPからGETストリームとセッションが消えた理由で扱っています。カスタムトランスポートを検討する前に、この2つで要件を満たせないかを先に確認する価値があります。標準トランスポートで足りるなら、独自実装の保守コストを負う理由はありません。

設計時に見落としやすい落とし穴

カスタムトランスポートの実装で崩れやすいポイントがいくつかあります。

  • メッセージパターンを崩す。サーバー側から能動的にクライアントへリクエストを送る経路を作ってしまうと、MRTR(Multi Round-Trip Requests)のようなコアプロトコル側のパターンと矛盾する。サーバー主導のやり取りが必要に見える場面ほど、既存のパターンで表現できないかを先に確認する
  • キャンセルの意味を定義し忘れる。「接続を切ればキャンセル」なのか「専用の通知メッセージが必要」なのかを決めないまま実装すると、クライアント側の実装によって挙動が変わってしまう
  • 後方互換性の扱いを個別に作り込みすぎる。古いバージョンとの識別・フォールバックの手順はコアプロトコル側に規定があるため、トランスポート層で独自の互換ロジックを重ねる必要は本来ない
  • 文書化を後回しにする。接続確立・フレーミング・キャンセルの3点を書かずに社内実装だけで運用すると、後から別チームが同じトランスポートに接続しようとしたときに仕様を推測するしかなくなる
  • Streamable HTTP向けのセキュリティ対策をそのまま流用したつもりになるOriginヘッダー検証やDNSリバインディング対策はHTTPというチャネル固有の脅威モデルに基づく要件で、Unixドメインソケットのようなチャネルには自動的には引き継がれない。チャネルが変われば脅威モデルも変わるため、アクセス制御はチャネルごとに独立して設計し直す

いずれも「MUSTの3要件は守ったが、SHOULDの3点を省略した」ときに起きる問題です。動くことと、他の実装と相互運用できることは別の基準だと考えておくと安全です。

まとめ

MCPのカスタムトランスポートは、JSON-RPCのメッセージ形式・メッセージパターン・リクエストごとのメタデータモデルという3つのMUSTさえ守れば、接続チャネル自体は自由に選べます。Unixドメインソケットやプレーンなソケット・TCPの上に組む場合は、新しいフレーミングを考案せずstdioのフレーミングを再利用し、プロセスのライフサイクルに紐づく部分だけをチャネル固有に作り直すのが仕様の推奨する設計です。接続確立・フレーミング・キャンセルパターンの3点をドキュメント化しておくことが、独自実装を後から他の開発者が扱えるものにする最低限の条件になります。

この記事を共有:XはてブLinkedIn
MCP をもっと見る →