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
2 changes: 2 additions & 0 deletions DEVELOPER_GUIDE.md

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,12 @@ versioned surfaces are the `cdidx` CLI, CLI JSON output, and `cdidx mcp`
JSON-RPC interface. There is no public library / SDK API. See
[INTEGRATION_POLICY.md](INTEGRATION_POLICY.md#api-surface-and-library-use).

The MCP `outline` tool supports CLI-compatible `fields` and `sort` controls,
bounded `limit` / `cursor` pages, and a `maxBytes` UTF-8 budget. Its opaque
continuation is bound to the path, ordering, and index generation so large file
outlines can be enumerated without gaps or duplicates. See
[AI Integration](USER_GUIDE.md#ai-integration).

## CLI JSON Error Contract

Recoverable non-database failures from commands such as `outline`, `hooks`,
Expand Down Expand Up @@ -698,6 +704,12 @@ commit し、構造化 `file_errors` を返して partial-result 終了コード
JSON-RPC interface です。公開 library / SDK API は提供していません。詳細は
[INTEGRATION_POLICY.md](INTEGRATION_POLICY.md#api-surface-and-library-use) を参照してください。

MCP の `outline` tool は、CLI 互換の `fields` / `sort`、上限付きの
`limit` / `cursor` page、UTF-8 byte 上限の `maxBytes` に対応します。
opaque な continuation は path、並び順、index generation に束縛されるため、
大きな file outline も gap や duplicate なしで列挙できます。詳細は
[AIとの連携](USER_GUIDE.md#aiとの連携) を参照してください。

## CLI JSON エラー契約

`outline`、`hooks`、`doctor`、`validate-config` などの回復可能な
Expand Down
2 changes: 2 additions & 0 deletions TESTING_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,7 @@ Use `docs/test-doc-maintenance-plan.md` before moving oversized suites or adding
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.
`McpServerOutlinePaginationIssue4897Tests.cs` owns the standalone large-file `outline` contract. Keep its 175-row deep tree, empty file, projected aliases, stable source/name ordering, exact UTF-8 byte boundary, no-progress budget error, and stale-generation replay checks together. The suite intentionally uses an isolated database and the `SQLite pool sensitive` collection because it closes one server, mutates the persisted generation, and opens another to prove cursor invalidation.
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 @@ -1504,6 +1505,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests"
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 を開いてください。
`McpServerOutlinePaginationIssue4897Tests.cs` は独立した大規模 file の `outline` 契約を担当します。175 row の深い tree、空 file、projection alias、安定した source/name 順、正確な UTF-8 byte 境界、進捗不能 budget error、stale-generation replay の検証を同じ suite に維持してください。この suite は一方の server を閉じ、永続化 generation を変更して別の server で cursor invalidation を証明するため、独立 database と `SQLite pool sensitive` collection を意図的に使います。
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
25 changes: 25 additions & 0 deletions USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2933,6 +2933,19 @@ Capped MCP result tools report `truncated` and `more_available` in `structuredCo

Graph tools that can page through result sets (`references`, `callers`, and `callees`) also return `offset` and, when truncated, `next_offset`; pass that value back as `offset` to fetch the next page without re-reading earlier rows.

The MCP `outline` tool pages independently with the same canonical symbol rows,
stable ordering, projection names, and `page:v1` cursor fingerprint as CLI
`outline`. It returns at most 100 symbols by default (200 maximum) and publishes
`total_symbol_count`, `returned_symbol_count`, `cursor_offset`, `has_more`, and
`next_cursor`. Use `fields`, `sort`, `limit`, and `cursor` to enumerate large
files deterministically. `maxBytes` additionally bounds serialized
`structuredContent` in UTF-8 bytes by removing only complete trailing symbol
rows and advancing the cursor by the rows actually returned. Cursors are bound
to the path, ordering, and index generation; restart without the cursor after a
stale-generation error. Unprojected rows retain `depth`, `containerName`, and
symbol `path`; include the corresponding projection fields when a projected
page must carry hierarchy information.

```mermaid
flowchart LR
tools["Claude Code<br/>Cursor<br/>Windsurf"]
Expand Down Expand Up @@ -6157,6 +6170,18 @@ basename candidate だけを確認し、見つからなければ unresolved docu

ページング可能な graph tool(`references`、`callers`、`callees`)は `offset` と、truncated 時には `next_offset` も返すため、その値を次の呼び出しの `offset` に渡すと、既に取得した行を読み直さずに次ページを取得できます。

MCP の `outline` tool は、CLI `outline` と同じ canonical symbol row、安定した
並び順、projection 名、`page:v1` cursor fingerprint を使って独立にページングします。
既定では最大 100 symbol、上限は 200 symbol で、`total_symbol_count`、
`returned_symbol_count`、`cursor_offset`、`has_more`、`next_cursor` を返します。
大きな file を決定的に列挙するには `fields`、`sort`、`limit`、`cursor` を使います。
`maxBytes` を指定すると、serialize 済み `structuredContent` の UTF-8 byte 数を上限とし、
末尾の完全な symbol row だけを除き、実際に返した row 数だけ cursor を進めます。
cursor は path、並び順、index generation に束縛されるため、stale-generation error の後は
cursor なしで再開してください。projection なしの row は `depth`、`containerName`、
symbol `path` を保持します。projection した page に階層情報が必要な場合は、対応する
projection field を含めてください。

```mermaid
flowchart LR
tools["Claude Code<br/>Cursor<br/>Windsurf"]
Expand Down
26 changes: 26 additions & 0 deletions changelog.d/unreleased/4897.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
category: added
issues:
- 4897
affected:
- src/CodeIndex/Cli/QueryCommandRunner.Outline.cs
- src/CodeIndex/Mcp/McpToolHandlers.Query.Source.cs
- src/CodeIndex/Mcp/McpToolCatalog.cs
- src/CodeIndex/Mcp/McpToolArgumentContracts.cs
- src/CodeIndex/Mcp/McpToolHandlers.ArgumentValidation.cs
- tests/CodeIndex.Tests/McpServerOutlinePaginationIssue4897Tests.cs
- tests/CodeIndex.Tests/McpServerToolsListTests.cs
- tests/CodeIndex.Tests/McpToolContractTests.cs
- README.md
- USER_GUIDE.md
- DEVELOPER_GUIDE.md
- TESTING_GUIDE.md
---

## English

- **MCP `outline` can now page and project large file trees deterministically (#4897)** — The tool exposes CLI-compatible `fields` and `sort`, bounded `limit` / `cursor` pages, and a UTF-8 `maxBytes` budget. Opaque continuations reuse the outline query and index-generation fingerprint, preserve complete hierarchy-bearing symbol rows, and advance by the rows actually returned so large outlines enumerate without gaps or duplicates.

## 日本語

- **MCP `outline` で大きな file tree を決定的に projection / pagination できるようになりました (#4897)** — CLI 互換の `fields` / `sort`、上限付きの `limit` / `cursor` page、UTF-8 の `maxBytes` budget を公開します。opaque な continuation は outline query と index generation の fingerprint を再利用し、階層情報を持つ完全な symbol row を維持しながら実際の返却件数だけ進むため、gap や duplicate なしで大きな outline を列挙できます。
101 changes: 101 additions & 0 deletions src/CodeIndex/Cli/QueryCommandRunner.Outline.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ namespace CodeIndex.Cli;

public static partial class QueryCommandRunner
{
internal sealed record OutlinePageBuildResult(
JsonObject? Payload,
OutlineResult? Outline,
string? Error,
bool NotFound,
IReadOnlyList<OutlineSymbol>? PageSymbols = null);

public static int RunOutline(string[] cmdArgs, JsonSerializerOptions jsonOptions)
{
var wantsJson = cmdArgs.Any(static arg =>
Expand Down Expand Up @@ -201,6 +208,100 @@ public static int RunOutline(string[] cmdArgs, JsonSerializerOptions jsonOptions
});
}

internal static bool TryNormalizeOutlineProjectionFields(
string rawValue,
out List<string>? fields,
out string? error)
{
var errors = new List<string>();
fields = ParseOutlineProjectionFields(rawValue, errors.Add);
error = errors.Count == 0
? null
: errors[0]
.Replace("Error: ", string.Empty, StringComparison.Ordinal)
.Replace("--outline-fields", "fields", StringComparison.Ordinal);
return error == null;
}

internal static OutlinePageBuildResult BuildOutlinePage(
DbReader reader,
string filePath,
IReadOnlyList<string>? fields,
bool fieldsExplicit,
string? requestedSort,
int limit,
string? cursor,
JsonSerializerOptions jsonOptions)
{
var sortExplicit = !string.IsNullOrWhiteSpace(requestedSort);
var outlineSortMode = OutlineSortMode.Source;
if (sortExplicit && !TryParseOutlineSortMode(requestedSort!, out outlineSortMode))
{
return new(
null,
null,
"sort must be one of source, name, kind, references, size, complexity, or path.",
NotFound: false);
}

int? cursorOffset = null;
if (cursor != null)
{
if (!TryParseScopedOffsetCursor(cursor, out var parsedCursor)
|| !string.Equals(parsedCursor.Scope, "outline", StringComparison.Ordinal))
{
return new(
null,
null,
"cursor must be an outline pagination cursor; restart without cursor.",
NotFound: false);
}
cursorOffset = parsedCursor.Offset;
}

var options = new QueryCommandOptions
{
Json = true,
Limit = limit,
LimitExplicit = true,
OutlineFields = fields?.ToList(),
OutlineFieldsExplicit = fieldsExplicit,
SortValue = requestedSort,
SortExplicit = sortExplicit,
CursorValue = cursor,
OutlineCursorOffset = cursorOffset,
};
var includeReferenceCounts = OutlineNeedsReferenceCounts(options, outlineSortMode);
var includeDerivedMetadata = OutlineNeedsDerivedMetadata(options, outlineSortMode);
var cursorComponents = new List<string?>
{
filePath,
FormatOutlineSortMode(outlineSortMode),
};
var cursorContext = BuildPaginationCursorContext(reader, "outline", cursorComponents);
var cursorValidationError = ValidateScopedOffsetCursor(options, "outline", cursorContext);
if (cursorValidationError != null)
return new(null, null, cursorValidationError, NotFound: false);

var outline = reader.GetOutline(filePath, includeReferenceCounts: includeReferenceCounts);
if (outline == null)
return new(null, null, null, NotFound: true);

var displaySymbols = ApplyOutlineSort(outline.Symbols, outlineSortMode, includeDerivedMetadata);
var pageOffset = Math.Min(cursorOffset ?? 0, displaySymbols.Count);
var pageSymbols = displaySymbols.Skip(pageOffset).Take(limit).ToList();
var payload = BuildOutlineJsonPayload(
outline,
displaySymbols,
[],
outlineSortMode,
options,
cursorContext,
jsonOptions,
compact: false);
return new(payload, outline, null, NotFound: false, pageSymbols);
}

private static JsonObject ApplyOutlineCompactCaps(OutlineResult outline, int sectionLimit)
=> ApplyOutlineSymbolLimit(outline, sectionLimit);

Expand Down
Loading
Loading