Skip to content
12 changes: 10 additions & 2 deletions DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -924,12 +924,16 @@ Each JSON-RPC MCP request gets a server-generated `correlation_id` in addition t

MCP stderr diagnostics are prefixed with `[rid=<opaque-token> rid_type=<id-type> rid_length=<decoded-value-length> cid=<correlation-id>]` when a request context has an id. Every `tools/call` also emits one structured JSON line with `event: "mcp.tool.invocation"`, the same opaque `request_id` / `request_id_type` / `request_id_length` tuple, the tool name, elapsed milliseconds, status, result count when available, error metadata, argument keys, and argument lengths. Neither the raw request id nor argument values are logged in this telemetry.

### MCP search pagination
### MCP query pagination

MCP `search` responses include `result_stable_at`, copied from the index freshness timestamp for the database snapshot used by that call. Clients that page through search results should compare `result_stable_at` across calls; if it changes, an intervening index mutation may have shifted the result set and the client should restart pagination.

Non-empty `search` responses also include `next_cursor`. Passing that value back as the `cursor` argument with the same query and filters continues after the last returned `(score, chunk rowid)` anchor. The cursor is an opaque response value; clients should not construct or edit it.

The high-volume discovery tools `symbols`, `files`, and `validate` also accept an opaque `cursor`. Every non-count response reports `returned_count`, `total_count`, `total_count_authoritative`, `remaining_count`, `cursor_offset`, `page_limit`, `has_more`, `result_stable_at`, and `next_cursor`; the final or empty page returns `has_more: false` and `next_cursor: null`. Totals are authoritative for `symbols` and `files`, and for `validate` while `file_issues_data_current` is true. If validation data is unavailable or not current, `validate` reports `total_count_authoritative: false` together with `issues_table_available` and `file_issues_data_current` instead of presenting its synthetic zero as a clean result. Each page reads its generation, total, and rows in one SQLite snapshot, and deterministic ordering lets clients enumerate all rows without gaps or duplicates. The generation includes the persisted monotonic indexed-file write counter, so separately committed indexing batches invalidate older tokens even when they update existing files within the same timestamp second.

Pass `next_cursor` back to the same tool with the exact same filters, `format`, and `limit`. These tokens are stateless and bound to both that normalized query and the index generation. Invalid tokens return `cursor_malformed`, changed arguments return `cursor_query_mismatch`, out-of-range offsets return `cursor_offset_out_of_range`, and an intervening index generation returns `cursor_stale` with the `index_stale` category. In all four cases, discard the token and restart without `cursor`. `countOnly: true` and `format: "count"` do not accept a cursor. The `status` tool publishes the token input bound as `mcp.limits.max_query_cursor_characters`.

### MCP health probes

The MCP JSON-RPC `ping` method returns a structured health object with `status`, `uptime_s`, `last_request_at`, `db_open`, `last_db_check_at`, and `transport_ready`. HTTP MCP transports expose the same object at `GET /healthz` on the existing listener. If the HTTP transport is protected by a bearer token, `/healthz` uses the same `Authorization: Bearer <token>` requirement as POST and `/events`.
Expand Down Expand Up @@ -4168,12 +4172,16 @@ operator は environment variable で既定値を上書きできる。

MCP stderr diagnostic は request context に id がある場合、`[rid=<opaque-token> rid_type=<id-type> rid_length=<decode 後の値長> cid=<correlation-id>]` prefix を付ける。すべての `tools/call` は同じ opaque な `request_id` / `request_id_type` / `request_id_length` tuple、`event: "mcp.tool.invocation"`、tool name、elapsed milliseconds、status、可能な場合の result count、error metadata、argument key、argument length を含む structured JSON line も出す。request id の生値と argument value はこの telemetry に記録しない。

### MCP 検索ページング
### MCP クエリページング

MCP `search` response には、その call が使った DB snapshot の index freshness timestamp からコピーした `result_stable_at` を含める。client が search result を page する場合は、call 間で `result_stable_at` を比較すること。値が変わっていれば、途中の index mutation により result set がずれた可能性があるため、pagination を最初からやり直すべきである。

non-empty な `search` response には `next_cursor` も含める。同じ query と filter でその値を `cursor` argument として渡すと、最後に返した `(score, chunk rowid)` anchor の後から継続する。cursor は opaque な response value であり、client が構築・編集してはいけない。

大量の discovery 結果を返す `symbols`、`files`、`validate` も opaque な `cursor` を受け付ける。count 以外の全 response は `returned_count`、`total_count`、`total_count_authoritative`、`remaining_count`、`cursor_offset`、`page_limit`、`has_more`、`result_stable_at`、`next_cursor` を報告し、最終 page または空 page は `has_more: false` と `next_cursor: null` を返す。`symbols` と `files` の total、および `file_issues_data_current` が true のときの `validate` の total は authoritative である。validation data が利用不能または current でない場合、`validate` は合成した 0 件を clean result として見せず、`issues_table_available` と `file_issues_data_current` とともに `total_count_authoritative: false` を報告する。各 page は generation、total、row を単一 SQLite snapshot で読み、決定的な順序により gap や duplicate なしで全 row を列挙できる。generation には永続的で単調増加する indexed-file write counter を含めるため、同じ timestamp 秒内に既存 file を更新した別 commit の indexing batch でも古い token は invalid になる。

`next_cursor` は、filter、`format`、`limit` を一切変えずに同じ tool へ渡す。token は stateless で、正規化済み query と index generation の両方に束縛される。不正 token は `cursor_malformed`、argument 変更は `cursor_query_mismatch`、範囲外 offset は `cursor_offset_out_of_range`、途中の index generation 変更は `index_stale` category の `cursor_stale` を返す。いずれも token を破棄し、`cursor` なしで最初からやり直す。`countOnly: true` と `format: "count"` は cursor を受け付けない。`status` tool は token 入力上限を `mcp.limits.max_query_cursor_characters` として公開する。

### MCP ヘルスプローブ

MCP JSON-RPC `ping` method は `status`、`uptime_s`、`last_request_at`、`db_open`、`last_db_check_at`、`transport_ready` を持つ structured health object を返す。HTTP MCP transport は同じ object を既存 listener の `GET /healthz` でも公開する。HTTP transport が bearer token で保護されている場合、`/healthz` も POST と `/events` と同じ `Authorization: Bearer <token>` requirement を使う。
Expand Down
2 changes: 2 additions & 0 deletions TESTING_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,7 @@ Use `docs/test-doc-maintenance-plan.md` before moving oversized suites or adding
MCP JSON-RPC behavior and tool outputs. Large server coverage is split into focused partial suites for tool calls, tool listing, protocol/session handling, and error handling while the root `McpServerTests` part keeps shared seeded fixture state. Request-timeout tests use signal-gated delay hooks instead of fixed sleeps: start the request, confirm the hook has begun, then await the timeout response with a bounded wait so they pay only the configured timeout while still proving in-flight actions drain after the timeout response.
The single-request timeout-lease regression uses an ID-specific dispatch signal, a one-second execution timeout for scheduler headroom, typed response-node assertions, and `TestDeterminism.AssertTaskRemainsBlockedAsync` for the queued request. Keep those checks together so full-suite load produces an actionable assertion instead of a null dereference (#4807).
The root seeded database/server fixture is initialized through a thread-safe `Lazy<T>` only when a test accesses its default fixture path. Static helpers and tests that build their own server or transport must not pay schema creation and seed cost; concurrent fixture access must still publish exactly one database/server pair.
High-volume discovery cursor coverage must consume `symbols`, `files`, and `validate` through the final page and assert authoritative totals, deterministic no-gap/no-duplicate enumeration, empty/final-page metadata, bounded opaque tokens, stateless reuse by concurrent server instances, and typed malformed, query-mismatch, and stale-generation failures. Keep an exact qualified Rust symbol query on the total-count path, and prove demoted issue readiness makes `validate` report a non-authoritative zero with explicit table/currentness signals. Seed only the rows needed by that focused partial suite and change the persisted generation before opening a fresh server for stale-token assertions.
Protocol negotiation coverage keeps `2025-06-18`, `2025-03-26`, and `2024-11-05` in one shared version-echo fixture and asserts the exact server-side capability keys for every version. The Codex compatibility regression separately uses the lifecycle-enforcing transport to send a `2025-06-18` initialize, `notifications/initialized`, and `tools/list`, because a direct handler assertion would not catch initialization-gate failures. Transport transcripts must also prove that a second initialize receives `duplicate_initialize` without mutating the session, and that `notifications/initialized` triggers `roots/list` only when the client advertised roots support. Signal-gated coverage must repeat initialized while the first roots response is blocked, prove only one client request starts and teardown drains it, and force both bounded drain deadlines to expire while a late roots write remains blocked to prove stdio resource disposal stays deferred. Release a timeout-delayed initialize worker after its frame cleanup to prove a corrected retry is accepted.
Request-id telemetry coverage uses credential-shaped and high-cardinality ids to prove raw values never reach stderr prefixes/events, Activity tags, MCP metrics, audit records, or timeout logs/status. Assert the fixed opaque token plus consistent type and decoded string-value UTF-16 code-unit length (`null` = `0`), different injected process salts, JSON type domain separation for equal textual values, and collapse to one overflow token after the distinct-id budget (including concurrent creation). Keep a CLI metrics negative case that omits all request-id fields.
Keep failed-then-success initialize isolation in the protocol/session suite: assert caller, client info, roots, capabilities, initialization lifecycle, and session ID after negotiation or success-response serialization failure and again after the corrected handshake, so failed metadata cannot poison the accepted session (#4540). Signal-gated duplicate-initialize coverage must hold a concurrent status reader on its captured snapshot and an in-flight `roots/list` response from the accepted handshake, proving readers see one complete state and a rejected duplicate cannot replace caller metadata or roots.
Expand Down Expand Up @@ -1433,6 +1434,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests"
MCP の JSON-RPC 挙動とツール出力のテスト。大きな server coverage は tool call、tool listing、protocol/session handling、error handling ごとの focused partial suite に分割し、共有の seed 済み fixture 状態は root 側の `McpServerTests` に残します。request-timeout test は固定 sleep ではなく signal-gated delay hook を使います。request を開始し、hook が始まったことを確認してから timeout response を bounded wait で待つことで、timeout response 後に in-flight action が drain されることは保ったまま、設定した timeout 分だけを待つようにします。
single-request の timeout-lease 回帰テストでは、ID 別の dispatch signal、scheduler の余裕を確保する 1 秒の execution timeout、型付き response-node assertion、queue 待ち request に対する `TestDeterminism.AssertTaskRemainsBlockedAsync` を使います。full-suite 負荷でも null 参照ではなく対応可能な assertion を返すよう、これらの検証をまとめて維持してください(#4807)。
root の seed 済み database/server fixture は、test が既定 fixture path へアクセスした場合だけ thread-safe な `Lazy<T>` で初期化します。static helper や独自 server / transport を構築する test は未使用 schema の作成・seed cost を支払わず、並行 fixture access でも database/server pair を必ず1組だけ公開してください。
大量 discovery 用 cursor の coverage では、`symbols`、`files`、`validate` を最終 page まで消費し、authoritative な total、gap・duplicate のない決定的列挙、空・最終 page metadata、上限内の opaque token、並行 server instance による stateless reuse、不正・query mismatch・stale generation の型付き failure を検証してください。total-count 経路には Rust の exact な完全修飾 symbol query も保持し、issue readiness を demote したときは `validate` が table/currentness signal とともに non-authoritative な 0 件を報告することも検証してください。focused partial suite に必要な row だけを seed し、stale token の assertion では永続化 generation を変更してから新しい server を開いてください。
protocol negotiation coverage は `2025-06-18`、`2025-03-26`、`2024-11-05` を共通の version-echo fixture にまとめ、全 version で server-side capability の正確な key を検証します。Codex 互換性の回帰テストでは別途 lifecycle を強制する transport を使い、`2025-06-18` の initialize、`notifications/initialized`、`tools/list` までを送ります。direct handler の assertion だけでは initialization gate の失敗を検出できないためです。transport transcript では、2回目の initialize が session を変更せず `duplicate_initialize` を返すことと、client が roots support を提示した場合だけ `notifications/initialized` の後に `roots/list` を送ることも検証してください。signal-gated coverage では最初の roots response を block したまま initialized を再送し、client request が1件だけ開始され teardown で drain されることを確認します。さらに、遅い roots write を block したまま両 bounded drain deadline を期限切れにし、stdio resource の dispose が引き続き defer されることを証明してください。また、frame cleanup 後に timeout で遅れた initialize worker を解放し、修正済み retry が受理されることを証明してください。
request-id telemetry coverage では credential 風および high-cardinality な id を使い、生値が stderr の prefix / event、Activity tag、MCP metrics、audit record、timeout log / status のどこにも出ないことを検証します。固定長 opaque token と、型および decode 後の string 値の UTF-16 code unit 数(`null` は `0`)が全 surface で一致することに加え、注入した process salt ごとの差、text が同じ JSON type 間の domain separation、concurrent creation を含む distinct-id budget 超過後の単一 overflow token への集約を確認してください。CLI metrics では request-id field をすべて省略する negative case を維持してください。
failed-then-success initialize の分離は protocol/session suite に維持し、交渉失敗または success response の serialization 失敗直後と、修正した handshake 後の caller、client info、roots、capabilities、initialization lifecycle、session ID を検証して、失敗 metadata が受理済み session を汚染できないようにします(#4540)。signal-gated な duplicate-initialize coverage では、並行 status reader を取得済み snapshot で保持し、受理済み handshake の進行中 `roots/list` response を保持することで、reader が完全な 1 state だけを見ることと、拒否された重複 initialize が caller metadata や roots を置き換えられないことも検証します。
Expand Down
31 changes: 31 additions & 0 deletions changelog.d/unreleased/4853.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
category: added
issues:
- 4853
affected:
- src/CodeIndex/Cli/JsonEnvelopeWrapper.Bounded.cs
- src/CodeIndex/Database/DbReader.FilesStatus.cs
- src/CodeIndex/Database/DbReader.cs
- src/CodeIndex/Database/DbSymbolReader.Search.cs
- src/CodeIndex/Database/DbWriter.FoldBackfill.cs
- src/CodeIndex/Mcp/McpToolArgumentContracts.cs
- src/CodeIndex/Mcp/McpToolCatalog.cs
- src/CodeIndex/Mcp/McpToolHandlers.Graph.cs
- src/CodeIndex/Mcp/McpToolHandlers.Pagination.cs
- src/CodeIndex/Mcp/McpToolHandlers.Query.Status.cs
- src/CodeIndex/Mcp/McpToolHandlers.Query.Symbols.cs
- src/CodeIndex/Mcp/McpToolHandlers.cs
- tests/CodeIndex.Tests/McpServerIssue4853Tests.cs
- tests/CodeIndex.Tests/McpServerToolsCallTests.cs
- tests/CodeIndex.Tests/McpToolContractTests.cs
- DEVELOPER_GUIDE.md
- TESTING_GUIDE.md
---

## English

- **High-volume MCP discovery tools now provide stable continuation tokens (#4853)** — `symbols`, `files`, and current `validate` data return authoritative pagination metadata and stateless, generation-bound `next_cursor` values, with deterministic full enumeration and typed errors for malformed, mismatched, or stale tokens. Degraded validation data is explicitly non-authoritative instead of presenting a synthetic zero as clean.

## 日本語

- **大量の結果を返す MCP discovery tool に安定した continuation token を追加しました (#4853)** — `symbols`、`files`、current な `validate` data は authoritative な pagination metadata と generation に束縛された stateless な `next_cursor` を返し、決定的な全件列挙と、不正・不一致・stale token に対する型付き error を提供します。degraded な validation data は合成した 0 件を clean と見せず、明示的に non-authoritative と報告します。
4 changes: 2 additions & 2 deletions src/CodeIndex/Cli/JsonEnvelopeWrapper.Bounded.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1250,7 +1250,7 @@ private static void RemoveOptionWithValue(List<string> args, string option)
}
}

private static string FormatResponseCursor(
internal static string FormatResponseCursor(
int offset,
string queryFingerprint,
string generationFingerprint,
Expand All @@ -1274,7 +1274,7 @@ private static string FormatResponseCursor(
return ResponseCursorPrefix + encoded;
}

private static bool TryParseResponseCursor(
internal static bool TryParseResponseCursor(
string cursor,
out int offset,
out string? queryFingerprint,
Expand Down
Loading
Loading