diff --git a/changelog.d/unreleased/3118.security.md b/changelog.d/unreleased/3118.security.md index fd70f5eb6a..636a858c02 100644 --- a/changelog.d/unreleased/3118.security.md +++ b/changelog.d/unreleased/3118.security.md @@ -12,8 +12,8 @@ affected: ## English -- **MCP unknown tool diagnostics truncate tool names (#3118)** — unknown tool names now use bounded display values in JSON-RPC errors, batch slot results, telemetry, and audit logs, with original length metadata when truncation occurs. +- **MCP unknown tool diagnostics truncate tool names (#3118)** — unknown tool names now use bounded display values in JSON-RPC errors, rate-limit responses, batch slot results, telemetry, and audit logs, with original length metadata when truncation occurs. ## 日本語 -- **MCP の未知tool診断でtool名を切り詰めるようになりました (#3118)** — 未知tool名は JSON-RPC error、batch slot結果、telemetry、audit log で bounded display 値を使い、切り詰め時は元の長さmetadataも返します。 +- **MCP の未知tool診断でtool名を切り詰めるようになりました (#3118)** — 未知tool名は JSON-RPC error、rate-limit response、batch slot結果、telemetry、audit log で bounded display 値を使い、切り詰め時は元の長さmetadataも返します。 diff --git a/changelog.d/unreleased/3122.security.md b/changelog.d/unreleased/3122.security.md index 243500350d..873ffcc069 100644 --- a/changelog.d/unreleased/3122.security.md +++ b/changelog.d/unreleased/3122.security.md @@ -10,8 +10,8 @@ affected: ## English -- **MCP resources/read caps resource URIs before parsing (#3122)** — `resources/read` now rejects oversized URIs before URI parsing or unescaping and echoes bounded URI display values in invalid and not-found diagnostics. +- **MCP resource URIs are capped consistently (#3122)** — `resources/read` now rejects oversized URIs before URI parsing or unescaping, `resources/list` omits URIs that would exceed the same readable limit, and invalid/not-found diagnostics echo bounded URI display values. ## 日本語 -- **MCP resources/read が URI parse 前に長さ制限するようになりました (#3122)** — `resources/read` は巨大 URI を URI parse / unescape 前に拒否し、invalid / not-found 診断には bounded URI display 値を返します。 +- **MCP resource URI の長さ制限が一貫するようになりました (#3122)** — `resources/read` は巨大 URI を URI parse / unescape 前に拒否し、`resources/list` は同じ読み取り可能上限を超える URI を返さず、invalid / not-found 診断には bounded URI display 値を返します。 diff --git a/src/CodeIndex/Mcp/McpServer.cs b/src/CodeIndex/Mcp/McpServer.cs index f61e202b88..a28f7d6d9b 100644 --- a/src/CodeIndex/Mcp/McpServer.cs +++ b/src/CodeIndex/Mcp/McpServer.cs @@ -1890,9 +1890,13 @@ private JsonNode HandleResourcesList(JsonNode? id, JsonNode? listParams) var resources = new JsonArray(); foreach (var file in page) { + var uri = BuildResourceUri(file.Path); + if (uri.Length > McpBoundedText.MaxResourceUriChars) + continue; + resources.Add(new JsonObject { - ["uri"] = BuildResourceUri(file.Path), + ["uri"] = uri, ["name"] = file.Path, ["description"] = $"{file.Path} ({file.Lang ?? "unknown"}, {file.Lines} lines)", ["mimeType"] = GetResourceMimeType(file.Lang), @@ -2310,6 +2314,7 @@ private static BoundedMcpText BoundClientIdentityForDisplay(string value) /// internal static JsonObject CreateRateLimitedErrorResponse(JsonNode? id, string tool, string caller, long retryAfterMs) { + var toolDisplay = BoundToolNameForDisplay(tool); var callerDisplay = BoundClientIdentityForDisplay(caller); // #1560 contract preserved: `error_category`, `tool`, `caller`, `retry_after_ms`. // #1581 adds the canonical envelope (`category`, `suggestion`, `retry_safe`) alongside. @@ -2318,10 +2323,11 @@ internal static JsonObject CreateRateLimitedErrorResponse(JsonNode? id, string t var extraData = new JsonObject { ["error_category"] = "rate_limited", - ["tool"] = tool, + ["tool"] = toolDisplay.Text, ["caller"] = callerDisplay.Text, ["retry_after_ms"] = retryAfterMs, }; + toolDisplay.AddMetadata(extraData, "tool"); callerDisplay.AddMetadata(extraData, "caller"); var data = McpErrorEnvelope.BuildData( category: McpErrorEnvelope.CategoryRateLimited, @@ -2331,7 +2337,7 @@ internal static JsonObject CreateRateLimitedErrorResponse(JsonNode? id, string t var error = new JsonObject { ["code"] = -32000, - ["message"] = $"Rate limit exceeded for tool '{tool}' (retry after {retryAfterMs} ms).", + ["message"] = $"Rate limit exceeded for tool '{toolDisplay.Text}' (retry after {retryAfterMs} ms).", ["data"] = data, }; var response = new JsonObject diff --git a/tests/CodeIndex.Tests/McpServerTests.cs b/tests/CodeIndex.Tests/McpServerTests.cs index 95eb0e1fef..fdfc1966fa 100644 --- a/tests/CodeIndex.Tests/McpServerTests.cs +++ b/tests/CodeIndex.Tests/McpServerTests.cs @@ -878,6 +878,21 @@ public void ResourcesList_ReturnsIndexedFilesAsResources() Assert.Equal("text/x-csharp", resource["mimeType"]!.GetValue()); } + [Fact] + public void ResourcesList_DoesNotAdvertiseUrisTooLongToRead_Issue3122() + { + var longPath = "src/" + new string('x', McpBoundedText.MaxResourceUriChars) + ".cs"; + InsertIndexedFile(longPath, "csharp", "public class TooLongResource { }"); + var request = JsonNode.Parse("""{"jsonrpc":"2.0","id":1,"method":"resources/list","params":{}}""")!; + + var response = _server.HandleMessage(request)!; + + var resources = response["result"]!["resources"]!.AsArray(); + Assert.DoesNotContain(resources, resource => resource!["name"]!.GetValue() == longPath); + Assert.All(resources, resource => + Assert.True(resource!["uri"]!.GetValue().Length <= McpBoundedText.MaxResourceUriChars)); + } + [Fact] public void ResourcesRead_ReturnsIndexedFileContent() { @@ -11970,6 +11985,25 @@ public void RateLimited_ErrorAndLog_TruncatesCallerIdentity_Issue3120() Assert.Contains(display.Text, log); } + [Fact] + public void RateLimited_ErrorAndLog_TruncatesToolName_Issue3118() + { + var tool = new string('t', McpBoundedText.MaxToolNameChars + 25); + var display = McpBoundedText.ForDisplay(tool, McpBoundedText.MaxToolNameChars); + + var response = McpServer.CreateRateLimitedErrorResponse(null, tool, "client", retryAfterMs: 123); + var log = McpServer.BuildRateLimitedLog(tool, "client", retryAfterMs: 123); + + Assert.DoesNotContain(tool, response.ToJsonString(), StringComparison.Ordinal); + Assert.DoesNotContain(tool, log, StringComparison.Ordinal); + Assert.Contains(display.Text, response["error"]!["message"]!.GetValue()); + Assert.Contains(display.Text, log); + var data = response["error"]!["data"]!; + Assert.Equal(display.Text, data["tool"]!.GetValue()); + Assert.Equal(tool.Length, data["tool_length"]!.GetValue()); + Assert.True(data["tool_truncated"]!.GetValue()); + } + [Fact] public void ToolResult_DatabaseMissing_CarriesEnvelopeOnStructuredContent() {