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
4 changes: 2 additions & 2 deletions changelog.d/unreleased/3118.security.md
Original file line number Diff line number Diff line change
Expand Up @@ -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も返します。
4 changes: 2 additions & 2 deletions changelog.d/unreleased/3122.security.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 値を返します。
12 changes: 9 additions & 3 deletions src/CodeIndex/Mcp/McpServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -2310,6 +2314,7 @@ private static BoundedMcpText BoundClientIdentityForDisplay(string value)
/// </summary>
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.
Expand All @@ -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,
Expand All @@ -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
Expand Down
34 changes: 34 additions & 0 deletions tests/CodeIndex.Tests/McpServerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -878,6 +878,21 @@ public void ResourcesList_ReturnsIndexedFilesAsResources()
Assert.Equal("text/x-csharp", resource["mimeType"]!.GetValue<string>());
}

[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<string>() == longPath);
Assert.All(resources, resource =>
Assert.True(resource!["uri"]!.GetValue<string>().Length <= McpBoundedText.MaxResourceUriChars));
}

[Fact]
public void ResourcesRead_ReturnsIndexedFileContent()
{
Expand Down Expand Up @@ -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<string>());
Assert.Contains(display.Text, log);
var data = response["error"]!["data"]!;
Assert.Equal(display.Text, data["tool"]!.GetValue<string>());
Assert.Equal(tool.Length, data["tool_length"]!.GetValue<int>());
Assert.True(data["tool_truncated"]!.GetValue<bool>());
}

[Fact]
public void ToolResult_DatabaseMissing_CarriesEnvelopeOnStructuredContent()
{
Expand Down
Loading