From e16ae55a4c234f2203fd597192346b7c244f3b61 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 13 Jun 2026 22:32:54 +0900 Subject: [PATCH] Improve MCP self-guiding metadata --- .../unreleased/+mcp-self-guiding.changed.md | 16 ++++ src/CodeIndex/Mcp/McpServer.cs | 36 +++++++++ src/CodeIndex/Mcp/McpToolDefinitions.cs | 77 +++++++++++++++---- src/CodeIndex/Mcp/McpToolHandlers.cs | 71 +++++++++++++++-- tests/CodeIndex.Tests/McpServerTests.cs | 66 ++++++++++++++++ 5 files changed, 243 insertions(+), 23 deletions(-) create mode 100644 changelog.d/unreleased/+mcp-self-guiding.changed.md diff --git a/changelog.d/unreleased/+mcp-self-guiding.changed.md b/changelog.d/unreleased/+mcp-self-guiding.changed.md new file mode 100644 index 0000000000..62bfab0894 --- /dev/null +++ b/changelog.d/unreleased/+mcp-self-guiding.changed.md @@ -0,0 +1,16 @@ +--- +category: changed +affected: + - src/CodeIndex/Mcp/McpServer.cs + - src/CodeIndex/Mcp/McpToolDefinitions.cs + - src/CodeIndex/Mcp/McpToolHandlers.cs + - tests/CodeIndex.Tests/McpServerTests.cs +--- + +## English + +- **MCP guidance is now more self-guiding for code investigation** — initialize instructions, tool descriptions, schema descriptions, prompts, and read-only response hints now steer clients toward CodeIndex-first search, focused excerpts, symbol navigation, graph-aware impact checks, and recovery steps when a query misses. + +## 日本語 + +- **MCP のコード調査ガイダンスをより自己誘導的にしました** — initialize instructions、tool descriptions、schema descriptions、prompts、read-only response hints が、CodeIndex-first な検索、焦点を絞った抜粋、シンボル移動、graph-aware な影響確認、検索失敗時の recovery steps へ誘導するようになりました。 diff --git a/src/CodeIndex/Mcp/McpServer.cs b/src/CodeIndex/Mcp/McpServer.cs index b024ecc76d..b363b9ed29 100644 --- a/src/CodeIndex/Mcp/McpServer.cs +++ b/src/CodeIndex/Mcp/McpServer.cs @@ -2211,6 +2211,10 @@ private JsonNode HandlePromptsList(JsonNode? id) CreatePromptDefinition("summarize_file", "Summarize the API surface and responsibilities of an indexed file.", "path", "Indexed file path to summarize."), CreatePromptDefinition("find_unused", "Find likely unused symbols in an optional language or path scope.", "scope", "Optional language, module, or path scope."), CreatePromptDefinition("impact_of_changing", "Plan impact analysis for changing a symbol.", "symbol", "Symbol name to analyze."), + CreatePromptDefinition("investigate_before_edit", "Investigate relevant code before making edits.", "topic", "Optional feature, symbol, file, or behavior to investigate."), + CreatePromptDefinition("find_existing_pattern", "Find existing implementation and test patterns before adding code.", "topic", "Optional API, behavior, module, or feature pattern to search for."), + CreatePromptDefinition("safe_symbol_change", "Plan a safe symbol rename or behavior change using graph-aware tools.", "symbol", "Symbol or behavior being changed."), + CreatePromptDefinition("debug_failure", "Debug a failing build, test, or runtime error using indexed evidence.", "failure", "Optional error text, test name, or failing behavior."), }; return CreateSuccessResponse(true, id, new JsonObject { ["prompts"] = prompts }); } @@ -2277,6 +2281,38 @@ private JsonNode HandlePromptsGet(JsonNode? id, JsonNode? getParams) text = $"Use `impact_analysis` for `{symbol ?? ""}`. Summarize direct callers, transitive callers, and files that likely need tests."; break; } + case "investigate_before_edit": + { + var topic = ReadArg("topic", out var argumentError); + if (argumentError is not null) + return argumentError; + text = $"Before editing `{topic ?? ""}`, use `map` for orientation if needed, `search` for broad discovery, `symbols` or `definition` for declarations, `references` for usage and tests, and focused `excerpt` calls for only the relevant ranges."; + break; + } + case "find_existing_pattern": + { + var topic = ReadArg("topic", out var argumentError); + if (argumentError is not null) + return argumentError; + text = $"Find existing patterns for `{topic ?? ""}` with `search` and `symbols`, inspect representative files with `outline`, then use focused `excerpt` ranges from implementation and tests before adding new code."; + break; + } + case "safe_symbol_change": + { + var symbol = ReadArg("symbol", out var argumentError); + if (argumentError is not null) + return argumentError; + text = $"For `{symbol ?? ""}`, confirm identity with `definition` or `symbols exactName:true`, inspect `references`, `callers`, and `callees`, then read focused `excerpt` ranges for declarations, call sites, and tests before changing behavior or names."; + break; + } + case "debug_failure": + { + var failure = ReadArg("failure", out var argumentError); + if (argumentError is not null) + return argumentError; + text = $"Debug `{failure ?? ""}` by searching exact error text with `search` or `exactSubstring`, finding related symbols with `definition` and `references`, checking callers/callees for the failing path, and reading focused `excerpt` ranges before proposing a fix."; + break; + } default: return CreateUnknownPromptError(id, name); } diff --git a/src/CodeIndex/Mcp/McpToolDefinitions.cs b/src/CodeIndex/Mcp/McpToolDefinitions.cs index de8bc56a3a..32d3a3e3e4 100644 --- a/src/CodeIndex/Mcp/McpToolDefinitions.cs +++ b/src/CodeIndex/Mcp/McpToolDefinitions.cs @@ -20,7 +20,7 @@ private JsonNode HandleToolsList(JsonNode? id) { CreateToolDefinition( "search", - "Full-text search across indexed code chunks, or list/run named search audit recipes. Returns match-centered snippets with line metadata plus `result_stable_at` for index-drift checks, `next_cursor` for non-empty paginated query responses, and `next_step_suggestion` or `recovery_hint`. Use `listRecipes:true` to list recipes and `recipe:\"name\"` to run one. Use `prefix` or trailing `*` to widen token matching, `rawQuery` for FTS5 syntax, and `exactSubstring` for case-sensitive text identity. Details and examples: USER_GUIDE.md#search. / インデックス済みコードチャンクの全文検索、または名前付き search audit recipe の一覧・実行。レスポンスには index drift 検出用の `result_stable_at`、非空ページ継続用の `next_cursor`、`next_step_suggestion` または `recovery_hint` を含める。`listRecipes:true` で recipe 一覧、`recipe:\"name\"` で実行。`prefix` / 末尾 `*` / `rawQuery` / `exactSubstring` の詳細と例は USER_GUIDE.md#search を参照。", + "Use this when starting broad code discovery, checking error text, or running named search audit recipes. Prefer it before shell grep; common next step is `excerpt`, `definition`, or `references` on the best hit. Returns snippets plus `result_stable_at`, `next_cursor`, and `next_step_suggestion` or `recovery_hint`. Use `prefix`/trailing `*` to widen token matching, `rawQuery` for FTS5 syntax, and `exactSubstring` for case-sensitive identity. Details and examples: USER_GUIDE.md#search. / 広いコード調査、エラー文言確認、search audit recipe 実行の起点に使う。shell grep より優先し、次は最有力ヒットに `excerpt` / `definition` / `references` を使う。`prefix` / 末尾 `*` / `rawQuery` / `exactSubstring` の詳細と例は USER_GUIDE.md#search を参照。", new JsonObject { ["type"] = "object", @@ -63,7 +63,7 @@ private JsonNode HandleToolsList(JsonNode? id) ReadOnlyAnnotations()), CreateToolDefinition( "definition", - "Resolve symbol definitions with definition ranges, signatures, and optional body content. Pass `lsp_compatible:true` to add `uri` and LSP `range` fields to each result. For exact matches, use `exactName`; `exact` is the legacy alias documented in USER_GUIDE.md's flag compatibility table. Examples: `definition {\"query\":\"McpServer\"}`; `definition {\"query\":\"HandleMessage\",\"lang\":\"csharp\",\"includeBody\":true,\"exactName\":true}`. / 定義範囲、シグネチャ、必要に応じて本体内容付きでシンボル定義を解決。`lsp_compatible:true` で各結果に `uri` と LSP `range` を追加する。完全一致には `exactName` を使う。`exact` は USER_GUIDE.md の flag compatibility table に記載された legacy alias。例: `definition {\"query\":\"McpServer\"}`; `definition {\"query\":\"HandleMessage\",\"lang\":\"csharp\",\"includeBody\":true,\"exactName\":true}`。", + "Use this when you know or suspect a symbol name and need its declaration before editing. Prefer `exactName:true` for identity checks; common next step is `references` or `excerpt`. Resolve symbol definitions with ranges, signatures, and optional body content. Pass `lsp_compatible:true` to add `uri` and LSP `range` fields to each result. `exact` is the legacy alias documented in USER_GUIDE.md's flag compatibility table. Examples: `definition {\"query\":\"McpServer\"}`; `definition {\"query\":\"HandleMessage\",\"lang\":\"csharp\",\"includeBody\":true,\"exactName\":true}`. / シンボル名が分かる、または推測できるときに編集前の宣言確認に使う。identity 確認では `exactName:true` を優先し、次は `references` または `excerpt` を使う。定義範囲、シグネチャ、必要に応じて本体内容付きでシンボル定義を解決。例: `definition {\"query\":\"McpServer\"}`; `definition {\"query\":\"HandleMessage\",\"lang\":\"csharp\",\"includeBody\":true,\"exactName\":true}`。", new JsonObject { ["type"] = "object", @@ -92,7 +92,7 @@ private JsonNode HandleToolsList(JsonNode? id) ReadOnlyAnnotations()), CreateToolDefinition( "references", - "Search indexed symbol references such as call sites. Non-empty responses include `next_step_suggestion` for reading the top hit context; empty responses include `recovery_hint`. Pass `lsp_compatible:true` to add `uri` and LSP `range` fields to each result. For exact matches, use `exactName`; `exact` is the legacy alias documented in USER_GUIDE.md's flag compatibility table. When `kind` is omitted, all indexed reference kinds including metadata uses (`attribute` / `annotation`) and compile-time type-position references (`type_reference`) stay visible, and identical constructor `call` + `instantiate` rows at one physical site are collapsed. Pass `kind: \"type_reference\"` to enumerate declaration types, generic constraints, `is`/`as`/`instanceof`, and XML-doc `cref` targets. Examples: `references {\"query\":\"Run\"}`; `references {\"query\":\"Service\",\"kind\":\"type_reference\",\"lang\":\"csharp\"}`. / 呼び出し箇所などのインデックス済みシンボル参照を検索。非空レスポンスには先頭ヒットの文脈を読む `next_step_suggestion`、空レスポンスには `recovery_hint` を含める。`lsp_compatible:true` で各結果に `uri` と LSP `range` を追加する。完全一致には `exactName` を使う。`exact` は USER_GUIDE.md の flag compatibility table に記載された legacy alias。`kind` 未指定時は metadata (`attribute` / `annotation`) と compile-time な型位置参照 (`type_reference`) も含む全 reference kind を表示したうえで、同じ物理位置にある constructor の `call` + `instantiate` 重複行を集約する。`kind: \"type_reference\"` を指定すると、宣言型・generic 制約・`is`/`as`/`instanceof`・XML-doc `cref` 対象を列挙できる。例: `references {\"query\":\"Run\"}`; `references {\"query\":\"Service\",\"kind\":\"type_reference\",\"lang\":\"csharp\"}`。", + "Use this when you need usage sites, examples, tests, metadata references, or type-position references for a symbol. Prefer it after `definition`; common next step is `excerpt` on representative rows or `callers`/`callees` for runtime impact. Search indexed symbol references such as call sites. Non-empty responses include `next_step_suggestion`; empty responses include `recovery_hint`. Pass `lsp_compatible:true` to add `uri` and LSP `range` fields to each result. For exact matches, use `exactName`; `exact` is the legacy alias documented in USER_GUIDE.md's flag compatibility table. When `kind` is omitted, all indexed reference kinds including metadata uses (`attribute` / `annotation`) and compile-time type-position references (`type_reference`) stay visible, and identical constructor `call` + `instantiate` rows at one physical site are collapsed. Pass `kind: \"type_reference\"` to enumerate declaration types, generic constraints, `is`/`as`/`instanceof`, and XML-doc `cref` targets. Examples: `references {\"query\":\"Run\"}`; `references {\"query\":\"Service\",\"kind\":\"type_reference\",\"lang\":\"csharp\"}`. / シンボルの利用箇所、例、テスト、metadata 参照、型位置参照を調べるときに使う。`definition` の後に優先し、次は代表行の `excerpt` または実行時影響の `callers` / `callees` を使う。例: `references {\"query\":\"Run\"}`; `references {\"query\":\"Service\",\"kind\":\"type_reference\",\"lang\":\"csharp\"}`。", new JsonObject { ["type"] = "object", @@ -120,7 +120,7 @@ private JsonNode HandleToolsList(JsonNode? id) ReadOnlyAnnotations()), CreateToolDefinition( "callers", - "Find caller symbols that reference a callee. For exact matches, use `exactName`; `exact` is the legacy alias documented in USER_GUIDE.md's flag compatibility table. When `kind` is omitted, call-graph kinds (`call`, `instantiate`, `subscribe`, `friend`) are returned so C++ friend access/coupling edges stay visible while metadata uses (`attribute` / `annotation`) and compile-time type-position references (`type_reference`) do not pollute caller edges; identical constructor `call` + `instantiate` rows at one physical site also collapse. Each grouped row additionally exposes `reference_kinds` (sorted distinct kinds behind the row) and `has_mixed_reference_kinds` so callers do not have to trust the single summary label when a container mixes `call` + `subscribe` edges. The existing `reference_kind` scalar is retained for back-compat and carries the preferred summary kind (`instantiate` > `subscribe` > `unsubscribe` > `MIN(kind)`). `callers` / `callees` are not a reliable path to metadata or type-position references — metadata rows are attributed to their enclosing body-range symbol (for a class-level declaration, that is the class itself; for a file-level target such as `[assembly: ...]`, `containerName` is `null` and the row drops from these graph queries entirely), and `type_reference` rows are compile-time type mentions (declaration types, generic constraints, `is`/`as`/`instanceof`, XML-doc `cref`) rather than runtime calls. Use `references` with `kind: \"attribute\"`, `\"annotation\"`, or `\"type_reference\"` instead. Examples: `callers {\"query\":\"HandleRequest\"}`; `callers {\"query\":\"ExecuteAsync\",\"kind\":\"call\",\"rankBy\":\"weighted\",\"lang\":\"csharp\"}`. / 指定シンボルを参照している呼び出し元シンボルを探す。完全一致には `exactName` を使う。`exact` は USER_GUIDE.md の flag compatibility table に記載された legacy alias。`kind` 未指定時は call-graph 種別 (`call` / `instantiate` / `subscribe` / `friend`) を返し、C++ friend の access/coupling edge は可視化しつつ、metadata 使用 (`attribute` / `annotation`) と compile-time な型位置参照 (`type_reference`) が phantom caller edge として混入しないようにする。同じ物理位置にある constructor の `call` + `instantiate` 重複行も集約する。各グループ行には `reference_kinds`(行内の distinct kind をソートした配列)と `has_mixed_reference_kinds` も追加で返すため、container が `call` + `subscribe` を混在させている行で要約 1 ラベルに騙されずに済む。既存のスカラー `reference_kind` は後方互換のため維持され、優先サマリー種別(`instantiate` > `subscribe` > `unsubscribe` > `MIN(kind)`)を持つ。metadata 行の container は注釈対象そのものではなく body-range 上の外側シンボル(クラス直下宣言ならクラス、ファイルレベル target なら `null`)になり、`type_reference` は実行時呼び出しではなく宣言型・generic 制約・`is`/`as`/`instanceof`・XML-doc `cref` といった compile-time な型言及なので、`callers` / `callees` は metadata / 型位置参照の列挙に向かない。Metadata / 型位置参照の列挙は `references --kind attribute|annotation|type_reference` / MCP `references` を使う。例: `callers {\"query\":\"HandleRequest\"}`; `callers {\"query\":\"ExecuteAsync\",\"kind\":\"call\",\"rankBy\":\"weighted\",\"lang\":\"csharp\"}`。", + "Use this when you need to know what calls or depends on a callee symbol before changing it. Prefer it after `definition`/`references`; common next step is `excerpt` on high-ranked caller rows. Find caller symbols that reference a callee. For exact matches, use `exactName`; `exact` is the legacy alias documented in USER_GUIDE.md's flag compatibility table. When `kind` is omitted, call-graph kinds (`call`, `instantiate`, `subscribe`, `friend`) are returned so C++ friend access/coupling edges stay visible while metadata uses (`attribute` / `annotation`) and compile-time type-position references (`type_reference`) do not pollute caller edges; identical constructor `call` + `instantiate` rows at one physical site also collapse. Each grouped row additionally exposes `reference_kinds` (sorted distinct kinds behind the row) and `has_mixed_reference_kinds` so callers do not have to trust the single summary label when a container mixes `call` + `subscribe` edges. The existing `reference_kind` scalar is retained for back-compat and carries the preferred summary kind (`instantiate` > `subscribe` > `unsubscribe` > `MIN(kind)`). `callers` / `callees` are not a reliable path to metadata or type-position references — metadata rows are attributed to their enclosing body-range symbol (for a class-level declaration, that is the class itself; for a file-level target such as `[assembly: ...]`, `containerName` is `null` and the row drops from these graph queries entirely), and `type_reference` rows are compile-time type mentions (declaration types, generic constraints, `is`/`as`/`instanceof`, XML-doc `cref`) rather than runtime calls. Use `references` with `kind: \"attribute\"`, `\"annotation\"`, or `\"type_reference\"` instead. Examples: `callers {\"query\":\"HandleRequest\"}`; `callers {\"query\":\"ExecuteAsync\",\"kind\":\"call\",\"rankBy\":\"weighted\",\"lang\":\"csharp\"}`. / callee シンボルの変更前に呼び出し元や依存元を知りたいときに使う。`definition` / `references` の後に優先し、次は上位 caller 行の `excerpt` を使う。指定シンボルを参照している呼び出し元シンボルを探す。完全一致には `exactName` を使う。`exact` は USER_GUIDE.md の flag compatibility table に記載された legacy alias。`kind` 未指定時は call-graph 種別 (`call` / `instantiate` / `subscribe` / `friend`) を返し、C++ friend の access/coupling edge は可視化しつつ、metadata 使用 (`attribute` / `annotation`) と compile-time な型位置参照 (`type_reference`) が phantom caller edge として混入しないようにする。同じ物理位置にある constructor の `call` + `instantiate` 重複行も集約する。各グループ行には `reference_kinds`(行内の distinct kind をソートした配列)と `has_mixed_reference_kinds` も追加で返すため、container が `call` + `subscribe` を混在させている行で要約 1 ラベルに騙されずに済む。既存のスカラー `reference_kind` は後方互換のため維持され、優先サマリー種別(`instantiate` > `subscribe` > `unsubscribe` > `MIN(kind)`)を持つ。metadata 行の container は注釈対象そのものではなく body-range 上の外側シンボル(クラス直下宣言ならクラス、ファイルレベル target なら `null`)になり、`type_reference` は実行時呼び出しではなく宣言型・generic 制約・`is`/`as`/`instanceof`・XML-doc `cref` といった compile-time な型言及なので、`callers` / `callees` は metadata / 型位置参照の列挙に向かない。Metadata / 型位置参照の列挙は `references --kind attribute|annotation|type_reference` / MCP `references` を使う。例: `callers {\"query\":\"HandleRequest\"}`; `callers {\"query\":\"ExecuteAsync\",\"kind\":\"call\",\"rankBy\":\"weighted\",\"lang\":\"csharp\"}`。", new JsonObject { ["type"] = "object", @@ -147,7 +147,7 @@ private JsonNode HandleToolsList(JsonNode? id) ReadOnlyAnnotations()), CreateToolDefinition( "callees", - "Find callees used by a caller/container symbol. For exact matches, use `exactName`; `exact` is the legacy alias documented in USER_GUIDE.md's flag compatibility table. When `kind` is omitted, call-graph kinds (`call`, `instantiate`, `subscribe`, `friend`) are returned so C++ friend access/coupling edges stay visible while metadata uses (`attribute` / `annotation`) and compile-time type-position references (`type_reference`) do not pollute callee edges; identical constructor `call` + `instantiate` rows at one physical site also collapse. Each grouped row additionally exposes `reference_kinds` (sorted distinct kinds behind the row) and `has_mixed_reference_kinds` for symmetry with `callers`, even though rows are already split per kind on this side. The existing `reference_kind` scalar is retained for back-compat and carries the same kind value. `callees` is not a reliable path to metadata or type-position references — the container assigned to an attribute / annotation row is the enclosing body-range symbol, not the annotated declaration, so `callees Method1 --kind attribute` does not return the attributes on `Method1`, and `type_reference` rows are compile-time type mentions (declaration types, generic constraints, `is`/`as`/`instanceof`, XML-doc `cref`) rather than runtime calls. Use `references` with `kind: \"attribute\"`, `\"annotation\"`, or `\"type_reference\"` instead. Examples: `callees {\"query\":\"Run\"}`; `callees {\"query\":\"Program.Main\",\"kind\":\"instantiate\",\"lang\":\"csharp\",\"limit\":10}`. / 呼び出し元シンボルが使っている呼び出し先を探す。完全一致には `exactName` を使う。`exact` は USER_GUIDE.md の flag compatibility table に記載された legacy alias。`kind` 未指定時は call-graph 種別 (`call` / `instantiate` / `subscribe` / `friend`) を返し、C++ friend の access/coupling edge は可視化しつつ、metadata 使用 (`attribute` / `annotation`) と compile-time な型位置参照 (`type_reference`) が phantom callee edge として混入しないようにする。同じ物理位置にある constructor の `call` + `instantiate` 重複行も集約する。各グループ行には `callers` との対称性のため `reference_kinds`(行内の distinct kind をソートした配列)と `has_mixed_reference_kinds` も返る(`callees` 側は元々 kind ごとに行を分けているため通常は単一要素)。既存のスカラー `reference_kind` は後方互換のため維持され、同じ kind 値を持つ。metadata 行の container は注釈対象自身ではなく body-range 上の外側シンボルになるため、`callees` で `Method1 --kind attribute` を引いても `Method1` に付いた属性は返らない。`type_reference` は実行時呼び出しではなく宣言型・generic 制約・`is`/`as`/`instanceof`・XML-doc `cref` といった compile-time な型言及なので、`callees` は metadata / 型位置参照の列挙に向かない。Metadata / 型位置参照の列挙は `references --kind attribute|annotation|type_reference` / MCP `references` を使う。例: `callees {\"query\":\"Run\"}`; `callees {\"query\":\"Program.Main\",\"kind\":\"instantiate\",\"lang\":\"csharp\",\"limit\":10}`。", + "Use this when you need to know what a caller/container symbol invokes or depends on. Prefer it after `definition` or `outline`; common next step is `excerpt` on a callee row. Find callees used by a caller/container symbol. For exact matches, use `exactName`; `exact` is the legacy alias documented in USER_GUIDE.md's flag compatibility table. When `kind` is omitted, call-graph kinds (`call`, `instantiate`, `subscribe`, `friend`) are returned so C++ friend access/coupling edges stay visible while metadata uses (`attribute` / `annotation`) and compile-time type-position references (`type_reference`) do not pollute callee edges; identical constructor `call` + `instantiate` rows at one physical site also collapse. Each grouped row additionally exposes `reference_kinds` (sorted distinct kinds behind the row) and `has_mixed_reference_kinds` for symmetry with `callers`, even though rows are already split per kind on this side. The existing `reference_kind` scalar is retained for back-compat and carries the same kind value. `callees` is not a reliable path to metadata or type-position references — the container assigned to an attribute / annotation row is the enclosing body-range symbol, not the annotated declaration, so `callees Method1 --kind attribute` does not return the attributes on `Method1`, and `type_reference` rows are compile-time type mentions (declaration types, generic constraints, `is`/`as`/`instanceof`, XML-doc `cref`) rather than runtime calls. Use `references` with `kind: \"attribute\"`, `\"annotation\"`, or `\"type_reference\"` instead. Examples: `callees {\"query\":\"Run\"}`; `callees {\"query\":\"Program.Main\",\"kind\":\"instantiate\",\"lang\":\"csharp\",\"limit\":10}`. / caller/container シンボルが呼ぶ先や依存先を知りたいときに使う。`definition` または `outline` の後に優先し、次は callee 行の `excerpt` を使う。呼び出し元シンボルが使っている呼び出し先を探す。完全一致には `exactName` を使う。`exact` は USER_GUIDE.md の flag compatibility table に記載された legacy alias。`kind` 未指定時は call-graph 種別 (`call` / `instantiate` / `subscribe` / `friend`) を返し、C++ friend の access/coupling edge は可視化しつつ、metadata 使用 (`attribute` / `annotation`) と compile-time な型位置参照 (`type_reference`) が phantom callee edge として混入しないようにする。同じ物理位置にある constructor の `call` + `instantiate` 重複行も集約する。各グループ行には `callers` との対称性のため `reference_kinds`(行内の distinct kind をソートした配列)と `has_mixed_reference_kinds` も返る(`callees` 側は元々 kind ごとに行を分けているため通常は単一要素)。既存のスカラー `reference_kind` は後方互換のため維持され、同じ kind 値を持つ。metadata 行の container は注釈対象自身ではなく body-range 上の外側シンボルになるため、`callees` で `Method1 --kind attribute` を引いても `Method1` に付いた属性は返らない。`type_reference` は実行時呼び出しではなく宣言型・generic 制約・`is`/`as`/`instanceof`・XML-doc `cref` といった compile-time な型言及なので、`callees` は metadata / 型位置参照の列挙に向かない。Metadata / 型位置参照の列挙は `references --kind attribute|annotation|type_reference` / MCP `references` を使う。例: `callees {\"query\":\"Run\"}`; `callees {\"query\":\"Program.Main\",\"kind\":\"instantiate\",\"lang\":\"csharp\",\"limit\":10}`。", new JsonObject { ["type"] = "object", @@ -174,7 +174,7 @@ private JsonNode HandleToolsList(JsonNode? id) ReadOnlyAnnotations()), CreateToolDefinition( "symbols", - "Search for code symbols (functions, classes, interfaces, imports) by name pattern. For exact matches, use `exactName`; `exact` is the legacy alias documented in USER_GUIDE.md's flag compatibility table. Examples: `symbols {\"query\":\"Service\"}`; `symbols {\"query\":\"Run\",\"kind\":\"function\",\"lang\":\"csharp\",\"exactName\":true}`. / シンボル(関数、クラス、インターフェース、import)を名前パターンで検索。完全一致には `exactName` を使う。`exact` は USER_GUIDE.md の flag compatibility table に記載された legacy alias。例: `symbols {\"query\":\"Service\"}`; `symbols {\"query\":\"Run\",\"kind\":\"function\",\"lang\":\"csharp\",\"exactName\":true}`。", + "Use this when discovering candidate symbols before `definition`, `references`, `callers`, or `callees`. Prefer `exactName:true` when the name must match exactly. Search for code symbols (functions, classes, interfaces, imports) by name pattern. `exact` is the legacy alias documented in USER_GUIDE.md's flag compatibility table. Examples: `symbols {\"query\":\"Service\"}`; `symbols {\"query\":\"Run\",\"kind\":\"function\",\"lang\":\"csharp\",\"exactName\":true}`. / `definition` / `references` / `callers` / `callees` の前に候補シンボルを探すときに使う。名前を厳密一致させるなら `exactName:true` を優先する。シンボル(関数、クラス、インターフェース、import)を名前パターンで検索。例: `symbols {\"query\":\"Service\"}`; `symbols {\"query\":\"Run\",\"kind\":\"function\",\"lang\":\"csharp\",\"exactName\":true}`。", new JsonObject { ["type"] = "object", @@ -201,7 +201,7 @@ private JsonNode HandleToolsList(JsonNode? id) ReadOnlyAnnotations()), CreateToolDefinition( "files", - "List indexed files, optionally filtered by name pattern and language. / インデックス済みファイルを一覧(名前パターン・言語でフィルタ可能)。", + "Use this when you need to locate indexed files by path, language, or recent-change scope before reading content. Prefer `outline` or `excerpt` as the next step after choosing a file. List indexed files, optionally filtered by name pattern and language. / 内容を読む前に path、言語、最近の変更範囲でインデックス済みファイルを探すときに使う。ファイルを選んだ後は `outline` または `excerpt` を優先する。インデックス済みファイルを一覧(名前パターン・言語でフィルタ可能)。", new JsonObject { ["type"] = "object", @@ -222,7 +222,7 @@ private JsonNode HandleToolsList(JsonNode? id) ReadOnlyAnnotations()), CreateToolDefinition( "excerpt", - "Reconstruct a file excerpt from indexed chunks for a given line range. Successful responses include `next_step_suggestion` for the file outline; empty responses include `recovery_hint`. / 指定行範囲について、インデックス済みチャンクからファイル抜粋を再構成。成功レスポンスにはファイル outline への `next_step_suggestion`、空レスポンスには `recovery_hint` を含める。", + "Use this after `search`, `definition`, `references`, `outline`, or `map` identifies a file and line range. Prefer focused excerpts over whole-file reads; common next step is `outline` for neighboring structure. Reconstruct a file excerpt from indexed chunks for a given line range. Successful responses include `next_step_suggestion`; empty responses include `recovery_hint`. / `search` / `definition` / `references` / `outline` / `map` でファイルと行範囲を絞った後に使う。ファイル全体ではなく必要範囲の抜粋を優先し、次は周辺構造確認の `outline` を使う。指定行範囲について、インデックス済みチャンクからファイル抜粋を再構成。", new JsonObject { ["type"] = "object", @@ -244,7 +244,7 @@ private JsonNode HandleToolsList(JsonNode? id) ReadOnlyAnnotations()), CreateToolDefinition( "find_in_file", - "Find literal substring matches inside one known indexed file or a small explicit file list, with line numbers and short surrounding context. / 既知のインデックス済みファイル1件または少数の明示ファイル群の中で、行番号と短い前後文脈付きのリテラル部分文字列一致を探す。", + "Use this when the target file is already known and you need literal or regex navigation inside it. Prefer `excerpt` on returned lines as the next step. Find literal substring matches inside one known indexed file or a small explicit file list, with line numbers and short surrounding context. / 対象ファイルが既に分かっていて、その中を literal または regex で移動したいときに使う。次は返された行の `excerpt` を優先する。既知のインデックス済みファイル1件または少数の明示ファイル群の中で、行番号と短い前後文脈付きの一致を探す。", new JsonObject { ["type"] = "object", @@ -271,7 +271,7 @@ private JsonNode HandleToolsList(JsonNode? id) ReadOnlyAnnotations()), CreateToolDefinition( "map", - "Return a repo-level overview with selectable sections (`tree`, `languages`, `hotspots`, `metrics`) and optional module depth control. / セクション選択(`tree`, `languages`, `hotspots`, `metrics`)とモジュール深さ制御に対応したリポジトリ俯瞰情報を返す。", + "Use this when orienting in an unfamiliar repo, module, language mix, or hotspot area before searching. Prefer `search`, `symbols`, `outline`, or `excerpt` as the next step after choosing a path. Return a repo-level overview with selectable sections (`tree`, `languages`, `hotspots`, `metrics`) and optional module depth control. / 不慣れなリポジトリ、モジュール、言語構成、hotspot 領域を search 前に把握するときに使う。path を選んだ後は `search` / `symbols` / `outline` / `excerpt` を優先する。セクション選択(`tree`, `languages`, `hotspots`, `metrics`)とモジュール深さ制御に対応したリポジトリ俯瞰情報を返す。", new JsonObject { ["type"] = "object", @@ -290,7 +290,7 @@ private JsonNode HandleToolsList(JsonNode? id) ReadOnlyAnnotations()), CreateToolDefinition( "analyze_symbol", - "Bundle definition, nearby symbols, references, callers, callees, file metadata, and graph-support metadata for one symbol query. For exact matches, use `exactName`; `exact` is the legacy alias documented in USER_GUIDE.md's flag compatibility table. Bundled caller/callee rows carry the same `reference_kind` (preferred summary kind, back-compat) plus `reference_kinds` (sorted distinct) and `has_mixed_reference_kinds` fields as the standalone `callers` / `callees` tools, so mixed `call` + `subscribe` containers stay visible in the bundle. Supports `format: count|compact`; CLI `since` filtering is intentionally not exposed because the backing analysis reader does not support it yet. / 1つのシンボルクエリに対して、定義、近傍シンボル、参照、caller、callee、ファイルメタデータ、グラフ対応メタデータをまとめて返す。完全一致には `exactName` を使う。`exact` は USER_GUIDE.md の flag compatibility table に記載された legacy alias。バンドルされた caller / callee 行にも単独の `callers` / `callees` と同じ `reference_kind`(後方互換の優先サマリー種別)、`reference_kinds`(distinct kind の昇順配列)、`has_mixed_reference_kinds` が付くため、`call` + `subscribe` が混在するコンテナも要約 1 ラベルに潰れず見える。`format: count|compact` 対応。CLI の `since` filter は backing analysis reader 未対応のため意図的に未公開。", + "Use this when one symbol needs a compact dossier and you would otherwise chain `definition`, `references`, `callers`, and `callees`. Prefer standalone tools when you need deeper pagination; common next step is `excerpt` on the most relevant rows. Bundle definition, nearby symbols, references, callers, callees, file metadata, and graph-support metadata for one symbol query. For exact matches, use `exactName`; `exact` is the legacy alias documented in USER_GUIDE.md's flag compatibility table. Bundled caller/callee rows carry the same `reference_kind` (preferred summary kind, back-compat) plus `reference_kinds` (sorted distinct) and `has_mixed_reference_kinds` fields as the standalone `callers` / `callees` tools, so mixed `call` + `subscribe` containers stay visible in the bundle. Supports `format: count|compact`; CLI `since` filtering is intentionally not exposed because the backing analysis reader does not support it yet. / 1つのシンボルについて compact な dossier が必要で、`definition` / `references` / `callers` / `callees` を連続呼び出ししそうなときに使う。深い pagination が必要なら単独ツールを優先し、次は重要行の `excerpt` を使う。1つのシンボルクエリに対して、定義、近傍シンボル、参照、caller、callee、ファイルメタデータ、グラフ対応メタデータをまとめて返す。完全一致には `exactName` を使う。`exact` は USER_GUIDE.md の flag compatibility table に記載された legacy alias。バンドルされた caller / callee 行にも単独の `callers` / `callees` と同じ `reference_kind`(後方互換の優先サマリー種別)、`reference_kinds`(distinct kind の昇順配列)、`has_mixed_reference_kinds` が付くため、`call` + `subscribe` が混在するコンテナも要約 1 ラベルに潰れず見える。`format: count|compact` 対応。CLI の `since` filter は backing analysis reader 未対応のため意図的に未公開。", new JsonObject { ["type"] = "object", @@ -315,7 +315,7 @@ private JsonNode HandleToolsList(JsonNode? id) ReadOnlyAnnotations()), CreateToolDefinition( "impact_analysis", - "Compute the transitive caller chain for a symbol. The symbol-level BFS walks only call-graph kinds (`call`, `instantiate`, `subscribe`) and excludes metadata-only edges (`attribute`, `annotation`, `type_reference`) so metadata cycles do not inflate caller counts. Multiple edge kinds from the same caller to the same target are counted and returned separately, with `reference_kind`, `reference_kinds`, and `reference_kindCounts` on each caller row. When a scoped query resolves to a single class / struct / interface but no symbol-level callers exist, may return heuristic file-level dependency hints instead; those file hints can include metadata edges, so check `impact_mode`, `heuristic`, and `file_impacts`. When `truncated` is true, inspect `truncated_reason` (`user_limit` means raising `limit` returns more; `safety_cap` means the graph is likely pathological and raising `limit` will not help). Pass `withPaths: true` when you need the call chain via specific intermediates — each caller then carries a `paths` array of shortest routes (issue #1536). / シンボルの推移的呼び出しチェーンを算出。symbol-level BFS は call graph 種別(`call`、`instantiate`、`subscribe`)のみを辿り、metadata-only edge(`attribute`、`annotation`、`type_reference`)を除外するため、metadata cycle で caller 件数が膨らまない。同じ caller から同じ target への複数 edge kind は別々に数えて返し、各 caller 行に `reference_kind`、`reference_kinds`、`reference_kindCounts` が付く。scoped query が単一の class / struct / interface に解決されても symbol-level caller が無い場合は、代わりに heuristic な file-level dependency hint を返すことがある。この file hint は metadata edge を含み得るため、`impact_mode`・`heuristic`・`file_impacts` を確認すること。`truncated` が真のときは `truncated_reason` を見て、`user_limit` なら `limit` を増やせば残りも取得可能、`safety_cap` ならグラフが病的で `limit` を増やしても解消しないことを区別すること。中間シンボル経由の経路が必要な場合は `withPaths: true` を渡すと、各 caller に経路配列 `paths` が付く(issue #1536)。", + "Use this when planning a symbol change and you need transitive caller impact rather than just direct references. Prefer `definition` first to confirm identity; common next step is `excerpt` on impacted callers or files. Compute the transitive caller chain for a symbol. The symbol-level BFS walks only call-graph kinds (`call`, `instantiate`, `subscribe`) and excludes metadata-only edges (`attribute`, `annotation`, `type_reference`) so metadata cycles do not inflate caller counts. Multiple edge kinds from the same caller to the same target are counted and returned separately, with `reference_kind`, `reference_kinds`, and `reference_kindCounts` on each caller row. When a scoped query resolves to a single class / struct / interface but no symbol-level callers exist, may return heuristic file-level dependency hints instead; those file hints can include metadata edges, so check `impact_mode`, `heuristic`, and `file_impacts`. When `truncated` is true, inspect `truncated_reason` (`user_limit` means raising `limit` returns more; `safety_cap` means the graph is likely pathological and raising `limit` will not help). Pass `withPaths: true` when you need the call chain via specific intermediates — each caller then carries a `paths` array of shortest routes (issue #1536). / シンボル変更を計画していて、直接参照だけでなく推移的 caller 影響が必要なときに使う。identity 確認には先に `definition` を優先し、次は影響 caller/file の `excerpt` を使う。シンボルの推移的呼び出しチェーンを算出。symbol-level BFS は call graph 種別(`call`、`instantiate`、`subscribe`)のみを辿り、metadata-only edge(`attribute`、`annotation`、`type_reference`)を除外するため、metadata cycle で caller 件数が膨らまない。同じ caller から同じ target への複数 edge kind は別々に数えて返し、各 caller 行に `reference_kind`、`reference_kinds`、`reference_kindCounts` が付く。scoped query が単一の class / struct / interface に解決されても symbol-level caller が無い場合は、代わりに heuristic な file-level dependency hint を返すことがある。この file hint は metadata edge を含み得るため、`impact_mode`・`heuristic`・`file_impacts` を確認すること。`truncated` が真のときは `truncated_reason` を見て、`user_limit` なら `limit` を増やせば残りも取得可能、`safety_cap` ならグラフが病的で `limit` を増やしても解消しないことを区別すること。中間シンボル経由の経路が必要な場合は `withPaths: true` を渡すと、各 caller に経路配列 `paths` が付く(issue #1536)。", new JsonObject { ["type"] = "object", @@ -357,7 +357,7 @@ private JsonNode HandleToolsList(JsonNode? id) ReadOnlyAnnotations()), CreateToolDefinition( "outline", - "Return the symbol outline of a single indexed file: all functions, classes, imports with line numbers, signatures, and nesting. / 1ファイルのシンボルアウトラインを返す: 関数、クラス、importの行番号、シグネチャ、ネスト構造。", + "Use this when a file is known but you need structure before reading content. Prefer it before whole-file reads; common next step is `excerpt` on a specific symbol range. Return the symbol outline of a single indexed file: all functions, classes, imports with line numbers, signatures, and nesting. / ファイルは分かっているが本文を読む前に構造を把握したいときに使う。ファイル全体を読む前に優先し、次は特定シンボル範囲の `excerpt` を使う。1ファイルのシンボルアウトラインを返す: 関数、クラス、importの行番号、シグネチャ、ネスト構造。", new JsonObject { ["type"] = "object", @@ -529,12 +529,12 @@ private JsonNode HandleToolsList(JsonNode? id) ReadOnlyAnnotations()), CreateToolDefinition( "unused_symbols", - "Find symbols that are defined but never referenced in the indexed codebase. " - + "Useful for dead code detection. Results include confidence buckets so private hits rank ahead of public/exported suspects, and the lowest-confidence bucket is reserved for config-bound properties or C#-style attribute-adjacent reflection surfaces. Only meaningful for languages with reference extraction support. " + "Use this when auditing potential dead code before removal. Prefer `references`, `callers`, or `excerpt` to verify surprising hits before editing. Find symbols that are defined but never referenced in the indexed codebase. " + + "Results include confidence buckets so private hits rank ahead of public/exported suspects, and the lowest-confidence bucket is reserved for config-bound properties or C#-style attribute-adjacent reflection surfaces. Only meaningful for languages with reference extraction support. " + "Structured output includes `summary.by_bucket`, `summary.by_confidence`, and `bucket_taxonomy`; bucket values are `likely_unused_private`, `maybe_unused_nonpublic`, `public_or_exported_no_refs`, and `reflection_or_config_suspect`. Use `bucket` or `minConfidence` to audit a single bucket or confidence class. " + "C# nameof/typeof and direct reflection member-name literals such as GetMethod(\"Foo\") are indexed as references; dynamically constructed reflection names can still require manual review. " - + "/ インデックス済みコードベースで定義されているが一度も参照されていないシンボルを検索する。" - + "デッドコード検出に有用。private 候補を public/exported suspect より前に返し、最低信頼 bucket は config-bound な property または C# 風 attribute 隣接の reflection surface 用に使う。参照抽出対応言語でのみ意味がある。" + + "/ 削除前に dead code 候補を監査するときに使う。意外なヒットは編集前に `references` / `callers` / `excerpt` で確認する。インデックス済みコードベースで定義されているが一度も参照されていないシンボルを検索する。" + + "private 候補を public/exported suspect より前に返し、最低信頼 bucket は config-bound な property または C# 風 attribute 隣接の reflection surface 用に使う。参照抽出対応言語でのみ意味がある。" + "構造化出力には `summary.by_bucket`、`summary.by_confidence`、`bucket_taxonomy` が含まれ、bucket 値は `likely_unused_private`、`maybe_unused_nonpublic`、`public_or_exported_no_refs`、`reflection_or_config_suspect`。`bucket` または `minConfidence` で単一 bucket や confidence class を監査できる。" + "C# の nameof/typeof と GetMethod(\"Foo\") のような直接の reflection member-name literal は参照として index されるが、動的に組み立てた reflection 名は手動確認が必要な場合がある。", new JsonObject @@ -827,6 +827,49 @@ private static void ApplyCommonSchemaMetadata(string toolName, string name, Json MarkDeprecatedAlias(obj, "maxHops", "Use `maxHops`; `maxDepth` is retained for compatibility."); break; } + + switch (name) + { + case "query": + AppendConstraintDescription(obj, "Use identifiers, symbol names, error messages, config keys, or short code/text fragments; add exactName/exactSubstring when identity matters."); + break; + case "exactName": + AppendConstraintDescription(obj, "Use this when the symbol name must match exactly, e.g. `Run` should not also match `RunAsync`."); + break; + case "path" when toolName != "index": + AppendConstraintDescription(obj, "Use this after broad results are noisy to narrow by module, directory, file name, project area, or tests."); + break; + case "excludeTests": + AppendConstraintDescription(obj, "Set true for production-code investigation; leave false when finding tests, examples, or coverage."); + break; + case "includeGenerated": + AppendConstraintDescription(obj, "Keep false by default unless generated code is explicitly part of the investigation."); + break; + case "format": + AppendConstraintDescription(obj, "Use `compact` or `count` while exploring large result sets; use `full` when snippets or complete rows are needed."); + break; + } + + switch (toolName, name) + { + case ("search", "exactSubstring"): + AppendConstraintDescription(obj, "Use this for case-sensitive exact text identity when tokenization, punctuation, emoji, or prefix matching would be misleading."); + break; + case ("search", "exact"): + AppendConstraintDescription(obj, "Alias of `exactSubstring`; use `exactSubstring` in new calls for search text identity."); + break; + case ("search", "prefix"): + AppendConstraintDescription(obj, "Use this for partial tokens, Japanese terms, or identifier prefixes when a broader token-prefix search is desired."); + break; + case ("definition", "exact"): + case ("references", "exact"): + case ("callers", "exact"): + case ("callees", "exact"): + case ("symbols", "exact"): + case ("analyze_symbol", "exact"): + AppendConstraintDescription(obj, "Alias of `exactName`; use `exactName` in new calls for exact symbol identity."); + break; + } } private static void MarkDeprecatedAlias(JsonObject obj, string aliasOf, string reason) diff --git a/src/CodeIndex/Mcp/McpToolHandlers.cs b/src/CodeIndex/Mcp/McpToolHandlers.cs index b9d793a13e..db0364d979 100644 --- a/src/CodeIndex/Mcp/McpToolHandlers.cs +++ b/src/CodeIndex/Mcp/McpToolHandlers.cs @@ -69,7 +69,10 @@ bool All(params string[] names) return true; } - var parts = new List { "cdidx is a code-index server." }; + var parts = new List + { + "cdidx is a local-first code-index server. Prefer CodeIndex MCP tools before shell grep/find/cat when investigating indexed repositories; use whole-file reads only after narrowing the target. cdidx は local-first なコード検索・取得サーバーです。インデックス済みリポジトリの調査では shell の grep/find/cat を乱発する前に CodeIndex MCP tools を優先し、ファイル全体の読み取りは対象を絞ってから使ってください。", + }; if (On("index")) parts.Add("If queries fail because no index exists, run 'index' first to build it."); @@ -83,6 +86,14 @@ bool All(params string[] names) else if (On("definition")) parts.Add("Use 'definition' for symbol lookup."); + var guidedFlowTools = new List(); + foreach (var name in new[] { "search", "definition", "references", "callers", "callees", "outline", "map", "excerpt" }) + if (On(name)) guidedFlowTools.Add(name); + if (guidedFlowTools.Count > 0) + { + 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 で絞り込んでください。"); + } + if (On("analyze_symbol")) parts.Add("Use 'analyze_symbol' to get definition, callers, callees, and references in one call instead of chaining separate tools."); @@ -287,17 +298,18 @@ private static void AddSymbolRecoveryHint(JsonObject payload, string query, stri AddRecoveryHint( payload, "no_results", - $"{toolName} returned no rows; check whether the symbol is indexed, whether filters are too narrow, or whether a broader symbol lookup finds a nearby name.", + $"{toolName} returned no rows; relax exactName/path/lang/kind filters, try symbols for nearby names, or search related identifiers/error text before assuming the symbol is absent.", "symbols", args); } - private static void AddNextStepSuggestion(JsonObject payload, string tool, JsonObject args) + private static void AddNextStepSuggestion(JsonObject payload, string tool, JsonObject args, string suggestedAction) { payload["next_step_suggestion"] = new JsonObject { ["tool"] = tool, ["args"] = args, + ["suggested_action"] = suggestedAction, }; } @@ -1097,6 +1109,13 @@ private static void AddResultEnvelope(JsonObject payload, int returnedCount, int payload["truncated"] = truncated; payload["more_available"] = truncated; payload["total"] = total.HasValue ? JsonValue.Create(total.Value) : null; + if (truncated) + { + payload["pagination_hint"] = new JsonObject + { + ["suggested_action"] = "More rows are available; continue with the provided cursor/next_offset when you need breadth, or narrow with path/lang/kind/excludeTests/format filters before reading details.", + }; + } } private static void AddPaginatedResultEnvelope(JsonObject payload, int returnedCount, int? total, bool truncated, int offset) @@ -1808,7 +1827,8 @@ private JsonNode ExecuteSearch(JsonNode? id, JsonNode? args) AddNextStepSuggestion( structured, "excerpt", - BuildExcerptArgs(topResult.Path, topResult.StartLine, topResult.EndLine)); + BuildExcerptArgs(topResult.Path, topResult.StartLine, topResult.EndLine), + "Use excerpt on the top hit before editing; for symbol changes, follow with definition or references to confirm declarations and usage sites."); if (suggestExactSubstring) AddExactSubstringRecoveryHint(structured, query); adjustments.ApplyTo(structured); @@ -2109,6 +2129,12 @@ private JsonNode ExecuteSymbols(JsonNode? id, JsonNode? args) } if (hasExactPredicate) AddExactGraphSignal(structured, exactSignal); + var topSymbol = results[0]; + AddNextStepSuggestion( + structured, + "definition", + new JsonObject { ["query"] = topSymbol.Name, ["limit"] = 5, ["exactName"] = true }, + "Use definition to confirm the declaration for the best symbol candidate; then use references, callers, or callees depending on the change."); adjustments.ApplyTo(structured); return CreateToolResult(id, ConsoleUi.FoundSummary(results.Count, "symbol"), structured); }); @@ -2195,6 +2221,14 @@ private JsonNode ExecuteDefinition(JsonNode? id, JsonNode? args) AddSymbolRecoveryHint(payload, query, "definition", lang, kind, PathEcho(pathPatterns)); AddFreshnessHint(payload, reader); } + else + { + AddNextStepSuggestion( + payload, + "references", + new JsonObject { ["query"] = results[0].Name, ["limit"] = 5, ["exactName"] = true }, + "Use references to inspect usage sites before changing this definition; then use excerpt for the relevant definition or reference ranges."); + } adjustments.ApplyTo(payload); return CreateToolResult(id, ConsoleUi.FoundSummary(results.Count, "definition"), @@ -2324,7 +2358,8 @@ private JsonNode ExecuteReferences(JsonNode? id, JsonNode? args) AddNextStepSuggestion( payload, "excerpt", - BuildExcerptArgs(topReference.Path, topReference.Line, topReference.Line)); + BuildExcerptArgs(topReference.Path, topReference.Line, topReference.Line), + "Use excerpt on representative usage sites before editing; use callers or callees when you need call graph impact."); } adjustments.ApplyTo(payload); return CreateToolResult(id, @@ -2426,6 +2461,15 @@ private JsonNode ExecuteCallers(JsonNode? id, JsonNode? args) AddSymbolRecoveryHint(payload, query, "callers", lang, kind, PathEcho(pathPatterns)); AddFreshnessHint(payload, reader); } + else + { + var topCaller = results[0]; + AddNextStepSuggestion( + payload, + "excerpt", + BuildExcerptArgs(topCaller.Path, topCaller.FirstLine, topCaller.FirstLine), + "Use excerpt on a caller row to understand the concrete call site before widening impact analysis or editing."); + } adjustments.ApplyTo(payload); return CreateToolResult(id, BuildGraphSummary("caller", "callers", results.Count, graphSupport.GraphLanguage, graphSupport.GraphSupported, graphSupport.GraphSupportReason), @@ -2526,6 +2570,15 @@ private JsonNode ExecuteCallees(JsonNode? id, JsonNode? args) AddSymbolRecoveryHint(payload, query, "callees", lang, kind, PathEcho(pathPatterns)); AddFreshnessHint(payload, reader); } + else + { + var topCallee = results[0]; + AddNextStepSuggestion( + payload, + "excerpt", + BuildExcerptArgs(topCallee.Path, topCallee.FirstLine, topCallee.FirstLine), + "Use excerpt on a callee row to inspect the concrete dependency before changing the caller or callee."); + } adjustments.ApplyTo(payload); return CreateToolResult(id, BuildGraphSummary("callee", "callees", results.Count, graphSupport.GraphLanguage, graphSupport.GraphSupported, graphSupport.GraphSupportReason), @@ -3361,6 +3414,11 @@ private JsonNode ExecuteOutline(JsonNode? id, JsonNode? args) } var structured = JsonSerializer.SerializeToNode(outline, _jsonOptions)!.AsObject(); + AddNextStepSuggestion( + structured, + "excerpt", + new JsonObject { ["path"] = path, ["startLine"] = 1, ["endLine"] = Math.Min(outline.TotalLines, 80) }, + "Use excerpt for only the relevant outline range instead of reading the whole file."); return CreateToolResult(id, $"Outline: {ConsoleUi.Counted(outline.SymbolCount, "symbol")} in {ConsoleUi.Counted(outline.TotalLines, "line")}.", structured); }); } @@ -3475,7 +3533,8 @@ private JsonNode ExecuteExcerpt(JsonNode? id, JsonNode? args) AddNextStepSuggestion( payload, "outline", - new JsonObject { ["path"] = excerpt.Path }); + new JsonObject { ["path"] = excerpt.Path }, + "Use outline to navigate neighboring symbols before requesting more ranges from the same file."); return CreateToolResult(id, "Excerpt returned.", payload); }); } diff --git a/tests/CodeIndex.Tests/McpServerTests.cs b/tests/CodeIndex.Tests/McpServerTests.cs index e97ea8e3d3..45129cab13 100644 --- a/tests/CodeIndex.Tests/McpServerTests.cs +++ b/tests/CodeIndex.Tests/McpServerTests.cs @@ -578,6 +578,8 @@ public void ToolsCall_Search_WithResultsIncludesNextStepSuggestion() Assert.Equal("src/app.cs", suggestion["args"]!["path"]!.GetValue()); Assert.True(suggestion["args"]!["startLine"]!.GetValue() >= 1); Assert.True(suggestion["args"]!["endLine"]!.GetValue() >= suggestion["args"]!["startLine"]!.GetValue()); + Assert.Contains("excerpt", suggestion["suggested_action"]!.GetValue()); + Assert.Contains("definition", suggestion["suggested_action"]!.GetValue()); } [Fact] @@ -601,6 +603,7 @@ void Target() { } Assert.Equal("excerpt", suggestion["tool"]!.GetValue()); Assert.Equal("src/reference-hint.cs", suggestion["args"]!["path"]!.GetValue()); + Assert.Contains("callers", suggestion["suggested_action"]!.GetValue()); } [Fact] @@ -615,6 +618,7 @@ public void ToolsCall_Excerpt_WithResultIncludesNextStepSuggestion() Assert.Equal("outline", suggestion["tool"]!.GetValue()); Assert.Equal("src/app.cs", suggestion["args"]!["path"]!.GetValue()); + Assert.Contains("outline", suggestion["suggested_action"]!.GetValue()); } [Fact] @@ -630,6 +634,8 @@ public void ToolsCall_Callers_EmptyResultIncludesRecoveryHint() Assert.Equal("no_results", hint["reason"]!.GetValue()); Assert.Equal("symbols", hint["tool"]!.GetValue()); Assert.Equal("MissingSymbol", hint["args"]!["query"]!.GetValue()); + Assert.Contains("relax exactName/path/lang/kind filters", hint["suggested_action"]!.GetValue()); + Assert.Contains("search", hint["suggested_action"]!.GetValue()); } // --- Protocol tests / プロトコルテスト --- @@ -1510,6 +1516,10 @@ public void PromptsListAndGet_ReturnPromptMessages() Assert.Contains("summarize_file", names); Assert.Contains("find_unused", names); Assert.Contains("impact_of_changing", names); + Assert.Contains("investigate_before_edit", names); + Assert.Contains("find_existing_pattern", names); + Assert.Contains("safe_symbol_change", names); + Assert.Contains("debug_failure", names); var get = JsonNode.Parse("""{"jsonrpc":"2.0","id":2,"method":"prompts/get","params":{"name":"impact_of_changing","arguments":{"symbol":"Run"}}}""")!; var getResponse = _server.HandleMessage(get)!; @@ -1517,6 +1527,14 @@ public void PromptsListAndGet_ReturnPromptMessages() Assert.Equal("user", message["role"]!.GetValue()); Assert.Contains("impact_analysis", message["content"]!["text"]!.GetValue()); Assert.Contains("Run", message["content"]!["text"]!.GetValue()); + + var investigate = JsonNode.Parse("""{"jsonrpc":"2.0","id":3,"method":"prompts/get","params":{"name":"investigate_before_edit","arguments":{"topic":"Run"}}}""")!; + var investigateResponse = _server.HandleMessage(investigate)!; + var investigateText = investigateResponse["result"]!["messages"]!.AsArray().Single()!["content"]!["text"]!.GetValue(); + Assert.Contains("search", investigateText); + Assert.Contains("definition", investigateText); + Assert.Contains("references", investigateText); + Assert.Contains("excerpt", investigateText); } [Fact] @@ -2168,6 +2186,14 @@ public void Initialize_ReturnsInstructions() Assert.Contains("map", instructions!); Assert.Contains("analyze_symbol", instructions); Assert.Contains("search", instructions); + Assert.Contains("CodeIndex MCP tools", instructions); + Assert.Contains("grep/find/cat", instructions); + Assert.Contains("resources/read", instructions); + Assert.Contains("whole-file reads", instructions); + Assert.Contains("definition", instructions); + Assert.Contains("references", instructions); + Assert.Contains("callers/callees", instructions); + Assert.Contains("excerpt", instructions); // Verify index-first bootstrap guidance / インデックス未作成時の案内を検証 Assert.Contains("index", instructions); Assert.Contains("backfill_fold", instructions); @@ -4155,6 +4181,46 @@ public void ToolsList_NavigationDescriptionsIncludeConcreteExamples() } } + [Fact] + public void ToolsList_NavigationDescriptionsExplainWhenAndNextStep() + { + var request = JsonNode.Parse("""{"jsonrpc":"2.0","id":1,"method":"tools/list"}""")!; + var response = _server.HandleMessage(request)!; + + var tools = response["result"]!["tools"]!.AsArray(); + foreach (var name in new[] { "search", "definition", "references", "callers", "callees", "symbols", "files", "excerpt", "find_in_file", "map", "outline" }) + { + var description = tools.First(t => t!["name"]!.GetValue() == name)!["description"]!.GetValue(); + Assert.Contains("Use this", description, StringComparison.Ordinal); + Assert.Contains("Prefer", description, StringComparison.Ordinal); + } + + var searchDescription = tools.First(t => t!["name"]!.GetValue() == "search")!["description"]!.GetValue(); + Assert.Contains("before shell grep", searchDescription, StringComparison.Ordinal); + Assert.Contains("common next step", searchDescription, StringComparison.Ordinal); + } + + [Fact] + public void ToolsList_CommonSchemaDescriptionsGuideDisambiguation() + { + var request = JsonNode.Parse("""{"jsonrpc":"2.0","id":1,"method":"tools/list"}""")!; + var response = _server.HandleMessage(request)!; + + var tools = response["result"]!["tools"]!.AsArray(); + var searchProperties = tools.First(t => t!["name"]!.GetValue() == "search")!["inputSchema"]!["properties"]!; + Assert.Contains("identifiers", searchProperties["query"]!["description"]!.GetValue(), StringComparison.Ordinal); + Assert.Contains("case-sensitive exact text identity", searchProperties["exactSubstring"]!["description"]!.GetValue(), StringComparison.Ordinal); + Assert.Contains("partial tokens", searchProperties["prefix"]!["description"]!.GetValue(), StringComparison.Ordinal); + Assert.Contains("narrow by module", searchProperties["path"]!["description"]!.GetValue(), StringComparison.Ordinal); + Assert.Contains("production-code investigation", searchProperties["excludeTests"]!["description"]!.GetValue(), StringComparison.Ordinal); + Assert.Contains("generated code", searchProperties["includeGenerated"]!["description"]!.GetValue(), StringComparison.Ordinal); + Assert.Contains("large result sets", searchProperties["format"]!["description"]!.GetValue(), StringComparison.Ordinal); + + var definitionProperties = tools.First(t => t!["name"]!.GetValue() == "definition")!["inputSchema"]!["properties"]!; + Assert.Contains("symbol name must match exactly", definitionProperties["exactName"]!["description"]!.GetValue(), StringComparison.Ordinal); + Assert.Contains("Alias of `exactName`", definitionProperties["exact"]!["description"]!.GetValue(), StringComparison.Ordinal); + } + [Fact] public void ToolsList_ExactAliasParametersAreExposed() {