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
9 changes: 7 additions & 2 deletions DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2760,7 +2760,12 @@ Piping `{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}` into
`resource_chunk_topology_invalid`, and `scan_limit_exceeded`.
`prompts/list` exposes the built-in `summarize_file`, `find_unused`, and
`impact_of_changing` prompts; `prompts/get` returns a user-message template
that directs clients toward the matching cdidx tools. `logging` advertises
that directs clients toward the matching cdidx tools. `summarize_file`
advertises `path` as required and rejects missing, non-string, blank, absolute,
drive-prefixed, control-character-containing, or `..`-traversing values with JSON-RPC
`-32602` before constructing the prompt. Accepted workspace-relative paths
preserve spaces, Unicode, and POSIX backslash filename characters while
normalizing Windows path separators to `/`. `logging` advertises
MCP `notifications/message`; `logging/setLevel` accepts `debug`, `info`,
`notice`, `warning`, `error`, `critical`, `alert`, and `emergency`.
- `protocolVersion` is **negotiated**, not hardcoded (#1554). The server
Expand Down Expand Up @@ -5865,7 +5870,7 @@ session は明示的な初期化前、初期化中、初期化済み、shutdown
- 独立した 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 には server 提供の `tools`、`resources`、`prompts`、`logging` だけが含まれ、client 提供の `roots` と `sampling` は含まれない。`resources/list` はインデックス済みファイルを `cdidx://file/<path>` 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/<path>` 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 を推測する必要がない。
- `initialize` の `instructions` はこれらの resource template / list control を直接案内し、各 `resources/list` response は accepted extension parameter と上限を `_meta.discovery_contract` に公開する。これにより AI client は標準外の protocol extension を推測する必要がない。`prompts/list` の `summarize_file` は `path` を必須として公開し、`prompts/get` は prompt 構築前に、欠落、文字列以外、空文字・空白のみ、絶対 path、drive prefix、制御文字、`..` traversal を JSON-RPC `-32602` で拒否する。受理した workspace-relative path は空白、Unicode、POSIX でファイル名文字となる backslash を保持し、Windows の path separator だけを `/` に正規化する。
- `protocolVersion` は**ハードコードではなく交渉**で決まる(#1554)。サーバーは
`McpServer.SupportedProtocolVersions`(新しい順: `2025-06-18`,
`2025-03-26`, `2024-11-05`)を保持し、`initialize` パラメータから
Expand Down
18 changes: 18 additions & 0 deletions changelog.d/unreleased/4899.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
category: fixed
issues:
- 4899
affected:
- src/CodeIndex/Mcp/McpPathBoundary.cs
- src/CodeIndex/Mcp/McpServer.Prompts.cs
- tests/CodeIndex.Tests/McpServerTests.cs
- DEVELOPER_GUIDE.md
---

## English

- **The `summarize_file` MCP prompt now requires a safe workspace-relative path (#4899)** — prompt discovery marks `path` as required, and `prompts/get` returns typed invalid-params errors for missing, blank, non-string, absolute, control-character-containing, or traversing paths instead of emitting an unresolved `<path>` placeholder.

## 日本語

- **`summarize_file` MCP prompt が安全な workspace-relative path を必須とするようになりました (#4899)** — prompt discovery は `path` を必須として公開し、`prompts/get` は未解決の `<path>` placeholder を出力せず、欠落、空白、文字列以外、絶対 path、制御文字を含む path、traversal path に対して型付き invalid-params error を返します。
11 changes: 9 additions & 2 deletions src/CodeIndex/Mcp/McpPathBoundary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -180,15 +180,22 @@ private static string CreateRootIdentityToken(FileIndexer.FileIdentity identity)
}
}

internal static bool TryValidateWorkspaceRelativePath(string value, int maxLength, string propertyName, out string? error)
internal static bool TryValidateWorkspaceRelativePath(
string value,
int maxLength,
string propertyName,
out string? error,
bool backslashIsSeparator = true)
{
if (value.Length > maxLength)
{
error = $"Parameter \"{propertyName}\" must be no longer than {maxLength} characters.";
return false;
}

var normalized = value.Replace("\\", "/", StringComparison.Ordinal);
var normalized = backslashIsSeparator
? value.Replace("\\", "/", StringComparison.Ordinal)
: value;
if (value.IndexOf("\0", StringComparison.Ordinal) >= 0
|| normalized.StartsWith("/", StringComparison.Ordinal)
|| PathUriNormalizer.HasWindowsDrivePrefix(normalized)
Expand Down
81 changes: 76 additions & 5 deletions src/CodeIndex/Mcp/McpServer.Prompts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ private JsonNode HandlePromptsList(JsonNode? id)
{
var prompts = new JsonArray
{
CreatePromptDefinition("summarize_file", "Summarize the API surface and responsibilities of an indexed file.", "path", "Indexed file path to summarize."),
CreatePromptDefinition("summarize_file", "Summarize the API surface and responsibilities of an indexed file.", "path", "Required workspace-relative indexed file path to summarize.", argumentRequired: true),
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."),
Expand Down Expand Up @@ -70,15 +70,81 @@ private JsonNode HandlePromptsGet(JsonNode? id, JsonNode? getParams)
return McpBoundedText.ForDisplay(s, McpBoundedText.MaxPromptArgumentChars).Text;
}

JsonNode? ReadRequiredWorkspaceRelativePathArg(string key, out string path)
{
path = string.Empty;
if (args == null
|| !args.TryGetPropertyValue(key, out var node)
|| node is null)
{
return CreateErrorResponse(hasId: true, id: id, code: -32602, message: $"Missing required prompt argument: {key}",
category: McpErrorEnvelope.CategoryMissingParameter,
suggestion: $"prompts/get for `summarize_file` requires a workspace-relative string in `params.arguments.{key}`.",
retrySafe: false,
extraData: new JsonObject { ["parameter"] = key });
}

if (node is not JsonValue value || !value.TryGetValue<string>(out var rawPath))
{
return CreateErrorResponse(hasId: true, id: id, code: -32602, message: $"Prompt argument '{key}' must be a string",
category: McpErrorEnvelope.CategoryInvalidArgument,
suggestion: $"Pass a workspace-relative indexed file path as `params.arguments.{key}`.",
retrySafe: false,
extraData: new JsonObject { ["parameter"] = key });
}

if (string.IsNullOrWhiteSpace(rawPath))
{
return CreateErrorResponse(hasId: true, id: id, code: -32602, message: $"Prompt argument '{key}' cannot be empty or whitespace-only",
category: McpErrorEnvelope.CategoryInvalidArgument,
suggestion: $"Pass a non-empty workspace-relative indexed file path as `params.arguments.{key}`.",
retrySafe: false,
extraData: new JsonObject { ["parameter"] = key });
}

if (rawPath.Length > McpBoundedText.MaxPromptArgumentChars)
{
return CreatePromptStringTooLongError(id, parameterName: key, value: rawPath, maxChars: McpBoundedText.MaxPromptArgumentChars,
messagePrefix: $"Prompt argument '{key}' is too long",
suggestion: "Shorten prompt arguments before calling prompts/get; long source or path context should be fetched with tools instead.");
}

if (rawPath.Any(static character => char.IsControl(character)))
{
return CreateErrorResponse(hasId: true, id: id, code: -32602, message: $"Prompt argument '{key}' must not contain control characters",
category: McpErrorEnvelope.CategoryInvalidArgument,
suggestion: $"Pass a workspace-relative indexed file path without control characters in `params.arguments.{key}`.",
retrySafe: false,
extraData: new JsonObject { ["parameter"] = key });
}

if (!McpPathBoundary.TryValidateWorkspaceRelativePath(
rawPath,
McpBoundedText.MaxPromptArgumentChars,
key,
out var validationError,
backslashIsSeparator: OperatingSystem.IsWindows()))
{
return CreateErrorResponse(hasId: true, id: id, code: -32602, message: validationError!,
category: McpErrorEnvelope.CategoryInvalidArgument,
suggestion: $"Pass a workspace-relative indexed file path without absolute roots, drive prefixes, NUL bytes, or `..` traversal in `params.arguments.{key}`.",
retrySafe: false,
extraData: new JsonObject { ["parameter"] = key });
}

path = FileIndexer.NormalizePathSeparators(rawPath);
return null;
}

string text;
switch (name)
{
case "summarize_file":
{
var path = ReadArg("path", out var argumentError);
var argumentError = ReadRequiredWorkspaceRelativePathArg("path", out var path);
if (argumentError is not null)
return argumentError;
text = $"Use the `outline` tool for `{path ?? "<path>"}`, then use `excerpt` only for the ranges needed to summarize public API, key symbols, and responsibilities.";
text = $"Use the `outline` tool for `{path}`, then use `excerpt` only for the ranges needed to summarize public API, key symbols, and responsibilities.";
break;
}
case "find_unused":
Expand Down Expand Up @@ -199,7 +265,12 @@ private async Task<JsonNode> HandleLoggingSetLevelAsync(JsonNode? id, JsonNode?
return CreateSuccessResponse(true, id, new JsonObject());
}

private static JsonObject CreatePromptDefinition(string name, string description, string argumentName, string argumentDescription)
private static JsonObject CreatePromptDefinition(
string name,
string description,
string argumentName,
string argumentDescription,
bool argumentRequired = false)
=> new()
{
["name"] = name,
Expand All @@ -210,7 +281,7 @@ private static JsonObject CreatePromptDefinition(string name, string description
{
["name"] = argumentName,
["description"] = argumentDescription,
["required"] = false,
["required"] = argumentRequired,
},
},
};
Expand Down
Loading
Loading