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
2 changes: 1 addition & 1 deletion AGENT_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ CI watching must be bounded. Do not loop indefinitely.
- `status` also surfaces filesystem case-sensitivity via `path_case_sensitive`, stamped on every successful `cdidx index` run (full scan AND partial update, plus MCP-driven indexes) from `core.ignorecase` + a live filesystem probe. `true` means the volume is case-sensitive (`Foo.cs` and `foo.cs` are distinct); `false` means case-insensitive. Omitted on legacy DBs that predate the stamp. Use it to audit path-equality decisions on case-sensitive APFS, WSL NTFS / dev-drive, and ReFS mounts where the prior OS-keyed heuristic could mis-classify the workspace (#1546).
- `status` also surfaces Linux mandatory-access-control context via `mac_profile` when `/proc/self/attr/current` or `/proc/self/attr/exec` indicates an AppArmor or SELinux profile. It is omitted on non-Linux hosts, unconstrained processes, or unreadable proc attributes (#1768).
- `status` also surfaces DB/WAL size, per-language symbol-kind histograms, current process heap/GC/working-set metrics, and the last successful index run metadata. `process` is captured at status-call time; `last_index_run` is persisted at the end of successful CLI and MCP index runs and can include a peak-memory summary when CLI `--memory-trace` was used. `last_workspace_freshened_at` is the latest successful index/update timestamp and can be newer than `indexed_at` when a partial or no-op update confirms freshness without rewriting indexed file rows.
- MCP `status` also surfaces session diagnostics via `mcp_session`. It is not persisted DB state; it includes the current `log_level`, captured `roots`, optional `client_info`, and optional `client_capabilities`.
- MCP `status` also surfaces session diagnostics via `mcp_session`. It is not persisted DB state; it includes the current `log_level`, bounded captured `roots`, optional `client_info`, and bounded optional `client_capabilities`. When advertised roots are capped, `roots_truncated`, `root_count`, `root_limit`, and `root_uri_length_limit` describe the truncation. When client capabilities are capped, `client_capabilities_truncated`, `client_capabilities_truncation_reason`, `client_capabilities_serialized_bytes`, `client_capabilities_byte_limit`, and `client_capabilities_depth_limit` describe the retained diagnostic subset.
- Keep `README.md`, `DEVELOPER_GUIDE.md`, and this file synchronized if this contract changes.

## Reference Extraction
Expand Down
4 changes: 2 additions & 2 deletions DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -963,7 +963,7 @@ For the AI agent search-rule template, see [AI Integration](USER_GUIDE.md#ai-int
| Version and forward compatibility | `index_writer_version`, `index_newer_than_reader`, `index_newer_than_reader_reason`. |
| Unknown-extension and runtime diagnostics | `unknown_extension_file_count`, `unknown_extension_files`, `unknown_extension_files_truncated`, `unknown_extension_file_path_limit`, `extractors`, `path_case_sensitive`, `data_dir_mode`, `mac_profile`, `stale_after_seconds`, `index_age_seconds`. |
| Remediation fields | `degraded_root_cause`, `degraded_reason`, `recommended_action`, `alternative_action`, `readiness_degradations`. |
| MCP-only session diagnostics | `mcp_session`, which is session-scoped diagnostics rather than persisted DB state. It contains `log_level`, `roots`, optional `client_info`, and optional `client_capabilities`. |
| MCP-only session diagnostics | `mcp_session`, which is session-scoped diagnostics rather than persisted DB state. It contains `log_level`, bounded `roots`, optional `client_info`, and bounded optional `client_capabilities`. When advertised roots are capped, `roots_truncated`, `root_count`, `root_limit`, and `root_uri_length_limit` describe the truncation. When client capabilities are capped, `client_capabilities_truncated`, `client_capabilities_truncation_reason`, `client_capabilities_serialized_bytes`, `client_capabilities_byte_limit`, and `client_capabilities_depth_limit` describe the retained diagnostic subset. |
| Documentation sync | Keep this list synchronized with `README.md` and `AGENT_GUIDE.md`; `DocumentationStatusContractTests` fails when any required field is missing from one of those docs. |

Runtime diagnostic subcontracts:
Expand Down Expand Up @@ -2974,7 +2974,7 @@ AI エージェント向け検索ルールのテンプレートについては
| version / forward compatibility | `index_writer_version`, `index_newer_than_reader`, `index_newer_than_reader_reason`。 |
| unknown-extension / runtime diagnostics | `unknown_extension_file_count`, `unknown_extension_files`, `unknown_extension_files_truncated`, `unknown_extension_file_path_limit`, `extractors`, `path_case_sensitive`, `data_dir_mode`, `mac_profile`, `stale_after_seconds`, `index_age_seconds`。 |
| remediation fields | `degraded_root_cause`, `degraded_reason`, `recommended_action`, `alternative_action`, `readiness_degradations`。 |
| MCP-only session diagnostics | `mcp_session`。これは persisted DB state ではなく session-scoped diagnostics で、`log_level`、`roots`、任意の `client_info`、任意の `client_capabilities` を含みます。 |
| MCP-only session diagnostics | `mcp_session`。これは persisted DB state ではなく session-scoped diagnostics で、`log_level`、上限付きの `roots`、任意の `client_info`、上限付きの任意の `client_capabilities` を含みます。advertised root が切り詰められた場合は `roots_truncated`、`root_count`、`root_limit`、`root_uri_length_limit` が切り詰め内容を示します。client capabilities が切り詰められた場合は `client_capabilities_truncated`、`client_capabilities_truncation_reason`、`client_capabilities_serialized_bytes`、`client_capabilities_byte_limit`、`client_capabilities_depth_limit` が保持された診断 subset を示します。 |
| documentation sync | この一覧は `README.md` と `AGENT_GUIDE.md` と同期してください。必須 field がそれらの docs から欠けると `DocumentationStatusContractTests` が失敗します。 |

runtime diagnostic subcontract:
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ After a current full-repository scan, `unknown_extension_file_count` reports how

`hooks[]` includes metadata-only hook candidates and `callback_budget_ms`; `status` does not load hook assemblies. `CDIDX_HOOK_CALLBACK_BUDGET_MS` bounds each post-extraction hook callback in milliseconds (default: 5000); callbacks that exceed the budget emit sanitized index warnings, drop timed-out mutations, and disable that hook for the current index run.

For MCP `status`, `mcp_session` is session-scoped diagnostic data rather than persisted index state. It includes `log_level`, `roots`, optional `client_info`, and optional `client_capabilities`.
For MCP `status`, `mcp_session` is session-scoped diagnostic data rather than persisted index state. It includes `log_level`, `roots`, optional `client_info`, and optional `client_capabilities`. Root diagnostics are bounded; when advertised roots are capped, `roots_truncated`, `root_count`, `root_limit`, and `root_uri_length_limit` describe the truncation. Client capabilities are capped by serialized size and depth; when capped, `client_capabilities_truncated`, `client_capabilities_truncation_reason`, `client_capabilities_serialized_bytes`, `client_capabilities_byte_limit`, and `client_capabilities_depth_limit` describe the retained diagnostic subset.

`process` is captured at status-call time and includes heap, GC collection, and working-set counters. `last_index_run` is persisted by successful CLI and MCP index runs with the run mode, duration, file counts, byte count, row-change counts, and optional peak-memory summary from CLI `--memory-trace`. `last_workspace_freshened_at` is the latest successful index/update run timestamp and can be newer than `indexed_at` when a partial or no-op update confirms freshness without rewriting indexed file rows.

Expand Down Expand Up @@ -531,7 +531,7 @@ readiness field のいずれかが degraded の場合、`degraded_root_cause`

`hooks[]` は metadata-only の hook candidate と `callback_budget_ms` を含み、`status` は hook assembly を読み込みません。`CDIDX_HOOK_CALLBACK_BUDGET_MS` は post-extraction hook callback ごとの上限ミリ秒を指定します(既定値: 5000)。上限を超えた callback は sanitized index warning を出し、timeout した変更を捨て、その index run 中は該当 hook を無効化します。

MCP `status` の `mcp_session` は永続化された index 状態ではなく、セッション単位の診断情報です。`log_level`、`roots`、任意の `client_info`、任意の `client_capabilities` を含みます。
MCP `status` の `mcp_session` は永続化された index 状態ではなく、セッション単位の診断情報です。`log_level`、`roots`、任意の `client_info`、任意の `client_capabilities` を含みます。root 診断は上限付きで、advertised root が切り詰められた場合は `roots_truncated`、`root_count`、`root_limit`、`root_uri_length_limit` が切り詰め内容を示します。client capabilities は serialized size と depth で上限管理され、切り詰められた場合は `client_capabilities_truncated`、`client_capabilities_truncation_reason`、`client_capabilities_serialized_bytes`、`client_capabilities_byte_limit`、`client_capabilities_depth_limit` が保持された診断 subset を示します。

`process` は status 呼び出し時点の heap、GC collection、working-set counters です。`last_index_run` は成功した CLI / MCP index 実行が永続化し、run mode、duration、file counts、byte count、row-change counts、CLI `--memory-trace` 由来の任意の peak-memory summary を含みます。

Expand Down
20 changes: 20 additions & 0 deletions changelog.d/unreleased/3076.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
category: fixed
issues:
- 3076
affected:
- src/CodeIndex/Mcp/McpServer.cs
- src/CodeIndex/Mcp/McpToolHandlers.cs
- tests/CodeIndex.Tests/McpServerTests.cs
- README.md
- DEVELOPER_GUIDE.md
- AGENT_GUIDE.md
---

## English

- **MCP session status now caps retained client roots (#3076)** — `initialize` root diagnostics keep a bounded root list, cap individual root URI text, and expose `roots_truncated` metadata in MCP `status` when advertised roots are shortened.

## 日本語

- **MCP session status が保持する client root を上限付きにしました (#3076)** — `initialize` の root 診断は root 一覧と個別 root URI の長さを上限付きで保持し、advertised root が短縮された場合は MCP `status` に `roots_truncated` metadata を出すようになりました。
20 changes: 20 additions & 0 deletions changelog.d/unreleased/3225.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
category: fixed
issues:
- 3225
affected:
- src/CodeIndex/Mcp/McpServer.cs
- src/CodeIndex/Mcp/McpToolHandlers.cs
- tests/CodeIndex.Tests/McpServerTests.cs
- README.md
- DEVELOPER_GUIDE.md
- AGENT_GUIDE.md
---

## English

- **MCP session status now caps retained client capabilities (#3225)** — `initialize` capabilities diagnostics are bounded by serialized byte size and JSON depth, and MCP `status` now reports explicit `client_capabilities_truncated` metadata when capabilities are shortened.

## 日本語

- **MCP session status が保持する client capabilities を上限付きにしました (#3225)** — `initialize` の capabilities 診断は serialized byte size と JSON depth で上限管理され、capabilities が短縮された場合は MCP `status` に明示的な `client_capabilities_truncated` metadata を出すようになりました。
87 changes: 83 additions & 4 deletions src/CodeIndex/Mcp/McpServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,14 @@ public partial class McpServer : IDisposable
private BoundedMcpText? _clientNameDisplay;
private BoundedMcpText? _clientVersionDisplay;
private JsonNode? _clientCapabilities;
private int? _clientCapabilitiesSerializedBytes;
private string? _clientCapabilitiesTruncationReason;
private bool _clientSupportsRoots;
private bool _clientSupportsSampling;
private JsonArray _clientRoots = [];
private JsonArray _clientRootDiagnostics = [];
private int _clientRootCount;
private bool _clientRootsTruncated;
private string _mcpLogLevel = "info";
// Opaque per-server-instance session id copied into suggestion attribution records (#1873).
// #1873 の提案 attribution 用に保存する、サーバーインスタンス単位の不透明セッションID。
Expand Down Expand Up @@ -176,6 +183,10 @@ public partial class McpServer : IDisposable
internal const int MaxBatchRequestCount = 100;
internal const int MaxRequestIdCharacterCount = 128;
internal const int MaxRequestIdByteLength = 256;
internal const int MaxClientRootCount = 16;
internal const int MaxClientRootUriChars = 512;
internal const int MaxClientCapabilitiesJsonBytes = 8 * 1024;
internal const int MaxClientCapabilitiesDepth = 8;
// Stdio buffer for the JSON-RPC loop. Sized to fit typical large MCP payloads (e.g. batch_query)
// in a single read so the StreamReader does not grow from its 1 KB default toward MaxLineCharacterCount.
// JSON-RPCループのstdioバッファ。大きめのMCPペイロードを1回の読み取りで吸収し、
Expand Down Expand Up @@ -1827,29 +1838,93 @@ private void CaptureClientInfo(JsonNode? initializeParams)
private void CaptureClientSession(JsonNode? initializeParams)
{
_clientCapabilities = null;
_clientRoots = [];
_clientCapabilitiesSerializedBytes = null;
_clientCapabilitiesTruncationReason = null;
_clientSupportsRoots = false;
_clientSupportsSampling = false;
ResetClientRoots();
if (initializeParams is not JsonObject obj)
return;

if (!obj.TryGetPropertyValue("capabilities", out var capabilities))
obj.TryGetPropertyValue("clientCapabilities", out capabilities);
if (capabilities is not null)
_clientCapabilities = JsonNode.Parse(capabilities.ToJsonString());
CaptureClientCapabilities(capabilities);

if (TryReadStringValue(obj["rootUri"]) is { Length: > 0 } rootUri)
_clientRoots.Add(rootUri);
CaptureClientRoot(rootUri);

if (obj["roots"] is JsonArray roots)
{
foreach (var root in roots)
{
var uri = TryReadStringValue(root?["uri"]) ?? TryReadStringValue(root);
if (!string.IsNullOrWhiteSpace(uri))
_clientRoots.Add(uri);
CaptureClientRoot(uri);
}
}
}

private void CaptureClientCapabilities(JsonNode capabilities)
{
CaptureClientCapabilityFlags(capabilities);
var json = capabilities.ToJsonString();
var serializedBytes = Encoding.UTF8.GetByteCount(json);
_clientCapabilitiesSerializedBytes = serializedBytes;
if (serializedBytes > MaxClientCapabilitiesJsonBytes)
{
TruncateClientCapabilities("byte_limit");
return;
}

try
{
_clientCapabilities = JsonNode.Parse(json, documentOptions: new JsonDocumentOptions { MaxDepth = MaxClientCapabilitiesDepth });
}
catch (JsonException)
{
TruncateClientCapabilities("depth_limit");
}
}

private void TruncateClientCapabilities(string reason)
{
_clientCapabilities = new JsonObject();
_clientCapabilitiesTruncationReason = reason;
}

private void CaptureClientCapabilityFlags(JsonNode capabilities)
{
if (capabilities is not JsonObject obj)
return;

_clientSupportsRoots = obj.TryGetPropertyValue("roots", out var roots) && roots is not null;
_clientSupportsSampling = obj.TryGetPropertyValue("sampling", out var sampling) && sampling is not null;
}

private void CaptureClientRoot(string uri)
{
_clientRoots.Add(uri);
_clientRootCount++;
if (_clientRootDiagnostics.Count >= MaxClientRootCount)
{
_clientRootsTruncated = true;
return;
}

var display = McpBoundedText.ForDisplay(uri, MaxClientRootUriChars);
_clientRootDiagnostics.Add(display.Text);
_clientRootsTruncated |= display.Truncated;
}

private void ResetClientRoots()
{
_clientRoots = [];
_clientRootDiagnostics = [];
_clientRootCount = 0;
_clientRootsTruncated = false;
}

internal JsonNode? ClientCapabilitiesForTests => _clientCapabilities is null ? null : JsonNode.Parse(_clientCapabilities.ToJsonString());

internal string[] ClientRootsForTests => _clientRoots
Expand All @@ -1858,6 +1933,10 @@ private void CaptureClientSession(JsonNode? initializeParams)
.Cast<string>()
.ToArray();

internal bool ClientSupportsRootsForTests => _clientSupportsRoots;

internal bool ClientSupportsSamplingForTests => _clientSupportsSampling;

internal string McpLogLevelForTests => _mcpLogLevel;

internal Func<string, JsonObject?, JsonNode?>? ClientRequestHandlerForTests { get; set; }
Expand Down
34 changes: 27 additions & 7 deletions src/CodeIndex/Mcp/McpToolHandlers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2288,14 +2288,21 @@ private JsonNode ExecuteStatus(JsonNode? id)
private JsonObject BuildMcpSessionStatus()
{
var roots = new JsonArray();
foreach (var root in _clientRoots)
foreach (var root in _clientRootDiagnostics)
roots.Add(root?.DeepClone());

var session = new JsonObject
{
["log_level"] = _mcpLogLevel,
["roots"] = roots,
};
if (_clientRootsTruncated)
{
session["roots_truncated"] = true;
session["root_count"] = _clientRootCount;
session["root_limit"] = MaxClientRootCount;
session["root_uri_length_limit"] = MaxClientRootUriChars;
}
if (_clientName is not null || _clientVersion is not null)
{
var clientInfo = new JsonObject();
Expand All @@ -2313,6 +2320,15 @@ private JsonObject BuildMcpSessionStatus()
}
if (_clientCapabilities is not null)
session["client_capabilities"] = _clientCapabilities.DeepClone();
if (_clientCapabilitiesTruncationReason is not null)
{
session["client_capabilities_truncated"] = true;
session["client_capabilities_truncation_reason"] = _clientCapabilitiesTruncationReason;
if (_clientCapabilitiesSerializedBytes is { } serializedBytes)
session["client_capabilities_serialized_bytes"] = serializedBytes;
session["client_capabilities_byte_limit"] = MaxClientCapabilitiesJsonBytes;
session["client_capabilities_depth_limit"] = MaxClientCapabilitiesDepth;
}
return session;
}

Expand Down Expand Up @@ -3713,14 +3729,13 @@ private async Task RefreshClientRootsIfNeededAsync()
if (result?["roots"] is not JsonArray roots)
return;

var refreshed = new JsonArray();
ResetClientRoots();
foreach (var root in roots)
{
var uri = TryReadStringValue(root?["uri"]) ?? TryReadStringValue(root);
if (!string.IsNullOrWhiteSpace(uri))
refreshed.Add(uri);
CaptureClientRoot(uri);
}
_clientRoots = refreshed;
_clientRootsStale = false;
}

Expand Down Expand Up @@ -4967,9 +4982,14 @@ private static string TruncateUtf8(string value, int maxBytes)
}

private bool HasClientCapability(string name)
=> _clientCapabilities is JsonObject obj
&& obj.TryGetPropertyValue(name, out var node)
&& node is not null;
=> name switch
{
"roots" => _clientSupportsRoots,
"sampling" => _clientSupportsSampling,
_ => _clientCapabilities is JsonObject obj
&& obj.TryGetPropertyValue(name, out var node)
&& node is not null,
};

private static bool IsSamplingEnabled()
{
Expand Down
Loading
Loading