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
17 changes: 17 additions & 0 deletions changelog.d/unreleased/1732.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
category: added
issues:
- 1732
affected:
- src/CodeIndex/Mcp/McpToolHandlers.cs
- src/CodeIndex/Mcp/McpToolDefinitions.cs
- tests/CodeIndex.Tests/McpServerTests.cs
---

## English

- **Added MCP `countOnly` probing for large result sets (#1732)** — `search`, `references`, `callers`, `callees`, and `impact_analysis` can now return count metadata plus a small top-file histogram without row payloads.

## 日本語

- **大きな結果セット向けに MCP `countOnly` プローブを追加しました (#1732)** — `search`、`references`、`callers`、`callees`、`impact_analysis` が、行 payload を返さずに件数 metadata と小さな top-file histogram を返せるようになりました。
17 changes: 17 additions & 0 deletions changelog.d/unreleased/1745.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
category: fixed
issues:
- 1745
affected:
- src/CodeIndex/Mcp/McpServer.cs
- src/CodeIndex/Mcp/McpToolHandlers.cs
- tests/CodeIndex.Tests/McpServerTests.cs
---

## English

- **MCP tool responses now have a server-wide byte ceiling (#1745)** — oversized responses fail with structured `response_too_large` metadata instead of being serialized and written unbounded.

## 日本語

- **MCP ツール応答にサーバー全体の byte 上限を追加しました (#1745)** — 過大な応答は無制限に直列化・書き込みされず、構造化された `response_too_large` metadata を持つエラーになります。
16 changes: 16 additions & 0 deletions changelog.d/unreleased/1808.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: fixed
issues:
- 1808
affected:
- src/CodeIndex/Mcp/McpToolHandlers.cs
- tests/CodeIndex.Tests/McpServerTests.cs
---

## English

- **MCP search and graph responses now expose result envelope metadata (#1808)** — `search`, `definition`, `references`, `callers`, and `callees` include `truncated` and `total` fields so clients can distinguish complete result sets from server-limited responses.

## 日本語

- **MCP の検索・グラフ応答が結果 envelope metadata を返すようになりました (#1808)** — `search`、`definition`、`references`、`callers`、`callees` に `truncated` と `total` を追加し、クライアントが完全な結果とサーバー上限で切られた応答を区別できるようにしました。
17 changes: 17 additions & 0 deletions changelog.d/unreleased/2027.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
category: fixed
issues:
- 2027
affected:
- src/CodeIndex/Cli/SearchSnippetFormatter.cs
- src/CodeIndex/Mcp/McpToolHandlers.cs
- tests/CodeIndex.Tests/SearchSnippetFormatterTests.cs
---

## English

- **Search snippet compaction now supports lazy enumeration (#2027)** — MCP search uses a formatter iterator so compact snippet rows are produced only as the response array is built.

## 日本語

- **検索スニペットの compact 化が lazy enumeration に対応しました (#2027)** — MCP search は formatter iterator を使い、応答配列を組み立てるタイミングでのみ compact snippet 行を生成します。
18 changes: 18 additions & 0 deletions changelog.d/unreleased/2029.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
category: fixed
issues:
- 2029
affected:
- src/CodeIndex/Database/DbReader.cs
- src/CodeIndex/Database/DbReader.GraphQueries.cs
- src/CodeIndex/Mcp/McpToolHandlers.cs
- src/CodeIndex/Models/QueryResults.cs
---

## English

- **Bounded grouped reference-kind aggregates in MCP graph results (#2029)** — caller and callee rows now cap parsed `GROUP_CONCAT` reference-kind aggregates and surface `aggregate_truncated` when an aggregate is trimmed before JSON serialization.

## 日本語

- **MCP graph 結果の reference-kind 集約に上限を設けました (#2029)** — caller / callee 行は `GROUP_CONCAT` された reference-kind 集約の解析量を制限し、JSON 直列化前に切り詰めた場合は `aggregate_truncated` を返します。
6 changes: 6 additions & 0 deletions src/CodeIndex/Cli/SearchSnippetFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ public static CompactSearchResult ToCompactResult(SearchResult result, string qu
};
}

public static IEnumerable<CompactSearchResult> ToCompactResults(IEnumerable<SearchResult> results, string query, int maxLines = DefaultSnippetLines, bool caseSensitive = false, int maxLineWidth = LineWidthFormatter.DefaultMaxLineWidth, string? lang = null, SearchSnippetFocusMode focusMode = SearchSnippetFocusMode.Quality)
{
foreach (var result in results)
yield return ToCompactResult(result, query, maxLines, caseSensitive, maxLineWidth, lang ?? result.Lang, focusMode);
}

public static SearchSnippetExcerpt BuildExcerpt(string content, string query, int absoluteStartLine, int maxLines = DefaultSnippetLines, bool caseSensitive = false, int maxLineWidth = LineWidthFormatter.DefaultMaxLineWidth, string? lang = null, SearchSnippetFocusMode focusMode = SearchSnippetFocusMode.Quality)
{
maxLines = ClampSnippetLines(maxLines);
Expand Down
14 changes: 10 additions & 4 deletions src/CodeIndex/Database/DbReader.GraphQueries.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,10 @@ FROM logical_references r
while (reader.TrackedRead())
{
var primaryKind = reader.GetString(5);
var kinds = ParseDistinctReferenceKinds(GetNullableString(reader, 8), primaryKind);
var counts = ParseReferenceKindCounts(GetNullableString(reader, 9), primaryKind, reader.GetInt32(7));
var kindAggregate = TruncateReferenceKindAggregate(GetNullableString(reader, 8), out var kindsTruncated);
var countAggregate = TruncateReferenceKindAggregate(GetNullableString(reader, 9), out var countsTruncated);
var kinds = ParseDistinctReferenceKinds(kindAggregate, primaryKind);
var counts = ParseReferenceKindCounts(countAggregate, primaryKind, reader.GetInt32(7));
results.Add(new CallerResult
{
Path = reader.GetString(0),
Expand All @@ -169,6 +171,7 @@ FROM logical_references r
ReferenceKinds = kinds,
HasMixedReferenceKinds = kinds.Count > 1,
ReferenceKindCounts = counts,
AggregateTruncated = kindsTruncated || countsTruncated,
ReferenceWeightScore = reader.GetDouble(10),
FirstLine = reader.GetInt32(6),
ReferenceCount = reader.GetInt32(7),
Expand Down Expand Up @@ -484,8 +487,10 @@ FROM logical_references r
while (reader.TrackedRead())
{
var primaryKind = reader.GetString(5);
var kinds = ParseDistinctReferenceKinds(GetNullableString(reader, 8), primaryKind);
var counts = ParseReferenceKindCounts(GetNullableString(reader, 9), primaryKind, reader.GetInt32(7));
var kindAggregate = TruncateReferenceKindAggregate(GetNullableString(reader, 8), out var kindsTruncated);
var countAggregate = TruncateReferenceKindAggregate(GetNullableString(reader, 9), out var countsTruncated);
var kinds = ParseDistinctReferenceKinds(kindAggregate, primaryKind);
var counts = ParseReferenceKindCounts(countAggregate, primaryKind, reader.GetInt32(7));
results.Add(new CalleeResult
{
Path = reader.GetString(0),
Expand All @@ -497,6 +502,7 @@ FROM logical_references r
ReferenceKinds = kinds,
HasMixedReferenceKinds = kinds.Count > 1,
ReferenceKindCounts = counts,
AggregateTruncated = kindsTruncated || countsTruncated,
ReferenceWeightScore = reader.GetDouble(10),
FirstLine = reader.GetInt32(6),
ReferenceCount = reader.GetInt32(7),
Expand Down
11 changes: 11 additions & 0 deletions src/CodeIndex/Database/DbReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public readonly record struct SqlGraphContractSignal(
public partial class DbReader
{
public const string VerifyFoldReadyRowsEnvironmentVariable = "CDIDX_VERIFY_FOLD_READY_ROWS";
internal const int MaxReferenceKindAggregateCharacters = 16 * 1024;

private static readonly Regex ImpactSignatureIdentifierRegex = new(@"[\p{L}_][\p{L}\p{Nd}_]*", RegexOptions.Compiled);
private static readonly Regex CSharpUsingStaticImportRegex = new(@"^\s*(?:global\s+)?using\s+static\s+(?<target>[^;]+)", RegexOptions.Compiled);
Expand Down Expand Up @@ -332,6 +333,16 @@ private static IReadOnlyList<string> ParseDistinctReferenceKinds(string? aggrega
return set.ToArray();
}

private static string? TruncateReferenceKindAggregate(string? aggregate, out bool truncated)
{
truncated = false;
if (aggregate == null || aggregate.Length <= MaxReferenceKindAggregateCharacters)
return aggregate;

truncated = true;
return aggregate[..MaxReferenceKindAggregateCharacters];
}

public DbReader(SqliteConnection connection, bool isReadOnly = false)
: this(connection, isReadOnly, schemaCache: null, cancellation: CancellationToken.None)
{
Expand Down
44 changes: 42 additions & 2 deletions src/CodeIndex/Mcp/McpServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ public partial class McpServer : IDisposable
private const int MaxContextLines = 1000;
internal const int MaxLineCharacterCount = 1_000_000;
internal const int MaxLineByteLength = 1_048_576;
internal const int DefaultMaxResponseBytes = 10 * 1024 * 1024;
private const string MaxResponseBytesEnvVar = "CDIDX_MCP_RESPONSE_MAX_BYTES";
internal const int MaxJsonDepth = 32;
internal const int MaxBatchRequestCount = 100;
// Stdio buffer for the JSON-RPC loop. Sized to fit typical large MCP payloads (e.g. batch_query)
Expand Down Expand Up @@ -728,7 +730,13 @@ private string SerializeResponseOrFallback(JsonNode response, bool hasId, JsonNo
{
try
{
return _serializeResponse(response);
var serialized = _serializeResponse(response);
var responseBytes = Encoding.UTF8.GetByteCount(serialized);
var responseLimit = GetMaxResponseBytes();
if (responseBytes <= responseLimit)
return serialized;

return CreateResponseTooLargeError(hasId, id, responseBytes, responseLimit).ToJsonString(_jsonOptions);
}
catch (Exception ex)
{
Expand Down Expand Up @@ -2232,7 +2240,39 @@ private static JsonObject CreateToolResult(JsonNode? id, string text, JsonNode?
};
if (structuredContent != null)
result["structuredContent"] = structuredContent;
return CreateSuccessResponse(true, id, result);
var response = CreateSuccessResponse(true, id, result);
var responseBytes = Encoding.UTF8.GetByteCount(response.ToJsonString());
var responseLimit = GetMaxResponseBytes();
if (responseBytes <= responseLimit)
return response;

return CreateResponseTooLargeError(true, id, responseBytes, responseLimit);
}

private static JsonObject CreateResponseTooLargeError(bool hasId, JsonNode? id, int responseBytes, int responseLimit)
{
return CreateErrorResponse(
hasId: hasId,
id: id,
code: -32603,
message: $"MCP response exceeded the server byte limit ({responseBytes} > {responseLimit}). Narrow the query or lower the result limit.",
category: McpErrorEnvelope.CategoryInvalidArgument,
suggestion: "Narrow the query, add path/language filters, lower limit, or use countOnly for a summary-first probe.",
retrySafe: false,
extraData: new JsonObject
{
["reason"] = "response_too_large",
["limit_bytes"] = responseLimit,
["actual_bytes"] = responseBytes,
});
}

private static int GetMaxResponseBytes()
{
var configured = Environment.GetEnvironmentVariable(MaxResponseBytesEnvVar);
if (int.TryParse(configured, out var limit) && limit > 0)
return limit;
return DefaultMaxResponseBytes;
}

/// <summary>
Expand Down
15 changes: 10 additions & 5 deletions src/CodeIndex/Mcp/McpToolDefinitions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ private JsonNode HandleToolsList(JsonNode? id)
["noDedup"] = new JsonObject { ["type"] = "boolean", ["description"] = "Disable overlapping-chunk deduplication for raw results", ["default"] = false },
["exactSubstring"] = new JsonObject { ["type"] = "boolean", ["description"] = "Preferred explicit name for search's exact mode: case-sensitive exact substring match (bypasses FTS5).", ["default"] = false },
["exact"] = new JsonObject { ["type"] = "boolean", ["description"] = "Backward-compatible alias for `exactSubstring`.", ["default"] = false },
["prefix"] = new JsonObject { ["type"] = "boolean", ["description"] = "Opt into FTS5 prefix expansion for every token in `query`. Cannot be combined with `exact`/`exactSubstring`.", ["default"] = false }
["prefix"] = new JsonObject { ["type"] = "boolean", ["description"] = "Opt into FTS5 prefix expansion for every token in `query`. Cannot be combined with `exact`/`exactSubstring`.", ["default"] = false },
["countOnly"] = new JsonObject { ["type"] = "boolean", ["description"] = "Return only count metadata and a small top-file histogram; omit row payloads.", ["default"] = false }
},
["required"] = new JsonArray { "query" }
},
Expand Down Expand Up @@ -87,7 +88,8 @@ private JsonNode HandleToolsList(JsonNode? id)
["excludeTests"] = new JsonObject { ["type"] = "boolean", ["description"] = "Exclude likely test files", ["default"] = false },
["includeGenerated"] = new JsonObject { ["type"] = "boolean", ["description"] = "Include files detected as generated code", ["default"] = false },
["exactName"] = new JsonObject { ["type"] = "boolean", ["description"] = "Preferred explicit name for exact referenced-symbol equality. Uses NFKC + Unicode CaseFold so `Run` no longer matches `RunAsync`.", ["default"] = false },
["exact"] = new JsonObject { ["type"] = "boolean", ["description"] = "Backward-compatible alias for `exactName`.", ["default"] = false }
["exact"] = new JsonObject { ["type"] = "boolean", ["description"] = "Backward-compatible alias for `exactName`.", ["default"] = false },
["countOnly"] = new JsonObject { ["type"] = "boolean", ["description"] = "Return only count metadata and a small top-file histogram; omit row payloads.", ["default"] = false }
},
["required"] = new JsonArray { "query" }
},
Expand All @@ -110,7 +112,8 @@ private JsonNode HandleToolsList(JsonNode? id)
["excludeTests"] = new JsonObject { ["type"] = "boolean", ["description"] = "Exclude likely test files", ["default"] = false },
["includeGenerated"] = new JsonObject { ["type"] = "boolean", ["description"] = "Include files detected as generated code", ["default"] = false },
["exactName"] = new JsonObject { ["type"] = "boolean", ["description"] = "Preferred explicit name for exact callee-name equality. Uses NFKC + Unicode CaseFold so `Run` no longer matches `RunAsync`.", ["default"] = false },
["exact"] = new JsonObject { ["type"] = "boolean", ["description"] = "Backward-compatible alias for `exactName`.", ["default"] = false }
["exact"] = new JsonObject { ["type"] = "boolean", ["description"] = "Backward-compatible alias for `exactName`.", ["default"] = false },
["countOnly"] = new JsonObject { ["type"] = "boolean", ["description"] = "Return only count metadata and a small top-file histogram; omit row payloads.", ["default"] = false }
},
["required"] = new JsonArray { "query" }
},
Expand All @@ -133,7 +136,8 @@ private JsonNode HandleToolsList(JsonNode? id)
["excludeTests"] = new JsonObject { ["type"] = "boolean", ["description"] = "Exclude likely test files", ["default"] = false },
["includeGenerated"] = new JsonObject { ["type"] = "boolean", ["description"] = "Include files detected as generated code", ["default"] = false },
["exactName"] = new JsonObject { ["type"] = "boolean", ["description"] = "Preferred explicit name for exact caller/container equality. Uses NFKC + Unicode CaseFold so `Run` no longer matches `RunAsync`.", ["default"] = false },
["exact"] = new JsonObject { ["type"] = "boolean", ["description"] = "Backward-compatible alias for `exactName`.", ["default"] = false }
["exact"] = new JsonObject { ["type"] = "boolean", ["description"] = "Backward-compatible alias for `exactName`.", ["default"] = false },
["countOnly"] = new JsonObject { ["type"] = "boolean", ["description"] = "Return only count metadata and a small top-file histogram; omit row payloads.", ["default"] = false }
},
["required"] = new JsonArray { "query" }
},
Expand Down Expand Up @@ -280,7 +284,8 @@ private JsonNode HandleToolsList(JsonNode? id)
["excludePaths"] = new JsonObject { ["type"] = "array", ["items"] = new JsonObject { ["type"] = "string" }, ["description"] = "Exclude any paths containing these texts" },
["excludeTests"] = new JsonObject { ["type"] = "boolean", ["description"] = "Exclude likely test files", ["default"] = false },
["includeGenerated"] = new JsonObject { ["type"] = "boolean", ["description"] = "Include files detected as generated code", ["default"] = false },
["withPaths"] = new JsonObject { ["type"] = "boolean", ["description"] = "When true, each caller carries a `paths` array of shortest call chains [resolvedRoot, intermediate..., callerName]; diamond convergence surfaces every shortest route (per-row cap; `pathsTruncated` flag indicates overflow).", ["default"] = false }
["withPaths"] = new JsonObject { ["type"] = "boolean", ["description"] = "When true, each caller carries a `paths` array of shortest call chains [resolvedRoot, intermediate..., callerName]; diamond convergence surfaces every shortest route (per-row cap; `pathsTruncated` flag indicates overflow).", ["default"] = false },
["countOnly"] = new JsonObject { ["type"] = "boolean", ["description"] = "Return only count metadata and a small top-file histogram; omit caller and file-impact row payloads.", ["default"] = false }
},
["required"] = new JsonArray { "query" }
},
Expand Down
Loading
Loading