diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index 96dbd0f9f8..79db3cc4d4 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -2329,6 +2329,10 @@ Piping `{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}` into restart-required error. Its optional `maxBytes` parameter (4,096–1,000,000; default 1,000,000) bounds the full JSON-RPC envelope, and bounded omission and continuation diagnostics are returned under `_meta.response_controls`. + The `initialize` instructions advertise these template and list controls, and + every `resources/list` result publishes the accepted extension parameters and + their bounds under `_meta.discovery_contract`, so AI clients do not need to + infer non-standard protocol extensions. `resources/read` accepts optional inclusive `startLine` / `endLine` ranges and a `maxBytes` UTF-8 text budget (4-byte minimum, 64 KiB by default, 128 KiB maximum). Every page is also capped at 1,000 logical lines. Successful @@ -5090,6 +5094,7 @@ sequenceDiagram - 独立した stdio request と HTTP POST は、設定された MCP request 上限まで並行実行する(#4536)。実行 slot が全て使用中でも read loop は cancellation/client-response frame を受け続ける。accepted-frame backlog は execution 上限 + 64 に別途制限し、超過 request には retry-safe な `-32003` / `server_busy` を返す。request id は protocol/gate 待機前に登録し、execution timeout は slot 取得後に開始し、timeout 後も cancellation を無視して動く action は実際に drain するまで slot を保持する。initialize など session mutation の受信順は protocol barrier で維持し、可変な request state は `AsyncLocal` または request-scoped snapshot に置き、shared writer tool は直列化する。JSON-RPC batch の各 item も同じ global execution slot を個別に消費する(#4545)。基本 `IMcpTransport` loop は outer frame slot を確保しないため、`maxConcurrency: 1` の single request は `_concurrencyGate` を1回だけ取得し、single request と batch item は dispatch 時だけ slot を消費する。 - advertised capability には `tools`、`resources`、`prompts`、`logging` が含まれる。`resources/list` はインデックス済みファイルを `cdidx://file/` URI としてページングし、世代対応の不透明 keyset cursor を返す。ページ間でインデックス済みファイルが変わった場合は、再開必須の stale-index error を明示的に返す。任意の `maxBytes`(4,096〜1,000,000、既定 1,000,000)で JSON-RPC envelope 全体を制限し、省略件数と継続理由を `_meta.response_controls` に有界な形で返す。`resources/read` は inclusive な `startLine` / `endLine` と UTF-8 本文の `maxBytes`(最小 4 byte、既定 64 KiB、最大 128 KiB)を任意指定として受け付ける。各ページは論理行 1,000 行でも上限化される。成功レスポンスは標準の `contents` item を維持し、`result._meta` に実効範囲、返却 byte 数、切り詰め理由、不透明な `nextCursor` を追加する。継続時は行境界を再送せず、その cursor と任意の新しい `maxBytes` を渡す。cursor は index 済みファイル版に結び付くため、resource 変更後は stale として失敗する。database reader は長い単一行を含め、managed response string を構築する前に incremental SQLite BLOB read で範囲と byte 上限を適用する。server は MCP レスポンス上限と active transport のレスポンス上限のうち小さい方から実効本文 budget を算出し、JSON-RPC envelope と最悪ケースの JSON escape に必要な領域を確保する。1 つの JSON-RPC batch に複数の `resources/read` call がある場合は aggregate frame 上限を共有し、各 item を frame の残り領域に合わせて budget 化する。page 化できない item が割当内に収まらない場合は、元の request ID を保持した構造化 `batch_response_budget_too_small` error に置換する。file metadata の取得、cursor 検証、chunk BLOB 読み取りは単一の deferred SQLite read snapshot 内で実行するため、並行 reindex によって異なる resource 版が混在しない。実際に空の index 済みファイルは空の成功レスポンスを返すが、非空 resource の content 欠落、chunk coverage の不足、安全上限を超える chunk topology は部分的または空の成功として返さず、構造化された `index_missing`、`index_stale`、`index_corrupted` error として失敗する。専用の range partial index がない read-only または immutable な legacy database では、既存の `idx_chunks_file` index を使い、SQLite VM-step budget 内で metadata-only の predecessor / candidate query を実行する。budget 超過時は無制限に scan せず、構造化された `resource_bounded_read_index_unavailable` を返す。stable reason には `resource_content_unavailable`、`resource_bounded_read_index_unavailable`、`resource_chunk_coverage_incomplete`、`chunk_limit_exceeded`、`chunk_candidate_scan_limit_exceeded`、`resource_file_metadata_inconsistent`、`resource_chunk_topology_invalid`、`scan_limit_exceeded` がある。`logging` は MCP `notifications/message` を示し、`logging/setLevel` は `debug`、`info`、`notice`、`warning`、`error`、`critical`、`alert`、`emergency` を受け付ける。 - `resources/templates/list` は正確な既知 path の直接解決用に `cdidx://file-path/{path}` を公開し、成功した read は canonical な `cdidx://file/` identity を返す。`resources/list` の `path`、`lang`、`includeGenerated` filter は server-side で有界に適用され、継続 cursor は canonical filter と generation の両方に結び付く。generated file は既定で list と read から除外され、明示的な `includeGenerated: true` が必要になる。 +- `initialize` の `instructions` はこれらの resource template / list control を直接案内し、各 `resources/list` response は accepted extension parameter と上限を `_meta.discovery_contract` に公開する。これにより AI client は標準外の protocol extension を推測する必要がない。 - `protocolVersion` は**ハードコードではなく交渉**で決まる(#1554)。サーバーは `McpServer.SupportedProtocolVersions`(新しい順: `2025-06-18`, `2025-03-26`, `2024-11-05`)を保持し、`initialize` パラメータから diff --git a/USER_GUIDE.md b/USER_GUIDE.md index fac0d00ea2..6cba3c54fa 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -2759,6 +2759,12 @@ cdidx exposes separate JSON-RPC integration surfaces with different framing and MCP file-resource discovery can avoid a repository-wide inventory scan. `resources/templates/list` advertises `cdidx://file-path/{path}`; expand `path` as one repository-relative string, then pass that URI to `resources/read`. The template safely percent-encodes separators and reserved filename characters, while a successful read returns the canonical `cdidx://file/` identity. For browsing, `resources/list` accepts one `path` string or an array of up to 100 path filters, a normalized `lang`, and `includeGenerated` (default `false`). Its continuation cursor is bound to both the index generation and the canonical filters, so changing filters requires restarting without the cursor. Generated resources also require `includeGenerated: true` when read directly. +The MCP `initialize` instructions advertise these template and browsing controls +in-band. Every `resources/list` result also publishes the accepted extension +parameters and bounds under `_meta.discovery_contract`, so AI clients can discover +`path`, `lang`, `includeGenerated`, `maxBytes`, and cursor semantics without +guessing beyond the standard protocol. + MCP `resources/read` is bounded too. Pass optional inclusive `startLine` / `endLine` values and `maxBytes` (the UTF-8 resource-text budget); the minimum is 4 bytes, omitted budgets default to 64 KiB, and requests cannot exceed 128 KiB. Each page is also capped at 1,000 logical lines. `result._meta.truncationReason` is `maxLines` when that cap is reached and `maxBytes` when the requested text budget is reached. If the configured MCP or HTTP response ceiling is tighter, `_meta.maxBytes` retains the requested budget, `_meta.effectiveMaxBytes` reports the reduced budget, and truncation uses `maxResponseBytes`. Multiple `resources/read` calls in one JSON-RPC batch share the aggregate frame ceiling, so each item also yields to the space remaining in that batch. A non-pageable item that cannot fit its allocation returns a structured `batch_response_budget_too_small` error while preserving the request ID. When `result._meta.truncated` is true, send the returned `nextCursor` with the same resource URI to continue. Do not combine a cursor with new line boundaries; you may change `maxBytes` for the next page. A cursor becomes stale if the indexed resource changes. Read-only or immutable legacy databases without the dedicated range indexes use the existing `idx_chunks_file` index for a metadata-only compatibility lookup under a SQLite VM-step budget; exceeding that budget returns a structured index-unavailable error instead of performing an unbounded scan. ### MCP Server (for Claude Code, Cursor, Windsurf, etc.) @@ -5965,6 +5971,12 @@ cdidx は framing と制限が異なる JSON-RPC integration surface を分け MCP file-resource discovery ではリポジトリ全体の inventory scan を回避できます。`resources/templates/list` は `cdidx://file-path/{path}` を公開します。`path` を 1 つのリポジトリ相対文字列として展開し、その URI を `resources/read` に渡してください。template は separator と予約 filename 文字を安全に percent encode し、成功した read は canonical な `cdidx://file/` identity を返します。browse 用の `resources/list` は 1 つの `path` 文字列または最大 100 件の path filter 配列、正規化される `lang`、`includeGenerated`(既定 `false`)を受け付けます。継続 cursor は index generation と canonical filter の両方に結び付くため、filter を変更する場合は cursor を省略して再開してください。generated resource の direct read にも `includeGenerated: true` が必要です。 +MCP `initialize` の `instructions` はこれらの template / browse control を +protocol 上で直接案内します。各 `resources/list` result も accepted extension +parameter と上限を `_meta.discovery_contract` に公開するため、AI client は標準 +protocol の外側にある `path`、`lang`、`includeGenerated`、`maxBytes`、cursor +semantics を推測せず発見できます。 + MCP `resources/read` にも上限があります。inclusive な `startLine` / `endLine` と、UTF-8 resource 本文の budget である `maxBytes` を任意指定できます。最小値は 4 byte、budget 省略時は 64 KiB、要求可能な最大値は 128 KiB です。各ページには論理行 1,000 行の上限もあります。この上限に達した場合、`result._meta.truncationReason` は `maxLines`、要求した本文 budget に達した場合は `maxBytes` になります。設定された MCP または HTTP のレスポンス上限の方が小さい場合、`_meta.maxBytes` は要求値を保持し、`_meta.effectiveMaxBytes` が縮小後の budget を示し、切り詰め理由は `maxResponseBytes` になります。1 つの JSON-RPC batch に複数の `resources/read` call がある場合は aggregate frame 上限を共有するため、各 item はその batch の残り領域にも従います。page 化できない item が割当内に収まらない場合は、request ID を保持した構造化 `batch_response_budget_too_small` error を返します。`result._meta.truncated` が true の場合、返された `nextCursor` を同じ resource URI とともに送って継続してください。cursor と新しい行境界は併用できませんが、次ページの `maxBytes` は変更できます。index 済み resource が変わると cursor は stale になります。専用の range index がない read-only または immutable な legacy database では、既存の `idx_chunks_file` index を使い、SQLite VM-step budget 内の metadata-only compatibility lookup を行います。この budget を超えた場合は無制限に scan せず、構造化された index-unavailable error を返します。 ### MCP サーバー(Claude Code、Cursor、Windsurf 等に対応) diff --git a/changelog.d/unreleased/+mcp-resource-discovery-guidance.fixed.md b/changelog.d/unreleased/+mcp-resource-discovery-guidance.fixed.md new file mode 100644 index 0000000000..2e5a398754 --- /dev/null +++ b/changelog.d/unreleased/+mcp-resource-discovery-guidance.fixed.md @@ -0,0 +1,18 @@ +--- +category: fixed +affected: + - src/CodeIndex/Mcp/McpToolHandlers.Instructions.cs + - src/CodeIndex/Mcp/McpServer.Resources.cs + - tests/CodeIndex.Tests/McpServerProtocolTests.cs + - tests/CodeIndex.Tests/McpServerTests.cs + - USER_GUIDE.md + - DEVELOPER_GUIDE.md +--- + +## English + +- **MCP file-resource controls are now discoverable in-band** — `initialize` instructions now explain exact-path templates and filtered `resources/list` usage, while each list response publishes accepted parameters, bounds, and cursor semantics under `_meta.discovery_contract`, so AI clients can use current resource discovery without guessing non-standard protocol extensions. + +## 日本語 + +- **MCP の file-resource control を protocol 上で発見できるようになりました** — `initialize` の instructions が exact-path template と filter 付き `resources/list` の使い方を案内し、各 list response が accepted parameter、上限、cursor semantics を `_meta.discovery_contract` に公開するため、AI client は標準外の protocol extension を推測せず最新の resource discovery を利用できます。 diff --git a/src/CodeIndex/Mcp/McpServer.Resources.cs b/src/CodeIndex/Mcp/McpServer.Resources.cs index 184da7ce60..1020285350 100644 --- a/src/CodeIndex/Mcp/McpServer.Resources.cs +++ b/src/CodeIndex/Mcp/McpServer.Resources.cs @@ -282,6 +282,7 @@ private static JsonObject CreateResourceListResponse( ["resources"] = resources, ["_meta"] = new JsonObject { + ["discovery_contract"] = CreateResourceListDiscoveryContract(), ["response_controls"] = CreateResourceListResponseControls( requestedMaxBytes, effectiveMaxBytes, @@ -298,6 +299,53 @@ private static JsonObject CreateResourceListResponse( return CreateSuccessResponse(true, id, result); } + private static JsonObject CreateResourceListDiscoveryContract() + => new() + { + ["accepted_params"] = new JsonArray + { + "cursor", + "path", + "lang", + "includeGenerated", + "maxBytes", + }, + ["filter_params"] = new JsonArray + { + "path", + "lang", + "includeGenerated", + }, + ["path_filter"] = new JsonObject + { + ["type"] = "string_or_array", + ["max_items"] = MaxResourceListPathFilterCount, + ["max_characters_per_item"] = MaxResourceListPathFilterChars, + ["max_wildcards_per_item"] = MaxResourceListPathFilterWildcards, + }, + ["language_filter"] = new JsonObject + { + ["type"] = "normalized_language_name_or_alias", + ["max_characters"] = MaxResourceListLanguageFilterChars, + }, + ["generated_files_excluded_by_default"] = true, + ["max_bytes"] = new JsonObject + { + ["scope"] = "json_rpc_envelope", + ["minimum"] = MinResourceListMaxBytes, + ["default"] = DefaultResourceListMaxBytes, + ["maximum"] = MaxResourceListMaxBytes, + }, + ["pagination"] = new JsonObject + { + ["cursor_param"] = "params.cursor", + ["next_cursor_field"] = "result.nextCursor", + ["cursor_is_opaque"] = true, + ["cursor_binds_index_generation"] = true, + ["cursor_binds_filters"] = true, + }, + }; + private static JsonObject CreateResourceListResponseControls( int requestedMaxBytes, int effectiveMaxBytes, diff --git a/src/CodeIndex/Mcp/McpToolHandlers.Instructions.cs b/src/CodeIndex/Mcp/McpToolHandlers.Instructions.cs index 7eaad7a781..aacf6b1eaa 100644 --- a/src/CodeIndex/Mcp/McpToolHandlers.Instructions.cs +++ b/src/CodeIndex/Mcp/McpToolHandlers.Instructions.cs @@ -53,6 +53,8 @@ bool All(params string[] names) parts.Add("Investigation flow: search broadly, use definition for declarations, references for usage sites, callers/callees for call graph impact, outline/map for structure, then excerpt or resources/read for focused line ranges. Prefer pagination, path/lang filters, exactName/exactSubstring, and prefix over dumping large files. 調査順序: まず広く search し、宣言は definition、利用箇所は references、呼び出し影響は callers/callees、構造把握は outline/map、その後に excerpt または resources/read で必要な行範囲だけを読んでください。大きなファイルを丸ごと読む前に pagination、path/lang filter、exactName/exactSubstring、prefix で絞り込んでください。"); } + parts.Add("For an exact known repository-relative path, expand the cdidx://file-path/{path} template from resources/templates/list and pass that URI to resources/read. 正確なリポジトリ相対 path が分かっている場合は、resources/templates/list の cdidx://file-path/{path} template を展開し、その URI を resources/read に渡してください。"); + parts.Add("For browsing, call resources/list with optional path, lang, includeGenerated, and maxBytes; continue with result.nextCursor and unchanged filters. Generated files are excluded by default. browse する場合は resources/list に任意の path、lang、includeGenerated、maxBytes を渡し、同じ filter のまま result.nextCursor で継続してください。generated file は既定で除外されます。"); parts.Add("For resources/read, use startLine/endLine and maxBytes, then continue truncated reads with result._meta.nextCursor. resources/read では startLine/endLine と maxBytes を使い、切り詰められた場合は result._meta.nextCursor で継続してください。"); if (On("analyze_symbol")) diff --git a/tests/CodeIndex.Tests/McpServerProtocolTests.cs b/tests/CodeIndex.Tests/McpServerProtocolTests.cs index a17c594f5b..d832ed84a1 100644 --- a/tests/CodeIndex.Tests/McpServerProtocolTests.cs +++ b/tests/CodeIndex.Tests/McpServerProtocolTests.cs @@ -1004,6 +1004,12 @@ public void Initialize_ReturnsInstructions() Assert.Contains("search", instructions); Assert.Contains("CodeIndex MCP tools", instructions); Assert.Contains("grep/find/cat", instructions); + Assert.Contains("resources/templates/list", instructions); + Assert.Contains("cdidx://file-path/{path}", instructions); + Assert.Contains("resources/list", instructions); + Assert.Contains("optional path, lang, includeGenerated, and maxBytes", instructions); + Assert.Contains("result.nextCursor", instructions); + Assert.Contains("unchanged filters", instructions); Assert.Contains("resources/read", instructions); Assert.Contains("startLine/endLine", instructions); Assert.Contains("maxBytes", instructions); diff --git a/tests/CodeIndex.Tests/McpServerTests.cs b/tests/CodeIndex.Tests/McpServerTests.cs index 3a36b310d5..37f1276a28 100644 --- a/tests/CodeIndex.Tests/McpServerTests.cs +++ b/tests/CodeIndex.Tests/McpServerTests.cs @@ -1621,10 +1621,34 @@ public void ResourcesList_ReturnsIndexedFilesAsResources() var request = JsonNode.Parse("""{"jsonrpc":"2.0","id":1,"method":"resources/list","params":{}}""")!; var response = _server.HandleMessage(request)!; - var resource = response["result"]!["resources"]!.AsArray() + var result = response["result"]!; + var resource = result["resources"]!.AsArray() .Single(r => r!["name"]!.GetValue() == "src/app.cs")!; Assert.Equal("cdidx://file/src/app.cs", resource["uri"]!.GetValue()); Assert.Equal("text/x-csharp", resource["mimeType"]!.GetValue()); + + var discovery = result["_meta"]!["discovery_contract"]!; + Assert.Equal( + ["cursor", "path", "lang", "includeGenerated", "maxBytes"], + discovery["accepted_params"]!.AsArray().Select(item => item!.GetValue())); + Assert.Equal( + ["path", "lang", "includeGenerated"], + discovery["filter_params"]!.AsArray().Select(item => item!.GetValue())); + Assert.Equal(McpServer.MaxResourceListPathFilterCount, discovery["path_filter"]!["max_items"]!.GetValue()); + Assert.Equal(McpServer.MaxResourceListPathFilterChars, discovery["path_filter"]!["max_characters_per_item"]!.GetValue()); + Assert.Equal(McpServer.MaxResourceListPathFilterWildcards, discovery["path_filter"]!["max_wildcards_per_item"]!.GetValue()); + Assert.Equal("normalized_language_name_or_alias", discovery["language_filter"]!["type"]!.GetValue()); + Assert.Equal(McpServer.MaxResourceListLanguageFilterChars, discovery["language_filter"]!["max_characters"]!.GetValue()); + Assert.True(discovery["generated_files_excluded_by_default"]!.GetValue()); + Assert.Equal("json_rpc_envelope", discovery["max_bytes"]!["scope"]!.GetValue()); + Assert.Equal(McpServer.MinResourceListMaxBytes, discovery["max_bytes"]!["minimum"]!.GetValue()); + Assert.Equal(McpServer.DefaultResourceListMaxBytes, discovery["max_bytes"]!["default"]!.GetValue()); + Assert.Equal(McpServer.MaxResourceListMaxBytes, discovery["max_bytes"]!["maximum"]!.GetValue()); + Assert.Equal("params.cursor", discovery["pagination"]!["cursor_param"]!.GetValue()); + Assert.Equal("result.nextCursor", discovery["pagination"]!["next_cursor_field"]!.GetValue()); + Assert.True(discovery["pagination"]!["cursor_is_opaque"]!.GetValue()); + Assert.True(discovery["pagination"]!["cursor_binds_index_generation"]!.GetValue()); + Assert.True(discovery["pagination"]!["cursor_binds_filters"]!.GetValue()); } [Fact]