Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1735,7 +1735,9 @@ Piping `{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}` into
a server-side ready signal can proceed without optimistic polling (#1780).
MCP also defines `notifications/initialized` as a client-to-server
notification; cdidx still accepts that direction as a no-op, and the
server-origin emission is intentionally limited to this compatibility signal.
compatibility signal is the only server-origin emission for non-HTTP
transports. HTTP sessions can also receive opt-in keep-alive notifications
on `/events` when `CDIDX_MCP_KEEP_ALIVE_INTERVAL_S` is configured.
On HTTP transport, out-of-band notifications are delivered only to connected
`/events` SSE streams; POST-only clients receive the initialize response but
no separate notification frame.
Expand Down Expand Up @@ -1823,8 +1825,9 @@ return `-32600`.
response body (`200 OK` / `application/json; charset=utf-8`) or
`204 No Content` for notifications. `GET /events` opens an independent
`text/event-stream` subscription for future server→client frames; the
current server does not yet emit unsolicited frames, but a long-lived
event stream does not block normal POST requests.
server emits no unsolicited frames unless keep-alive notifications are opted
in with `CDIDX_MCP_KEEP_ALIVE_INTERVAL_S`. A long-lived event stream does not
block normal POST requests.
Non-POST verbs on `/` return `405 Method Not Allowed`. Empty / whitespace
bodies are treated like a closed stdio line and return `204 No Content`
*without* killing the loop, so a misbehaving client cannot pin the server
Expand Down Expand Up @@ -3124,7 +3127,7 @@ sequenceDiagram

- `McpServer` が stdin/stdout を持ち、JSON-RPC 2.0 フレームを解析する。
- レスポンス構築は `JsonSerializer.Serialize<T>(...)` ではなく、`System.Text.Json.Nodes.JsonObject` / `JsonArray` を**手組み**する。これが、トリミング済みバイナリでリフレクションベースのシリアライズが無効でも MCP パスが動き続ける理由。
- `initialize` レスポンスは `protocolVersion`、`capabilities`、`serverInfo.name`、`serverInfo.version`(`ConsoleUi.LoadVersion()` — `version.json` が源)、および AI クライアントにツール選択を案内する長い `instructions` 文字列を返す。レスポンスを書き終えた後、サーバーはセッションごとに 1 回だけ互換性用の `notifications/initialized` ready signal を送るため、サーバー側の ready signal を待つクライアントも optimistic polling なしで進める(#1780)。MCP は `notifications/initialized` を client-to-server 通知としても定義しており、cdidx はその方向も no-op として受理する。server-origin emission はこの互換性 signal に限定する。HTTP transport では out-of-band 通知は接続済みの `/events` SSE stream にだけ配送され、POST のみのクライアントは initialize response だけを受け取り、別通知 frame は受け取らない。
- `initialize` レスポンスは `protocolVersion`、`capabilities`、`serverInfo.name`、`serverInfo.version`(`ConsoleUi.LoadVersion()` — `version.json` が源)、および AI クライアントにツール選択を案内する長い `instructions` 文字列を返す。レスポンスを書き終えた後、サーバーはセッションごとに 1 回だけ互換性用の `notifications/initialized` ready signal を送るため、サーバー側の ready signal を待つクライアントも optimistic polling なしで進める(#1780)。MCP は `notifications/initialized` を client-to-server 通知としても定義しており、cdidx はその方向も no-op として受理する。非 HTTP transport では、この互換性 signal が唯一の server-origin emission です。HTTP session は `CDIDX_MCP_KEEP_ALIVE_INTERVAL_S` を設定した場合、opt-in の keep-alive notification も `/events` で受け取れる。HTTP transport では out-of-band 通知は接続済みの `/events` SSE stream にだけ配送され、POST のみのクライアントは initialize response だけを受け取り、別通知 frame は受け取らない。
- advertised capability には `tools`、`resources`、`prompts`、`logging` が含まれる。`logging` は MCP `notifications/message` を示し、`logging/setLevel` は `debug`、`info`、`notice`、`warning`、`error`、`critical`、`alert`、`emergency` を受け付ける。
- `protocolVersion` は**ハードコードではなく交渉**で決まる(#1554)。サーバーは `McpServer.SupportedProtocolVersions`(新しい順: `2025-03-26`, `2024-11-05`)を保持し、`initialize` パラメータからクライアント要求バージョンを読み取って、対応集合にあればそれを返し(合意)、未指定/非文字列なら既定の最新バージョンに fallback し、対応外なら `error.data` に `requestedVersion` と `supportedVersions` を入れた JSON-RPC `-32602` で拒否する。これにより将来 MCP 仕様が改訂されても、wire format が黙ってずれるのではなく actionable な handshake 失敗として表面化する。配列を新バージョンで更新する際は `ProtocolVersion` を先頭エントリと揃えて意図的に bump する。
- **認証ミドルウェア**(#1559)。`McpServer` はパース済み JSON-RPC リクエストごとに、メソッド抽出 *後*・dispatch *前* で `IMcpAuthenticator` を呼ぶ。既定の `LocalStdioAuthenticator` は permissive で(従来の stdio 動作を維持し、呼び出し元を `stdio` / `local` でタグ付けする)、`CDIDX_MCP_AUTH_TOKEN` を設定すると `TokenMcpAuthenticator` に切り替わる。`TokenMcpAuthenticator` は応答が必要な全リクエストに対し、`params.auth.token` が一致することを要求し、比較は `CryptographicOperations.FixedTimeEquals` による定数時間比較で行う。失敗は統一された JSON-RPC `-32001 "Unauthorized"` を返し(#1530 の sanitization 方針に従い、ワイヤでは未提示と不一致を区別しない)、`BuildAuthFailureLog` が詳細を stderr に書き出す。通知(`notifications/initialized`、`notifications/cancelled`)は応答もエラーコードも持たないため、ゲート *より前* で short-circuit する。このミドルウェアが将来 transport の差し替え seam になる — ネットワーク listener は別の `IMcpAuthenticator` を提供しつつ、`McpCallerIdentity`(`Source` + `Subject`)の形を保ち、監査ログ(#1562)から再利用できる。
Expand All @@ -3144,7 +3147,7 @@ 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 を開く。現サーバーは自発的な frame をまだ送信しないが、長寿命の event stream は通常の POST リクエストを塞がない。`/` への POST 以外は `405 Method Not Allowed`。空 / 空白のみのボディは stdio の空行と同じ扱いで `204 No Content` を返し、ループは殺さない — クライアントの誤動作で junk フレームに引っかからないため。リクエスト本文は `CDIDX_MCP_HTTP_MAX_REQUEST_BYTES`(既定: 1,000,000 bytes)で制限し、超過時は全量を buffer する前に `413 Payload Too Large` を返す。保留中 request queue は `CDIDX_MCP_HTTP_MAX_QUEUE_DEPTH`(既定: 64)で制限し、満杯時は無制限に work を保持せず `Retry-After: 1` 付きの `429 Too Many Requests` を返す。
- 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)で制限し、超過時は全量を buffer する前に `413 Payload Too Large` を返す。保留中 request queue は `CDIDX_MCP_HTTP_MAX_QUEUE_DEPTH`(既定: 64)で制限し、満杯時は無制限に work を保持せず `Retry-After: 1` 付きの `429 Too Many Requests` を返す。
- SSE stream lifetime は active stream registry だけで表現し、その 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>` を要求し、定数時間で比較する。トークン未指定で非 loopback ホストへ bind しようとした場合、CLI は MCP カタログを LAN に漏らさないよう既定で拒否する。
Expand Down
15 changes: 15 additions & 0 deletions changelog.d/unreleased/2963.docs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
category: docs
issues:
- 2963
affected:
- DEVELOPER_GUIDE.md
---

## English

- **Clarified HTTP MCP SSE keep-alive documentation (#2963)** - `DEVELOPER_GUIDE.md` now states that `/events` emits server-to-client frames only for opt-in keep-alive notifications configured through `CDIDX_MCP_KEEP_ALIVE_INTERVAL_S`.

## 日本語

- **HTTP MCP SSE keep-alive の開発者向けドキュメントを明確化しました (#2963)** - `DEVELOPER_GUIDE.md` は、`/events` が `CDIDX_MCP_KEEP_ALIVE_INTERVAL_S` で opt-in された keep-alive notification の場合にだけ server-to-client frame を送ることを明記しました。