Skip to content
33 changes: 21 additions & 12 deletions DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1820,11 +1820,18 @@ return `-32600`.
before they are fully buffered. The
pending request queue is bounded by `CDIDX_MCP_HTTP_MAX_QUEUE_DEPTH`
(default: 64, maximum: 1,024); full queues return `429 Too Many Requests`
with `Retry-After: 1` instead of retaining unbounded work. Non-positive or
non-numeric environment values fall back to defaults, while values above
the maximum are rejected before listener startup.
- SSE stream lifetime is represented by the active stream registry only;
completed stream tasks are not retained after that registry entry is removed.
with `Retry-After: 1` instead of retaining unbounded work. Accepted context
handler tasks are bounded by `CDIDX_MCP_HTTP_MAX_CONCURRENT_HANDLERS`
(default: 64, maximum: 1,024), and concurrent `/events` streams are bounded
by `CDIDX_MCP_HTTP_MAX_EVENT_STREAMS` (default: 16, maximum: 1,024);
saturated limits return `429 Too Many Requests` with `Retry-After: 1`.
Non-positive or non-numeric environment values fall back to defaults, while
values above the maximum are rejected before listener startup.
- SSE stream lifetime is represented by the active stream registry and a
bounded active-stream counter only. Idle streams receive minimal SSE comment
heartbeats so disconnected clients are detected and stream slots are
released; completed stream tasks are not retained after the registry entry
is removed.
- `ResolveListenSpec("host:port")` resolves the prefix up-front so the
CLI can log the bound port to stderr (`Listening on http://...`).
Port `0` is resolved by probing a temporary `TcpListener`; the
Expand All @@ -1837,13 +1844,15 @@ return `-32600`.
HTTP falls back to `CDIDX_MCP_AUTH_TOKEN` as the bearer secret; when both
are set, `CDIDX_MCP_HTTP_TOKEN` wins. HTTP clients never need to also send
`params.auth.token`. The CLI refuses to bind to a non-loopback host without
either token to keep the MCP catalog off the LAN by default.
either token to keep the MCP catalog off the LAN by default. Configured and
supplied tokens over 4096 characters are rejected before hashing.
- Optional request-loop logging: `ProgramRunner` connects `HttpMcpTransport`
to `GlobalToolLog`, so persistent logging records one `mcp_http_request`
line per HTTP request when the lifecycle log is enabled. The record includes
method, path, status, duration, auth outcome, remote peer, correlation id,
and JSON-RPC request id when available; it never includes request or response
bodies.
and JSON-RPC request id when available; caller-controlled method, path,
remote peer, and request id values are capped at 256 characters with a
`...<truncated>` marker, and it never includes request or response bodies.
- Cancellation hooks the `CancellationToken` into
`_listener.Stop()` so `GetContextAsync()` unblocks on shutdown;
`HttpListenerException` / `ObjectDisposedException` are treated as
Expand Down Expand Up @@ -3487,11 +3496,11 @@ MCP は独立したシリアライズ戦略(オブジェクトを JSON など

`HttpMcpTransport`(同じく #1558)は `System.Net.HttpListener` をラップする:

- HTTP POST 1 件 = JSON-RPC フレーム 1 件で、対応する応答は HTTP レスポンスのボディ(`200 OK` / `application/json; charset=utf-8`)に乗る。通知は `204 No Content`。`GET /events` は将来のサーバー→クライアント frame 用に独立した `text/event-stream` subscription を開く。サーバーは `CDIDX_MCP_KEEP_ALIVE_INTERVAL_S` で keep-alive notification が opt-in された場合を除き、自発的な frame を送信しない。長寿命の event stream は通常の POST リクエストを塞がない。`/` への POST 以外は `405 Method Not Allowed`。空 / 空白のみのボディは stdio の空行と同じ扱いで `204 No Content` を返し、ループは殺さない — クライアントの誤動作で junk フレームに引っかからないため。リクエスト本文は `CDIDX_MCP_HTTP_MAX_REQUEST_BYTES`(既定: 1,000,000 bytes、最大: 16,777,216 bytes)で制限し、超過時は全量を buffer する前に `413 Payload Too Large` を返す。保留中 request queue は `CDIDX_MCP_HTTP_MAX_QUEUE_DEPTH`(既定: 64、最大: 1,024)で制限し、満杯時は無制限に work を保持せず `Retry-After: 1` 付きの `429 Too Many Requests` を返す。正でない値や数値でない環境変数値は既定にフォールバックし、最大値を超える値は listener 起動前に拒否する。
- SSE stream lifetime は active stream registry だけで表現し、その registry entry が削除された後に完了済み stream task を保持しない。
- HTTP POST 1 件 = JSON-RPC フレーム 1 件で、対応する応答は HTTP レスポンスのボディ(`200 OK` / `application/json; charset=utf-8`)に乗る。通知は `204 No Content`。`GET /events` は将来のサーバー→クライアント frame 用に独立した `text/event-stream` subscription を開く。サーバーは `CDIDX_MCP_KEEP_ALIVE_INTERVAL_S` で keep-alive notification が opt-in された場合を除き、自発的な frame を送信しない。長寿命の event stream は通常の POST リクエストを塞がない。`/` への POST 以外は `405 Method Not Allowed`。空 / 空白のみのボディは stdio の空行と同じ扱いで `204 No Content` を返し、ループは殺さない — クライアントの誤動作で junk フレームに引っかからないため。リクエスト本文は `CDIDX_MCP_HTTP_MAX_REQUEST_BYTES`(既定: 1,000,000 bytes、最大: 16,777,216 bytes)で制限し、超過時は全量を buffer する前に `413 Payload Too Large` を返す。保留中 request queue は `CDIDX_MCP_HTTP_MAX_QUEUE_DEPTH`(既定: 64、最大: 1,024)、受理済み context handler task は `CDIDX_MCP_HTTP_MAX_CONCURRENT_HANDLERS`(既定: 64、最大: 1,024)、同時 `/events` stream は `CDIDX_MCP_HTTP_MAX_EVENT_STREAMS`(既定: 16、最大: 1,024)で制限し、満杯時は無制限に work を保持せず `Retry-After: 1` 付きの `429 Too Many Requests` を返す。正でない値や数値でない環境変数値は既定にフォールバックし、最大値を超える値は listener 起動前に拒否する。
- SSE stream lifetime は active stream registry と上限付き active-stream counter だけで表現する。idle stream には最小限の SSE comment heartbeat を送り、切断済み client を検出して stream slot を解放する。その registry entry が削除された後に完了済み stream task を保持しない。
- `ResolveListenSpec("host:port")` は prefix を事前に解決するため、CLI が stderr に `Listening on http://...` を出せる。ポート `0` は一時 `TcpListener` を probe して空きポートを取得する。probe から `HttpListener.Start()` までの TOCTOU window は、本トランスポートが local-only / single-tenant 想定であるため許容する。ワイルドカードホスト `+` / `*` はパース時点で拒否する。
- 任意の共有秘密による認証: `CDIDX_MCP_HTTP_TOKEN` が設定されていれば、listener はすべてのリクエストに `Authorization: Bearer <token>` を要求し、定数時間で比較する。`CDIDX_MCP_HTTP_TOKEN` が未設定なら HTTP は `CDIDX_MCP_AUTH_TOKEN` を bearer secret として fallback し、両方が設定されている場合は `CDIDX_MCP_HTTP_TOKEN` を優先する。HTTP クライアントが `params.auth.token` も送る必要はない。どちらのトークンも未指定で非 loopback ホストへ bind しようとした場合、CLI は MCP カタログを LAN に漏らさないよう既定で拒否する。
- 任意のリクエストループログ: `ProgramRunner` は `HttpMcpTransport` を `GlobalToolLog` に接続するため、lifecycle log が有効な場合は HTTP リクエストごとに `mcp_http_request` 行を 1 件記録する。記録内容は method、path、status、duration、auth outcome、remote peer、correlation id、利用可能な JSON-RPC request id で、リクエスト/レスポンス本文は含めない。
- 任意の共有秘密による認証: `CDIDX_MCP_HTTP_TOKEN` が設定されていれば、listener はすべてのリクエストに `Authorization: Bearer <token>` を要求し、定数時間で比較する。`CDIDX_MCP_HTTP_TOKEN` が未設定なら HTTP は `CDIDX_MCP_AUTH_TOKEN` を bearer secret として fallback し、両方が設定されている場合は `CDIDX_MCP_HTTP_TOKEN` を優先する。HTTP クライアントが `params.auth.token` も送る必要はない。どちらのトークンも未指定で非 loopback ホストへ bind しようとした場合、CLI は MCP カタログを LAN に漏らさないよう既定で拒否する。設定 token と受信 token は 4096 文字を超える場合、hash 前に拒否する。
- 任意のリクエストループログ: `ProgramRunner` は `HttpMcpTransport` を `GlobalToolLog` に接続するため、lifecycle log が有効な場合は HTTP リクエストごとに `mcp_http_request` 行を 1 件記録する。記録内容は method、path、status、duration、auth outcome、remote peer、correlation id、利用可能な JSON-RPC request id で、caller-controlled な method、path、remote peer、request id は 256 文字を上限に `...<truncated>` marker 付きで切り詰める。リクエスト/レスポンス本文は含めない。
- キャンセルは `_listener.Stop()` に接続するため、シャットダウン時に `GetContextAsync()` が unblock する。`HttpListenerException` / `ObjectDisposedException` は EOS と同じ扱いで MCP ループを stdin クローズと同じ経路で終了させる。

ワイヤー選択は `ProgramRunner.RunMcp` で行う。`--transport stdio|http` と `--http-listen <host:port>` は下流の引数解析より前に取り除かれ、HTTP bearer token 解決は `CDIDX_MCP_HTTP_TOKEN` を先に見て、未設定なら `CDIDX_MCP_AUTH_TOKEN` に fallback する。ディスパッチは旧来の stdio 経路または `RunMcpHttp` に着地する。プラガブルなシームは JSON-RPC 順序不変条件を両トランスポートで同一に保つので、既存の McpServer テスト群(`ProcessLineAsync` を叩く)は引き続きメソッド単位の挙動をカバーし、新トランスポートのワイヤーレベル契約は `HttpMcpTransportTests` がカバーする。
Expand Down
Loading
Loading