コンテンツにスキップ

MCPプロトコルとトランスポート内部構造

このドキュメントでは、coding-agentがMCP JSON-RPCメッセージングをどのように実装しているか、またプロトコルの関心事がトランスポートの関心事からどのように分離されているかについて説明します。

対象範囲:

  • JSON-RPCのリクエスト/レスポンスおよび通知フロー
  • stdioおよびHTTP/SSEトランスポートにおけるリクエストの相関とライフサイクル
  • タイムアウトとキャンセルの動作
  • エラーの伝搬と不正なペイロードの処理
  • トランスポート選択の境界(stdio vs http/sse
  • 再接続/リトライの責任がトランスポートレベルかマネージャーレベルか

エクステンション作成のUXやコマンドUIは対象外です。

プロトコルレイヤー(JSON-RPC + MCPメソッド)

Section titled “プロトコルレイヤー(JSON-RPC + MCPメソッド)”
  • メッセージの形状はtypes.tsで定義されています(JsonRpcRequestJsonRpcNotificationJsonRpcResponseJsonRpcMessage)。
  • MCPクライアントロジック(client.ts)がメソッドの順序とセッションハンドシェイクを決定します:
    1. initialize リクエスト
    2. notifications/initialized 通知
    3. tools/listtools/call などのメソッド呼び出し

トランスポートレイヤー(MCPTransport

Section titled “トランスポートレイヤー(MCPTransport)”

MCPTransportは配信とライフサイクルを抽象化します:

  • request(method, params, options?) -> Promise<T>
  • notify(method, params?) -> Promise<void>
  • close()
  • connected
  • オプションのコールバック:onCloseonErroronNotification

トランスポート実装はフレーミングとI/Oの詳細を担当します:

  • StdioTransport:サブプロセスのstdioを介した改行区切りJSON
  • HttpTransport:HTTP POSTを介したJSON-RPC、オプションのSSEレスポンス/リスニング付き

トランスポートコールバック(onCloseonErroronNotification)は実装されていますが、現在のMCPClient/MCPManagerフローではこれらのコールバックに再接続ロジックが接続されていません。通知は呼び出し元がハンドラーを登録した場合のみ消費されます。

client.ts:createTransport()が設定からトランスポートを選択します:

  • typeが省略または"stdio" -> createStdioTransport
  • "http"または"sse" -> createHttpTransport

"sse"はHTTPトランスポートのバリアント(同じクラス)として扱われ、別個のトランスポート実装ではありません。

各トランスポートはリクエストごとにIDを生成します(Math.random + タイムスタンプ文字列)。IDはトランスポートローカルの相関トークンです。

  • 送信リクエストは1つのJSONオブジェクト + \nとしてシリアライズされます。
  • #pendingRequests: Map<id, {resolve,reject}>がインフライトリクエストを格納します。
  • 読み取りループがstdoutからJSONLをパースし、#handleMessageを呼び出します。
  • 受信メッセージに一致するidがある場合、リクエストはresolve/rejectされます。
  • 受信メッセージにmethodがありidがない場合、通知として扱われonNotificationに送信されます。

不明なIDは無視されます(rejectもエラーコールバックもありません)。

  • 送信リクエストは生成されたidを含むJSONボディのHTTP POSTです。
  • 非SSEレスポンスパス:1つのJSON-RPCレスポンスをパースし、resultを返すかerrorでスローします。
  • SSEレスポンスパス(Content-Type: text/event-stream):イベントをストリーミングし、期待するリクエストIDに一致しresultまたはerrorを持つ最初のメッセージを返します。
  • methodがありidがないSSEメッセージは通知として扱われます。

一致するレスポンスの前にSSEストリームが終了した場合、リクエストはNo response received for request ID ...で失敗します。

クライアントはtransport.notify(...)を介してJSON-RPC通知を送信します。

  • Stdio:通知フレーム(jsonrpcmethod、オプションのparams)と改行をstdinに書き込みます。
  • HTTP:idなしのPOSTボディを送信します。成功は2xxまたは202 Acceptedを受け入れます。

サーバー起点の通知はトランスポートのonNotificationを通じてのみ表面化されます。マネージャー/クライアントにはデフォルトのグローバルサブスクライバーはありません。

  • 初期状態:connected=falseprocess=null、pendingマップは空
  • connect()
    • 設定されたcommand/args/env/cwdでサブプロセスを生成
    • connectedをマーク
    • stdoutの読み取りループを開始(readJsonl
    • stderrのループを開始(読み取り/破棄;現在はサイレント)
  • close()
    • disconnectedをマーク
    • すべての保留中リクエストをreject(Transport closed
    • サブプロセスをkill
    • 読み取りループのシャットダウンを待機
    • onCloseを発行

読み取りループが予期せず終了した場合、finally#handleClose()をトリガーし、同じ保留中リクエストのrejectとcloseコールバックを実行します。

リクエストごとに:

  • タイムアウトのデフォルトはconfig.timeout ?? 30000
  • 呼び出し元からのオプションのAbortSignal
  • abortとtimeoutの両方が保留中のPromiseをrejectしマップエントリをクリーンアップ

キャンセルはローカルのみです:トランスポートはプロトコルレベルのキャンセル通知をサーバーに送信しません。

読み取りループ内:

  • パースされた各JSONL行はtry/catch内の#handleMessageに渡されます
  • 不正/無効なメッセージ処理の例外は破棄されます(Skip malformed linesコメント)
  • ループは継続するため、1つの不正メッセージで接続が切断されることはありません

基盤となるストリームパーサーがスローした場合、onErrorが呼び出され(まだ接続中の場合)、その後接続が閉じます。

プロセスの終了またはストリームの閉鎖時:

  • すべてのインフライトリクエストはTransport closedでrejectされます
  • 自動再起動や再接続はありません
  • 上位レイヤーが新しいトランスポートを作成して再接続する必要があります

バックプレッシャー/ストリーミングに関する注意事項

Section titled “バックプレッシャー/ストリーミングに関する注意事項”
  • 送信書き込みはstdin.write() + flush()を使用し、drainセマンティクスを待機しません。
  • トランスポートには明示的なキューやhigh-watermark管理がありません。
  • 受信処理は(readJsonlに対するfor awaitによる)ストリーム駆動で、パースされたメッセージを1つずつ処理します。

HTTP/SSEトランスポートの内部構造

Section titled “HTTP/SSEトランスポートの内部構造”

ライフサイクルと接続セマンティクス

Section titled “ライフサイクルと接続セマンティクス”

HTTPトランスポートは論理的な接続状態を持ちますが、リクエストパスはHTTP呼び出しごとにステートレスです:

  • connect()connected=trueを設定(ソケット/セッションハンドシェイクなし)
  • Mcp-Session-Idヘッダーによるオプションのサーバーセッション追跡
  • close()はオプションでMcp-Session-Id付きのDELETEを送信し、SSEリスナーを中断し、onCloseを発行

したがってconnectedは「トランスポートが使用可能」を意味し、「永続ストリームが確立済み」を意味しません。

  • POSTレスポンスでMcp-Session-Idヘッダーが存在する場合、トランスポートがそれを保存します。
  • 後続のリクエスト/通知にMcp-Session-Idが含まれます。
  • close()はHTTP DELETEでサーバーセッションの終了を試みます。終了の失敗は無視されます。

request()notify()の両方について:

  • タイムアウトはAbortControllerを使用(config.timeout ?? 30000
  • 外部シグナルが提供された場合、AbortSignal.any([...])でマージされます
  • AbortErrorの処理は呼び出し元のabortとタイムアウトを区別します

スローされるエラー:

  • タイムアウト:Request timeout after ...ms(またはSSE response timeout ...Notify timeout ...
  • 呼び出し元のabort:外部シグナルが既にabortされている場合、元のAbortErrorが再スローされます

非OKレスポンスの場合:

  • レスポンステキストがスローされるエラーに含まれます(HTTP <status>: <text>
  • 存在する場合、WWW-AuthenticateMcp-Auth-Serverからの認証ヒントが追加されます

JSON-RPCエラーオブジェクトの場合:

  • MCP error <code>: <message>をスローします

不正なJSONボディ(response.json()の失敗)はパース例外として伝搬します。

2つのSSEパスが存在します:

  1. リクエストごとのSSEレスポンス#parseSSEResponse

    • POSTレスポンスのContent Typeがtext/event-streamの場合に使用
    • 一致するレスポンスIDが見つかるまでストリームを消費
    • 同じストリーム内のインターリーブされた通知を処理可能
  2. バックグラウンドSSEリスナーstartSSEListener()

    • サーバー起点の通知用のオプションのGETリスナー
    • 現在MCPマネージャー/クライアントによって自動的に開始されません
    • GETが405を返した場合、リスナーはサイレントに自身を無効化します(サーバーがこのモードをサポートしていません)

不正なペイロードと切断の処理

Section titled “不正なペイロードと切断の処理”

SSE JSONパースエラーはreadSseJsonから浮上し、リクエスト/リスナーをrejectします。

  • リクエストSSEのパースエラーはアクティブなリクエストをrejectします。
  • バックグラウンドリスナーのエラーはonErrorをトリガーします(AbortErrorを除く)。
  • バックグラウンドリスナーの自動再接続はありません。

json-rpc.tsユーティリティとトランスポート抽象化

Section titled “json-rpc.tsユーティリティとトランスポート抽象化”

src/mcp/json-rpc.tsは、MCPClient/MCPManagerが使用するMCPTransport抽象化ではなく、直接的なHTTP MCP呼び出し(Exa統合で使用)のためのcallMCP()parseSSE()ヘルパーを提供します。

HttpTransportとの主な違い:

  • まずレスポンステキスト全体をパースし、次に最初のdata:行を抽出(parseSSE)、JSONフォールバック付き
  • リクエストタイムアウト管理、abort API、session-id処理、トランスポートライフサイクルなし
  • 生のJSON-RPCエンベロープオブジェクトを返す

このパスは軽量ですが、完全なトランスポート実装ほど堅牢ではありません。

現在のトランスポート実装は以下を行いません

  • 失敗したリクエストのリトライ
  • stdioプロセス終了後の再接続
  • SSEリスナーの再接続
  • 切断後のインフライトリクエストの再送信

即座に失敗し、エラーを伝搬します。

マネージャー/クライアントレベル

Section titled “マネージャー/クライアントレベル”

MCPManagerはディスカバリー/初期接続のオーケストレーションを処理し、接続フローを再実行することでのみ再接続できます(connectToServer/discoverAndConnectパス)。ランタイム障害コールバック時に既に接続済みのトランスポートを自動修復することはしません。

MCPManagerには遅いサーバー向けの起動時フォールバック動作(キャッシュからの遅延ツール)がありますが、これはツール可用性のフォールバックであり、トランスポートのリトライではありません。

  • 不正なstdioメッセージ行:破棄されます。ストリームは継続します。
  • stdioストリーム/プロセスの終了:トランスポートが閉じます。保留中のリクエストはTransport closedでrejectされます。
  • HTTP非2xx:リクエスト/notifyがHTTPエラーをスローします。
  • 無効なJSONレスポンス:パース例外が伝搬されます。
  • 一致するIDなしでSSEが終了:リクエストがNo response received for request ID ...で失敗します。
  • タイムアウト:トランスポート固有のタイムアウトエラー。
  • 呼び出し元のabort:呼び出し元のシグナルからAbortError/reasonが伝搬されます。

関心事がメッセージの形状、ID相関、またはMCPメソッドの順序であれば、プロトコル/クライアントロジックに属します。

関心事がフレーミング(JSONL vs HTTP/SSE)、ストリームパース、fetch/spawnのライフサイクル、タイムアウトクロック、または接続のティアダウンであれば、トランスポート実装に属します。