From 46f961c3cf0d6d38e200df6aeeb17670c91a640b Mon Sep 17 00:00:00 2001 From: Widthdom Date: Mon, 25 May 2026 04:45:01 +0900 Subject: [PATCH 1/8] Fix MCP initialize and logging gaps (#1836 #1837 #1840 #1895) --- .../unreleased/1836-1837-1840-1895.fixed.md | 22 +++++ src/CodeIndex/Mcp/McpServer.cs | 92 ++++++++++++++++++- src/CodeIndex/Mcp/McpToolHandlers.cs | 17 ++++ tests/CodeIndex.Tests/McpServerTests.cs | 81 +++++++++++++++- 4 files changed, 208 insertions(+), 4 deletions(-) create mode 100644 changelog.d/unreleased/1836-1837-1840-1895.fixed.md diff --git a/changelog.d/unreleased/1836-1837-1840-1895.fixed.md b/changelog.d/unreleased/1836-1837-1840-1895.fixed.md new file mode 100644 index 0000000000..172fe53ac4 --- /dev/null +++ b/changelog.d/unreleased/1836-1837-1840-1895.fixed.md @@ -0,0 +1,22 @@ +--- +category: fixed +issues: + - 1836 + - 1837 + - 1840 + - 1895 +affected: + - src/CodeIndex/Mcp/McpServer.cs + - src/CodeIndex/Mcp/McpToolHandlers.cs + - tests/CodeIndex.Tests/McpServerTests.cs +--- + +## English + +- **MCP initialize now preserves session negotiation details (#1836, #1840)** — `initialize` captures client capabilities and roots for MCP session diagnostics, and the advertised capabilities now include logging and unsupported sampling. +- **MCP logging and notification handling are clearer (#1837, #1895)** — `logging/setLevel` updates the session log level at runtime, and unknown `notifications/*` methods are covered by regression tests to ensure they log without sending a JSON-RPC response. + +## 日本語 + +- **MCP initialize がセッション交渉情報を保持するようになりました (#1836, #1840)** — `initialize` は client capabilities と roots を MCP セッション診断用に保持し、advertise する capabilities に logging と未対応 sampling を含めるようになりました。 +- **MCP logging と notification 処理が明確になりました (#1837, #1895)** — `logging/setLevel` でセッション中のログレベルを変更できるようにし、未知の `notifications/*` は JSON-RPC 応答を返さずログに残すことを回帰テストで固定しました。 diff --git a/src/CodeIndex/Mcp/McpServer.cs b/src/CodeIndex/Mcp/McpServer.cs index 4b047619d5..770f504441 100644 --- a/src/CodeIndex/Mcp/McpServer.cs +++ b/src/CodeIndex/Mcp/McpServer.cs @@ -90,6 +90,9 @@ public partial class McpServer : IDisposable // にする。`initialize` 毎に上書きすることで再接続時に caller identity が追随する。 private string? _clientName; private string? _clientVersion; + private JsonNode? _clientCapabilities; + private JsonArray _clientRoots = []; + private string _mcpLogLevel = "info"; // Opaque per-server-instance session id copied into suggestion attribution records (#1873). // #1873 の提案 attribution 用に保存する、サーバーインスタンス単位の不透明セッションID。 private readonly string _sessionId = Guid.NewGuid().ToString("D"); @@ -900,10 +903,11 @@ private static string BuildMinimalInternalErrorResponse(bool hasId, JsonNode? id "resources/read" => HandleResourcesRead(id, request["params"]), "prompts/list" => HandlePromptsList(id), "prompts/get" => HandlePromptsGet(id, request["params"]), + "logging/setLevel" => HandleLoggingSetLevel(id, request["params"]), "ping" => CreateSuccessResponse(hasId, id, new JsonObject()), _ => CreateErrorResponse(hasId: true, id: id, code: -32601, message: $"Method not found: {method}", category: McpErrorEnvelope.CategoryMethodNotFound, - suggestion: "Supported methods: initialize, tools/list, tools/call, resources/list, resources/read, prompts/list, prompts/get, ping, notifications/initialized, notifications/cancelled, notifications/shutdown.", + suggestion: "Supported methods: initialize, tools/list, tools/call, resources/list, resources/read, prompts/list, prompts/get, logging/setLevel, ping, notifications/initialized, notifications/cancelled, notifications/shutdown.", retrySafe: false), }); } @@ -1044,6 +1048,7 @@ internal static string BuildAuthFailureLog(string? method, string? reason) => private JsonNode HandleInitialize(JsonNode? id, JsonNode? _params) { CaptureClientInfo(_params); + CaptureClientSession(_params); // Caller stickiness: allow upgrading from the default "unknown" bucket to a named // identity, but reject re-initialize attempts that swap one named identity for // another. Otherwise a single networked session could reset its rate-limit bucket @@ -1092,7 +1097,9 @@ private JsonNode HandleInitialize(JsonNode? id, JsonNode? _params) ["prompts"] = new JsonObject { ["listChanged"] = false - } + }, + ["logging"] = new JsonObject(), + ["sampling"] = false }, ["serverInfo"] = new JsonObject { @@ -1147,6 +1154,40 @@ private void CaptureClientInfo(JsonNode? initializeParams) _clientVersion = TryReadStringMember(info, "version"); } + private void CaptureClientSession(JsonNode? initializeParams) + { + _clientCapabilities = null; + _clientRoots = []; + if (initializeParams is not JsonObject obj) + return; + + if (obj.TryGetPropertyValue("clientCapabilities", out var capabilities) && capabilities is not null) + _clientCapabilities = JsonNode.Parse(capabilities.ToJsonString()); + + if (TryReadStringValue(obj["rootUri"]) is { Length: > 0 } rootUri) + _clientRoots.Add(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); + } + } + } + + internal JsonNode? ClientCapabilitiesForTests => _clientCapabilities is null ? null : JsonNode.Parse(_clientCapabilities.ToJsonString()); + + internal string[] ClientRootsForTests => _clientRoots + .Select(root => root?.GetValue()) + .Where(root => !string.IsNullOrWhiteSpace(root)) + .Cast() + .ToArray(); + + internal string McpLogLevelForTests => _mcpLogLevel; + private static string? TryReadStringMember(JsonObject obj, string key) { if (!obj.TryGetPropertyValue(key, out var node)) @@ -1291,6 +1332,21 @@ private JsonNode HandlePromptsGet(JsonNode? id, JsonNode? getParams) }); } + private JsonNode HandleLoggingSetLevel(JsonNode? id, JsonNode? setLevelParams) + { + var level = TryReadStringValue(setLevelParams?["level"]); + if (!IsSupportedMcpLogLevel(level)) + return CreateErrorResponse(hasId: true, id: id, code: -32602, message: "Invalid logging level", + category: McpErrorEnvelope.CategoryInvalidArgument, + suggestion: "logging/setLevel requires params.level to be one of: debug, info, notice, warning, error.", + retrySafe: false); + + var previous = _mcpLogLevel; + _mcpLogLevel = level!; + EmitLogNotification("info", $"MCP logging level changed from {previous} to {_mcpLogLevel}."); + return CreateSuccessResponse(true, id, new JsonObject()); + } + private static JsonObject CreatePromptDefinition(string name, string description, string argumentName, string argumentDescription) => new() { @@ -1335,6 +1391,9 @@ private static bool TryParseResourceUri(string uri, out string path) private static string? TryReadStringValue(JsonNode? node) => node is JsonValue value && value.TryGetValue(out var text) ? text : null; + private static bool? TryReadBooleanValue(JsonNode? node) + => node is JsonValue value && value.TryGetValue(out var flag) ? flag : null; + private static string GetResourceMimeType(string? lang) => lang?.ToLowerInvariant() switch { @@ -1692,7 +1751,7 @@ private JsonNode HandleToolsCall(JsonNode? id, JsonNode? callParams) private void EmitProgressNotification(JsonNode? progressToken, long progress, long? total, string? message = null) { - if (progressToken is null || _currentOutOfBandFrameWriter.Value is not { } writer) + if (progressToken is null || !ClientSupportsProgressNotifications() || _currentOutOfBandFrameWriter.Value is not { } writer) return; var parameters = new JsonObject @@ -1714,6 +1773,30 @@ private void EmitProgressNotification(JsonNode? progressToken, long progress, lo writer(notification.ToJsonString(_jsonOptions)); } + private void EmitLogNotification(string level, string message) + { + if (_currentOutOfBandFrameWriter.Value is not { } writer) + return; + + var notification = new JsonObject + { + ["jsonrpc"] = "2.0", + ["method"] = "notifications/message", + ["params"] = new JsonObject + { + ["level"] = level, + ["logger"] = "cdidx", + ["data"] = message, + }, + }; + writer(notification.ToJsonString(_jsonOptions)); + } + + private bool ClientSupportsProgressNotifications() + => TryReadBooleanValue(_clientCapabilities?["experimental"]?["progress"]) == true + || TryReadBooleanValue(_clientCapabilities?["progress"]) == true + || TryReadBooleanValue(_clientCapabilities?["notifications"]?["progress"]) == true; + /// /// Emit a single audit record for the just-executed tool call. Inspects the wire /// response to derive the result count and error code so the audit trail matches what @@ -1917,6 +2000,9 @@ internal static string BuildCallerSwapRejectionLog(string current, string attemp internal static string BuildUnknownNotificationLog(string method) => $"[cdidx-mcp] Ignoring unknown notification: {method}"; + internal static bool IsSupportedMcpLogLevel(string? level) + => level is "debug" or "info" or "notice" or "warning" or "error"; + // Wire-safe error body for the tool catch-all. Mentions the tool and the // exception type so the client can branch (retry vs. surface to user) // while keeping bound values or matched content out of the response (#1530). diff --git a/src/CodeIndex/Mcp/McpToolHandlers.cs b/src/CodeIndex/Mcp/McpToolHandlers.cs index b85a640754..da5549735b 100644 --- a/src/CodeIndex/Mcp/McpToolHandlers.cs +++ b/src/CodeIndex/Mcp/McpToolHandlers.cs @@ -1257,10 +1257,27 @@ private JsonNode ExecuteStatus(JsonNode? id) structured["sqlGraphContractReady"] = status.SqlGraphContractReady; if (status.SqlGraphContractDegradedReason != null) structured["sqlGraphContractDegradedReason"] = status.SqlGraphContractDegradedReason; + structured["mcp_session"] = BuildMcpSessionStatus(); return CreateToolResult(id, "Database stats returned.", structured); }); } + private JsonObject BuildMcpSessionStatus() + { + var roots = new JsonArray(); + foreach (var root in _clientRoots) + roots.Add(root?.DeepClone()); + + var session = new JsonObject + { + ["log_level"] = _mcpLogLevel, + ["roots"] = roots, + }; + if (_clientCapabilities is not null) + session["client_capabilities"] = _clientCapabilities.DeepClone(); + return session; + } + private JsonNode ExecuteOutline(JsonNode? id, JsonNode? args) { if (!TryReadRequiredStringParameter(args, "path", out var path, out var requiredError)) diff --git a/tests/CodeIndex.Tests/McpServerTests.cs b/tests/CodeIndex.Tests/McpServerTests.cs index 1c1b40963b..314049ac1a 100644 --- a/tests/CodeIndex.Tests/McpServerTests.cs +++ b/tests/CodeIndex.Tests/McpServerTests.cs @@ -176,6 +176,72 @@ public void Initialize_AdvertisesResourcesAndPrompts() Assert.False(capabilities["resources"]!["subscribe"]!.GetValue()); Assert.False(capabilities["resources"]!["listChanged"]!.GetValue()); Assert.False(capabilities["prompts"]!["listChanged"]!.GetValue()); + Assert.NotNull(capabilities["logging"]); + Assert.False(capabilities["sampling"]!.GetValue()); + } + + [Fact] + public void Initialize_CapturesClientCapabilitiesAndRootsForSessionStatus() + { + var request = JsonNode.Parse("""{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientCapabilities":{"experimental":{"progress":true}},"rootUri":"file:///workspace","roots":[{"uri":"file:///workspace/src"}]}}""")!; + _server.HandleMessage(request); + + Assert.True(_server.ClientCapabilitiesForTests!["experimental"]!["progress"]!.GetValue()); + Assert.Equal(["file:///workspace", "file:///workspace/src"], _server.ClientRootsForTests); + + var status = JsonNode.Parse("""{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"status","arguments":{}}}""")!; + var response = _server.HandleMessage(status)!; + var session = response["result"]!["structuredContent"]!["mcp_session"]!; + + Assert.True(session["client_capabilities"]!["experimental"]!["progress"]!.GetValue()); + Assert.Equal("file:///workspace", session["roots"]!.AsArray()[0]!.GetValue()); + Assert.Equal("info", session["log_level"]!.GetValue()); + } + + [Fact] + public void LoggingSetLevel_UpdatesSessionLogLevel() + { + var request = JsonNode.Parse("""{"jsonrpc":"2.0","id":1,"method":"logging/setLevel","params":{"level":"debug"}}""")!; + var response = _server.HandleMessage(request)!; + + Assert.NotNull(response["result"]); + Assert.Equal("debug", _server.McpLogLevelForTests); + } + + [Fact] + public void LoggingSetLevel_InvalidLevel_ReturnsInvalidParams() + { + var request = JsonNode.Parse("""{"jsonrpc":"2.0","id":1,"method":"logging/setLevel","params":{"level":"trace"}}""")!; + var response = _server.HandleMessage(request)!; + + Assert.Equal(-32602, response["error"]!["code"]!.GetValue()); + Assert.Equal("invalid_argument", response["error"]!["data"]!["category"]!.GetValue()); + } + + [Fact] + public void UnknownNotification_ReturnsNoResponseAndLogsWarning() + { + using var writer = new StringWriter(); + lock (TestConsoleLock.Gate) + { + var previous = Console.Error; + try + { + Console.SetError(writer); + var request = JsonNode.Parse("""{"jsonrpc":"2.0","method":"notifications/initalized"}""")!; + + var response = _server.HandleMessage(request); + + Assert.Null(response); + } + finally + { + Console.SetError(previous); + } + } + + Assert.Contains("Ignoring unknown notification", writer.ToString()); + Assert.Contains("notifications/initalized", writer.ToString()); } [Fact] @@ -1540,7 +1606,20 @@ public async Task RunAsync_IndexWithProgressToken_EmitsProgressNotificationBefor ["_meta"] = new JsonObject { ["progressToken"] = "issue-1684" }, }, }; - var transport = new ShutdownProbeTransport("stdio", (Action?)null, request.ToJsonString()); + var initialize = new JsonObject + { + ["jsonrpc"] = "2.0", + ["id"] = 1, + ["method"] = "initialize", + ["params"] = new JsonObject + { + ["clientCapabilities"] = new JsonObject + { + ["experimental"] = new JsonObject { ["progress"] = true }, + }, + }, + }; + var transport = new ShutdownProbeTransport("stdio", (Action?)null, initialize.ToJsonString(), request.ToJsonString()); await server.RunAsync(transport, CancellationToken.None); From c19eae49d464d512db4ec9ce06bb6b40d23129b3 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Mon, 25 May 2026 04:54:03 +0900 Subject: [PATCH 2/8] Use standard MCP capabilities params (#1836) --- src/CodeIndex/Mcp/McpServer.cs | 4 +++- tests/CodeIndex.Tests/McpServerTests.cs | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/CodeIndex/Mcp/McpServer.cs b/src/CodeIndex/Mcp/McpServer.cs index 770f504441..c9d17b20c7 100644 --- a/src/CodeIndex/Mcp/McpServer.cs +++ b/src/CodeIndex/Mcp/McpServer.cs @@ -1161,7 +1161,9 @@ private void CaptureClientSession(JsonNode? initializeParams) if (initializeParams is not JsonObject obj) return; - if (obj.TryGetPropertyValue("clientCapabilities", out var capabilities) && capabilities is not null) + if (!obj.TryGetPropertyValue("capabilities", out var capabilities)) + obj.TryGetPropertyValue("clientCapabilities", out capabilities); + if (capabilities is not null) _clientCapabilities = JsonNode.Parse(capabilities.ToJsonString()); if (TryReadStringValue(obj["rootUri"]) is { Length: > 0 } rootUri) diff --git a/tests/CodeIndex.Tests/McpServerTests.cs b/tests/CodeIndex.Tests/McpServerTests.cs index 314049ac1a..75e29ae303 100644 --- a/tests/CodeIndex.Tests/McpServerTests.cs +++ b/tests/CodeIndex.Tests/McpServerTests.cs @@ -183,7 +183,7 @@ public void Initialize_AdvertisesResourcesAndPrompts() [Fact] public void Initialize_CapturesClientCapabilitiesAndRootsForSessionStatus() { - var request = JsonNode.Parse("""{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientCapabilities":{"experimental":{"progress":true}},"rootUri":"file:///workspace","roots":[{"uri":"file:///workspace/src"}]}}""")!; + var request = JsonNode.Parse("""{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{"experimental":{"progress":true}},"rootUri":"file:///workspace","roots":[{"uri":"file:///workspace/src"}]}}""")!; _server.HandleMessage(request); Assert.True(_server.ClientCapabilitiesForTests!["experimental"]!["progress"]!.GetValue()); @@ -1613,7 +1613,7 @@ public async Task RunAsync_IndexWithProgressToken_EmitsProgressNotificationBefor ["method"] = "initialize", ["params"] = new JsonObject { - ["clientCapabilities"] = new JsonObject + ["capabilities"] = new JsonObject { ["experimental"] = new JsonObject { ["progress"] = true }, }, From fc5f3be42becaabcf7bb82247ba395f71063e9a0 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Mon, 25 May 2026 04:59:09 +0900 Subject: [PATCH 3/8] Address MCP review findings (#1837) --- src/CodeIndex/Mcp/McpServer.cs | 14 +++----------- tests/CodeIndex.Tests/McpServerTests.cs | 19 +++---------------- 2 files changed, 6 insertions(+), 27 deletions(-) diff --git a/src/CodeIndex/Mcp/McpServer.cs b/src/CodeIndex/Mcp/McpServer.cs index c9d17b20c7..95059ab88f 100644 --- a/src/CodeIndex/Mcp/McpServer.cs +++ b/src/CodeIndex/Mcp/McpServer.cs @@ -1340,7 +1340,7 @@ private JsonNode HandleLoggingSetLevel(JsonNode? id, JsonNode? setLevelParams) if (!IsSupportedMcpLogLevel(level)) return CreateErrorResponse(hasId: true, id: id, code: -32602, message: "Invalid logging level", category: McpErrorEnvelope.CategoryInvalidArgument, - suggestion: "logging/setLevel requires params.level to be one of: debug, info, notice, warning, error.", + suggestion: "logging/setLevel requires params.level to be one of: debug, info, notice, warning, error, critical, alert, emergency.", retrySafe: false); var previous = _mcpLogLevel; @@ -1393,9 +1393,6 @@ private static bool TryParseResourceUri(string uri, out string path) private static string? TryReadStringValue(JsonNode? node) => node is JsonValue value && value.TryGetValue(out var text) ? text : null; - private static bool? TryReadBooleanValue(JsonNode? node) - => node is JsonValue value && value.TryGetValue(out var flag) ? flag : null; - private static string GetResourceMimeType(string? lang) => lang?.ToLowerInvariant() switch { @@ -1753,7 +1750,7 @@ private JsonNode HandleToolsCall(JsonNode? id, JsonNode? callParams) private void EmitProgressNotification(JsonNode? progressToken, long progress, long? total, string? message = null) { - if (progressToken is null || !ClientSupportsProgressNotifications() || _currentOutOfBandFrameWriter.Value is not { } writer) + if (progressToken is null || _currentOutOfBandFrameWriter.Value is not { } writer) return; var parameters = new JsonObject @@ -1794,11 +1791,6 @@ private void EmitLogNotification(string level, string message) writer(notification.ToJsonString(_jsonOptions)); } - private bool ClientSupportsProgressNotifications() - => TryReadBooleanValue(_clientCapabilities?["experimental"]?["progress"]) == true - || TryReadBooleanValue(_clientCapabilities?["progress"]) == true - || TryReadBooleanValue(_clientCapabilities?["notifications"]?["progress"]) == true; - /// /// Emit a single audit record for the just-executed tool call. Inspects the wire /// response to derive the result count and error code so the audit trail matches what @@ -2003,7 +1995,7 @@ internal static string BuildUnknownNotificationLog(string method) => $"[cdidx-mcp] Ignoring unknown notification: {method}"; internal static bool IsSupportedMcpLogLevel(string? level) - => level is "debug" or "info" or "notice" or "warning" or "error"; + => level is "debug" or "info" or "notice" or "warning" or "error" or "critical" or "alert" or "emergency"; // Wire-safe error body for the tool catch-all. Mentions the tool and the // exception type so the client can branch (retry vs. surface to user) diff --git a/tests/CodeIndex.Tests/McpServerTests.cs b/tests/CodeIndex.Tests/McpServerTests.cs index 75e29ae303..4396fc3fbd 100644 --- a/tests/CodeIndex.Tests/McpServerTests.cs +++ b/tests/CodeIndex.Tests/McpServerTests.cs @@ -201,11 +201,11 @@ public void Initialize_CapturesClientCapabilitiesAndRootsForSessionStatus() [Fact] public void LoggingSetLevel_UpdatesSessionLogLevel() { - var request = JsonNode.Parse("""{"jsonrpc":"2.0","id":1,"method":"logging/setLevel","params":{"level":"debug"}}""")!; + var request = JsonNode.Parse("""{"jsonrpc":"2.0","id":1,"method":"logging/setLevel","params":{"level":"emergency"}}""")!; var response = _server.HandleMessage(request)!; Assert.NotNull(response["result"]); - Assert.Equal("debug", _server.McpLogLevelForTests); + Assert.Equal("emergency", _server.McpLogLevelForTests); } [Fact] @@ -1606,20 +1606,7 @@ public async Task RunAsync_IndexWithProgressToken_EmitsProgressNotificationBefor ["_meta"] = new JsonObject { ["progressToken"] = "issue-1684" }, }, }; - var initialize = new JsonObject - { - ["jsonrpc"] = "2.0", - ["id"] = 1, - ["method"] = "initialize", - ["params"] = new JsonObject - { - ["capabilities"] = new JsonObject - { - ["experimental"] = new JsonObject { ["progress"] = true }, - }, - }, - }; - var transport = new ShutdownProbeTransport("stdio", (Action?)null, initialize.ToJsonString(), request.ToJsonString()); + var transport = new ShutdownProbeTransport("stdio", (Action?)null, request.ToJsonString()); await server.RunAsync(transport, CancellationToken.None); From a4f0c7e344082e617c31f09057dc8a613749e698 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Mon, 25 May 2026 05:05:27 +0900 Subject: [PATCH 4/8] Remove unsupported MCP sampling advertisement (#1840) --- changelog.d/unreleased/1836-1837-1840-1895.fixed.md | 4 ++-- src/CodeIndex/Mcp/McpServer.cs | 3 +-- tests/CodeIndex.Tests/McpServerTests.cs | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/changelog.d/unreleased/1836-1837-1840-1895.fixed.md b/changelog.d/unreleased/1836-1837-1840-1895.fixed.md index 172fe53ac4..c0813590f4 100644 --- a/changelog.d/unreleased/1836-1837-1840-1895.fixed.md +++ b/changelog.d/unreleased/1836-1837-1840-1895.fixed.md @@ -13,10 +13,10 @@ affected: ## English -- **MCP initialize now preserves session negotiation details (#1836, #1840)** — `initialize` captures client capabilities and roots for MCP session diagnostics, and the advertised capabilities now include logging and unsupported sampling. +- **MCP initialize now preserves session negotiation details (#1836, #1840)** — `initialize` captures client capabilities and roots for MCP session diagnostics, and the advertised capabilities now include logging. - **MCP logging and notification handling are clearer (#1837, #1895)** — `logging/setLevel` updates the session log level at runtime, and unknown `notifications/*` methods are covered by regression tests to ensure they log without sending a JSON-RPC response. ## 日本語 -- **MCP initialize がセッション交渉情報を保持するようになりました (#1836, #1840)** — `initialize` は client capabilities と roots を MCP セッション診断用に保持し、advertise する capabilities に logging と未対応 sampling を含めるようになりました。 +- **MCP initialize がセッション交渉情報を保持するようになりました (#1836, #1840)** — `initialize` は client capabilities と roots を MCP セッション診断用に保持し、advertise する capabilities に logging を含めるようになりました。 - **MCP logging と notification 処理が明確になりました (#1837, #1895)** — `logging/setLevel` でセッション中のログレベルを変更できるようにし、未知の `notifications/*` は JSON-RPC 応答を返さずログに残すことを回帰テストで固定しました。 diff --git a/src/CodeIndex/Mcp/McpServer.cs b/src/CodeIndex/Mcp/McpServer.cs index 95059ab88f..f1bd60d147 100644 --- a/src/CodeIndex/Mcp/McpServer.cs +++ b/src/CodeIndex/Mcp/McpServer.cs @@ -1098,8 +1098,7 @@ private JsonNode HandleInitialize(JsonNode? id, JsonNode? _params) { ["listChanged"] = false }, - ["logging"] = new JsonObject(), - ["sampling"] = false + ["logging"] = new JsonObject() }, ["serverInfo"] = new JsonObject { diff --git a/tests/CodeIndex.Tests/McpServerTests.cs b/tests/CodeIndex.Tests/McpServerTests.cs index 4396fc3fbd..2648ac0179 100644 --- a/tests/CodeIndex.Tests/McpServerTests.cs +++ b/tests/CodeIndex.Tests/McpServerTests.cs @@ -177,7 +177,7 @@ public void Initialize_AdvertisesResourcesAndPrompts() Assert.False(capabilities["resources"]!["listChanged"]!.GetValue()); Assert.False(capabilities["prompts"]!["listChanged"]!.GetValue()); Assert.NotNull(capabilities["logging"]); - Assert.False(capabilities["sampling"]!.GetValue()); + Assert.Null(capabilities["sampling"]); } [Fact] From 056d7d5070eac0d89437767a505c72c20a3115b9 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Mon, 25 May 2026 05:12:11 +0900 Subject: [PATCH 5/8] Restore MCP list argument validation (#1895) --- src/CodeIndex/Mcp/McpServer.cs | 11 +++ src/CodeIndex/Mcp/McpToolHandlers.cs | 100 ++++++++++++++++++++++++++- 2 files changed, 110 insertions(+), 1 deletion(-) diff --git a/src/CodeIndex/Mcp/McpServer.cs b/src/CodeIndex/Mcp/McpServer.cs index f1bd60d147..1b20b234a5 100644 --- a/src/CodeIndex/Mcp/McpServer.cs +++ b/src/CodeIndex/Mcp/McpServer.cs @@ -1622,6 +1622,17 @@ private JsonNode HandleToolsCall(JsonNode? id, JsonNode? callParams) return disabledResponse; } + if (ValidateCommonListArguments(args) is JsonObject listArgumentError) + { + var validationResponse = CreateToolErrorResponse(id, listArgumentError["message"]?.GetValue() ?? "Invalid list argument", + category: McpErrorEnvelope.CategoryInvalidArgument, + suggestion: "Tool argument validation failed. Inspect the tool's `inputSchema` via tools/list and adjust the call.", + retrySafe: false, + extraData: listArgumentError); + TryEmitAudit(toolName, id, args, validationResponse, DateTimeOffset.UtcNow, 0.0, errorType: "invalid_argument"); + return validationResponse; + } + Database.DbDebug.ResetContext(); var metricsStartedAt = DateTimeOffset.UtcNow; var metricsStopwatch = System.Diagnostics.Stopwatch.StartNew(); diff --git a/src/CodeIndex/Mcp/McpToolHandlers.cs b/src/CodeIndex/Mcp/McpToolHandlers.cs index da5549735b..8931c470f7 100644 --- a/src/CodeIndex/Mcp/McpToolHandlers.cs +++ b/src/CodeIndex/Mcp/McpToolHandlers.cs @@ -18,6 +18,8 @@ public partial class McpServer { private const int DefaultBatchQueryResponseByteLimit = MaxLineLength; private const string BatchQueryResponseByteLimitEnvVar = "CDIDX_MCP_BATCH_RESPONSE_MAX_BYTES"; + internal const int MaxMcpArrayFilterCount = 100; + internal const int MaxMcpArrayFilterStringLength = 4096; // --- Tool implementations / ツール実装 --- @@ -223,13 +225,99 @@ private static string BuildNonCallGraphKindRejectionMessage(string command, stri private static List ReadStringList(JsonNode? args, string propertyName) { return args?[propertyName] is JsonArray array - ? array.Select(node => node?.GetValue()) + ? array.Select(node => node is JsonValue value && value.TryGetValue(out var text) ? text : null) .Where(value => !string.IsNullOrWhiteSpace(value)) .Cast() .ToList() : []; } + private static JsonObject? ValidateCommonListArguments(JsonNode? args) + { + foreach (var propertyName in new[] { "path", "project", "excludePaths", "names" }) + { + if (ValidateStringListArgument(args, propertyName) is JsonObject error) + return error; + } + + return null; + } + + private static JsonObject? ValidateStringListArgument(JsonNode? args, string propertyName) + { + var node = args?[propertyName]; + if (node is null) + return null; + + if (node is JsonArray array) + { + if (array.Count > MaxMcpArrayFilterCount) + return new JsonObject + { + ["message"] = $"{propertyName} must contain at most {MaxMcpArrayFilterCount} entries.", + ["invalid_count"] = array.Count - MaxMcpArrayFilterCount, + }; + + var invalidCount = 0; + var invalidSamples = new JsonArray(); + for (var i = 0; i < array.Count; i++) + { + var element = array[i]; + if (element is not JsonValue value || !value.TryGetValue(out var text) || string.IsNullOrWhiteSpace(text)) + { + invalidCount++; + if (invalidSamples.Count < 3) + invalidSamples.Add($"[{i}]"); + continue; + } + + if (text.Length > MaxMcpArrayFilterStringLength) + { + invalidCount++; + if (invalidSamples.Count < 3) + invalidSamples.Add($"[{i}] length {text.Length}"); + } + } + + if (invalidCount > 0 && !(propertyName == "names" && invalidCount == array.Count)) + return new JsonObject + { + ["message"] = $"{propertyName} contains {invalidCount} invalid entr{(invalidCount == 1 ? "y" : "ies")}. Entries must be non-empty strings no longer than {MaxMcpArrayFilterStringLength} characters.", + ["invalid_count"] = invalidCount, + ["invalid_samples"] = invalidSamples, + }; + return null; + } + + if (node is JsonValue scalar && scalar.TryGetValue(out var scalarText)) + { + if (propertyName == "names") + return null; + if (propertyName == "path" && string.IsNullOrWhiteSpace(scalarText)) + return null; + if (string.IsNullOrWhiteSpace(scalarText)) + return new JsonObject + { + ["message"] = $"{propertyName} cannot be empty or whitespace-only.", + ["invalid_count"] = 1, + }; + if (scalarText.Length > MaxMcpArrayFilterStringLength) + return new JsonObject + { + ["message"] = $"{propertyName} must be no longer than {MaxMcpArrayFilterStringLength} characters.", + ["invalid_count"] = 1, + ["invalid_samples"] = new JsonArray { $"length {scalarText.Length}" }, + }; + return null; + } + + return new JsonObject + { + ["message"] = $"{propertyName} must be a string or array of strings.", + ["invalid_count"] = 1, + }; + } + private static bool TryResolveSearchExactArgument(JsonNode? args, out bool exact, out string? error) { var legacyExact = args?["exact"]?.GetValue() ?? false; @@ -1643,6 +1731,16 @@ void AppendRateLimitedSlot(string? toolName, JsonNode? toolArgs, Stopwatch slotS continue; } + if (ValidateCommonListArguments(toolArgs) is JsonObject listArgumentError) + { + AppendSlotError(toolName, toolArgs, slotStopwatch, + listArgumentError["message"]?.GetValue() ?? "Invalid list argument", + category: McpErrorEnvelope.CategoryInvalidArgument, + suggestion: "Tool argument validation failed. Inspect the tool's `inputSchema` via tools/list and adjust the call.", + retrySafe: false); + continue; + } + // Throttle each inner slot too, otherwise a single allowed batch_query call could // still drive N inner searches through and defeat the per-(tool, caller) limiter // the outer dispatch enforces. The decision is per (inner-tool, caller) so an From f411f4278c496e9881c4d2d9175832f4941cc137 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Mon, 25 May 2026 05:16:49 +0900 Subject: [PATCH 6/8] Resolve MCP validation merge conflicts (#1895) --- src/CodeIndex/Mcp/McpServer.cs | 11 ----------- src/CodeIndex/Mcp/McpToolHandlers.cs | 10 ---------- 2 files changed, 21 deletions(-) diff --git a/src/CodeIndex/Mcp/McpServer.cs b/src/CodeIndex/Mcp/McpServer.cs index 148bcf3eaa..c8d4298490 100644 --- a/src/CodeIndex/Mcp/McpServer.cs +++ b/src/CodeIndex/Mcp/McpServer.cs @@ -1622,17 +1622,6 @@ private JsonNode HandleToolsCall(JsonNode? id, JsonNode? callParams) return disabledResponse; } - if (ValidateCommonListArguments(args) is JsonObject listArgumentError) - { - var validationResponse = CreateToolErrorResponse(id, listArgumentError["message"]?.GetValue() ?? "Invalid list argument", - category: McpErrorEnvelope.CategoryInvalidArgument, - suggestion: "Tool argument validation failed. Inspect the tool's `inputSchema` via tools/list and adjust the call.", - retrySafe: false, - extraData: listArgumentError); - TryEmitAudit(toolName, id, args, validationResponse, DateTimeOffset.UtcNow, 0.0, errorType: "invalid_argument"); - return validationResponse; - } - Database.DbDebug.ResetContext(); var metricsStartedAt = DateTimeOffset.UtcNow; var metricsStopwatch = System.Diagnostics.Stopwatch.StartNew(); diff --git a/src/CodeIndex/Mcp/McpToolHandlers.cs b/src/CodeIndex/Mcp/McpToolHandlers.cs index 5f11428ca1..9a54b294b8 100644 --- a/src/CodeIndex/Mcp/McpToolHandlers.cs +++ b/src/CodeIndex/Mcp/McpToolHandlers.cs @@ -1752,16 +1752,6 @@ void AppendRateLimitedSlot(int requestIndex, string? toolName, JsonNode? toolArg continue; } - if (ValidateCommonListArguments(toolArgs) is JsonObject listArgumentError) - { - AppendSlotError(toolName, toolArgs, slotStopwatch, - listArgumentError["message"]?.GetValue() ?? "Invalid list argument", - category: McpErrorEnvelope.CategoryInvalidArgument, - suggestion: "Tool argument validation failed. Inspect the tool's `inputSchema` via tools/list and adjust the call.", - retrySafe: false); - continue; - } - // Throttle each inner slot too, otherwise a single allowed batch_query call could // still drive N inner searches through and defeat the per-(tool, caller) limiter // the outer dispatch enforces. The decision is per (inner-tool, caller) so an From 848ba085ba6d9ec8238cc12d5743d9af4ca7c034 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Mon, 25 May 2026 05:24:12 +0900 Subject: [PATCH 7/8] Document MCP session status fields (#1836) --- AGENT_GUIDE.md | 3 ++- DEVELOPER_GUIDE.md | 4 ++-- README.md | 8 ++++++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/AGENT_GUIDE.md b/AGENT_GUIDE.md index 445fffb318..c6758eae49 100644 --- a/AGENT_GUIDE.md +++ b/AGENT_GUIDE.md @@ -123,7 +123,7 @@ CI watching must be bounded. Do not loop indefinitely. ## Status Contract -- `status --json` and related JSON/MCP payloads currently expose the trust fields documented in `README.md` and `DEVELOPER_GUIDE.md`, including `fold_ready`, `fold_ready_reason`, `graph_table_available`, `issues_table_available`, `sql_graph_contract_ready`, `sql_graph_contract_degraded_reason`, `hotspot_family_ready`, `hotspot_family_degraded_reason`, `csharp_symbol_name_ready`, `csharp_metadata_target_ready`, `csharp_metadata_target_degraded_reason`, `indexed_head_commit`, `worktree_head_changed`, `index_writer_version`, `index_newer_than_reader`, `index_newer_than_reader_reason`, `unknown_extension_file_count`, `path_case_sensitive`, `data_dir`, `data_dir_source`, `data_dir_mode`, `mac_profile`, `db_pragma_settings` (`journal_mode`, `synchronous`, `wal_autocheckpoint`, `page_count`, `freelist_count`, `page_size`), `hooks`, and the `status --check`-only `stale_after_seconds` / `index_age_seconds` threshold audit fields. +- `status --json` and related JSON/MCP payloads currently expose the trust fields documented in `README.md` and `DEVELOPER_GUIDE.md`, including `fold_ready`, `fold_ready_reason`, `graph_table_available`, `issues_table_available`, `sql_graph_contract_ready`, `sql_graph_contract_degraded_reason`, `hotspot_family_ready`, `hotspot_family_degraded_reason`, `csharp_symbol_name_ready`, `csharp_metadata_target_ready`, `csharp_metadata_target_degraded_reason`, `indexed_head_commit`, `worktree_head_changed`, `index_writer_version`, `index_newer_than_reader`, `index_newer_than_reader_reason`, `unknown_extension_file_count`, `path_case_sensitive`, `data_dir`, `data_dir_source`, `data_dir_mode`, `mac_profile`, `db_pragma_settings` (`journal_mode`, `synchronous`, `wal_autocheckpoint`, `page_count`, `freelist_count`, `page_size`), `hooks`, MCP-only `mcp_session`, and the `status --check`-only `stale_after_seconds` / `index_age_seconds` threshold audit fields. - When `fold_ready` or `csharp_metadata_target_ready` is the only degraded readiness bit, the CLI also adds `degraded_reason`, `recommended_action`, and `alternative_action`. - `index_writer_version` records the `cdidx` version that last wrote to the DB (stamped into `codeindex_meta` as `cdidx_writer_version` on every full scan, update, and MCP index). `index_newer_than_reader` flips to `true` whenever any persisted numeric contract stamp in `codeindex_meta` (or unknown `PRAGMA user_version` readiness bits) exceeds the current binary's compiled maximum, so an older CLI re-opening a DB written by a newer CLI degrades loudly with an audit trail instead of silently dropping back to text-search fallbacks. `index_newer_than_reader_reason` enumerates the specific newer-than-reader stamps. - `status` also surfaces indexed-HEAD freshness via `indexed_head_sha`, `indexed_head_branch`, `indexed_head_timestamp`, and `commits_ahead_of_indexed_head`. They are stamped by `cdidx index` on every successful run (full scan AND partial update, distinct from `indexed_head_commit` which is full-scan only) on a best-effort basis (never blocks an otherwise-successful index) and omitted on non-git workspaces, detached HEAD (branch only), or legacy DBs created before this contract. @@ -131,6 +131,7 @@ CI watching must be bounded. Do not loop indefinitely. - `status` also surfaces `.cdidx` data-directory permissions via `data_dir_mode` on POSIX filesystems. New `.cdidx` data directories are forced to `0700`; the field is omitted on Windows, URI DBs, or when the directory mode cannot be inspected. - `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). +- MCP `status` also surfaces session diagnostics via `mcp_session`. It is not persisted DB state; it includes the current `log_level`, captured `roots`, and optional `client_capabilities`. - Keep `README.md`, `DEVELOPER_GUIDE.md`, and this file synchronized if this contract changes. ## Reference Extraction diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index ac673479ba..4a76b44ce1 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -664,7 +664,7 @@ Adding `--json-envelope` to a query command (`search`, `definition`, `references Every top-level CLI/MCP JSON DTO (`StatusResult`, `RepoMapResult`, `SymbolAnalysisResult`, `ImpactAnalysisResult`, `OutlineResult`, `FileExcerptResult`, `CompactSearchResult`, `SymbolResult`, `DefinitionResult`, `UnusedSymbolResult`, `ReferenceResult`, `CallerResult`, `CalleeResult`, `FileResult`, `FileFindResult`) carries an `api_version` string field stamped from `JsonOutputContract.ApiVersion`. The same value is mirrored on the `--json-envelope` `metadata` block. This describes the JSON output contract, not the cdidx binary version (which is still surfaced via `version.json` and `cdidx --version`). Bump `JsonOutputContract.ApiVersion` only on **breaking** shape changes — renames, removals, or type changes of an existing field. Additive changes (new optional fields, new readiness flags, new enum values) keep the version stable so older consumers continue to parse the payload. Strict downstream consumers should pin against the major value and degrade gracefully when it changes. Issue #1555. -The documented `status --json` trust contract spans `fold_ready`, `fold_ready_reason`, `graph_table_available`, `issues_table_available`, `sql_graph_contract_ready`, `sql_graph_contract_degraded_reason`, `hotspot_family_ready`, `hotspot_family_degraded_reason`, `csharp_symbol_name_ready`, `csharp_metadata_target_ready`, `csharp_metadata_target_degraded_reason`, `indexed_head_commit`, `worktree_head_changed`, `indexed_head_sha`, `indexed_head_branch`, `indexed_head_timestamp`, `commits_ahead_of_indexed_head`, `index_writer_version`, `index_newer_than_reader`, `index_newer_than_reader_reason`, `unknown_extension_file_count`, `path_case_sensitive`, `data_dir_mode`, `mac_profile`, `stale_after_seconds`, `index_age_seconds`, plus the fold-only and C# metadata-target-only remediation fields `degraded_reason`, `recommended_action`, and `alternative_action`. Keep this list synchronized with `README.md` and `AGENT_GUIDE.md`; `DocumentationStatusContractTests` fails when any required field is missing from one of those docs. +The documented `status --json` trust contract spans `fold_ready`, `fold_ready_reason`, `graph_table_available`, `issues_table_available`, `sql_graph_contract_ready`, `sql_graph_contract_degraded_reason`, `hotspot_family_ready`, `hotspot_family_degraded_reason`, `csharp_symbol_name_ready`, `csharp_metadata_target_ready`, `csharp_metadata_target_degraded_reason`, `indexed_head_commit`, `worktree_head_changed`, `indexed_head_sha`, `indexed_head_branch`, `indexed_head_timestamp`, `commits_ahead_of_indexed_head`, `index_writer_version`, `index_newer_than_reader`, `index_newer_than_reader_reason`, `unknown_extension_file_count`, `path_case_sensitive`, `data_dir_mode`, `mac_profile`, `stale_after_seconds`, `index_age_seconds`, plus the fold-only and C# metadata-target-only remediation fields `degraded_reason`, `recommended_action`, `alternative_action`, and MCP-only `mcp_session`. MCP `mcp_session` is session-scoped diagnostics, not persisted DB state, and contains `log_level`, `roots`, and optional `client_capabilities`. Keep this list synchronized with `README.md` and `AGENT_GUIDE.md`; `DocumentationStatusContractTests` fails when any required field is missing from one of those docs. `references` already prefixes each human-readable row with `reference_kind`, and `callers` does the same for its grouped caller rows. When one grouped container mixes kinds (for example `call` and `subscribe` on the same event member), the human-readable label joins the distinct kinds with `+` (for example `call+subscribe`) instead of collapsing to a single preferred label, and the reference-kind column widens dynamically to fit the longest label in the batch so mixed rows do not overrun the neighbouring column. JSON output for `callers` and `callees` keeps the scalar `reference_kind` for back-compat (it reports the preferred summary kind `instantiate` > `subscribe` > `MIN(call)`) and adds a sorted `reference_kinds` array plus a `has_mixed_reference_kinds` bool so consumers can detect mixed containers without trusting a single collapsed label. This lets terminal users distinguish `call` / `instantiate` / `subscribe` / mixed without re-running the command with `--json` and lets AI clients answer mixed-kind questions without chasing a second `--exact` query. @@ -2371,7 +2371,7 @@ USER_GUIDEの[終了コード](USER_GUIDE.md#終了コード)セクションを - **AI向けの軽量検索スニペット** — `search --json` と MCP の `search` は、チャンク全文ではなく snippet range、match line、highlight、context count、`truncated_line_count`、`truncation_context` を持つ一致中心スニペットを返す。`truncation_context.char_counts` と `truncation_context.total_chars` はクランプされた各スニペット行の省略文字数を公開し、truncated な highlight も `truncated_char_counts` を持つ。`--snippet-lines` でペイロード量と文脈量のバランスを取れ、`--max-line-width`(CLI)/ `maxLineWidth`(MCP)は `find` / `references` / `excerpt` / `inspect` と同じ共有 `LineWidthFormatter.ClampLine` 契約で各スニペット行を最初のマッチトークン周辺にクランプするため、minified / transpiled / 生成された 1 行ファイル内の 1 ヒットで数百 KB を返さなくなる。クランプされた行はスニペットに `...(+N)...` マーカーが入り、`highlights[].truncated` と `highlights[].original_line_length` で AI クライアントがクランプを検出できる。 - **初動向けの repo map** — `map` は、インデックス済みデータから言語、モジュール、主要ファイル、ホットスポット、推定エントリポイントを集約し、AIクライアントが精密検索前に見るべき場所を決めやすくする。シンボル抽出が `Main` 系シンボルを出さない場合でも、既知のトップレベル実行ファイルへフォールバックして入口候補を補う。 - **信用判断のための鮮度メタデータ** — `status` はワークスペース全体の鮮度と git 状態を返す。`map` は `indexed_at` / `latest_modified` を絞り込み結果の鮮度として維持しつつ、`workspace_indexed_at` / `workspace_latest_modified` でワークスペース全体の鮮度も返す。`inspect` も同じワークスペース鮮度と git フィールドを返すため、シンボル中心の AI フローで `status` を別途呼ばずに済む。さらに `status` は `sql_graph_contract_ready` / `sql_graph_contract_degraded_reason`、`hotspot_family_ready` / `hotspot_family_degraded_reason` に加えて、forward-compatibility 監査 (`index_writer_version`、`index_newer_than_reader`、`index_newer_than_reader_reason`、詳細は「リーダー側の forward-compatibility 監査」を参照)、および fold-only remediation 用の `fold_ready_reason`、`degraded_reason`、`recommended_action`、`alternative_action` も返すため、AI クライアントは SQL graph/dependency/impact、duplicate-name hotspot family、Unicode `--exact` のどれが authoritative か、また DB が現在の binary より新しい `cdidx` で書かれていないかを最初に判断できる。現行の全体 scan 後は `unknown_extension_file_count` も返すため、未知拡張子で index 対象外になった件数を `status` から確認できる。これらの fold-only remediation field は、明示的な read-only `file:///...?...` DB URI から導出された場合でも、失敗する read-only URI をそのままコマンドへ埋め込まず、writable な filesystem path に正規化して返す。さらに `impact` / MCP `impact_analysis` に加えて、`inspect` / MCP `analyze_symbol`、`references` / `callers` / `callees`、`deps` / `unused` / `hotspots` 系も、SQL ベースの graph/dependency read が実際に結果へ関与したときだけ `sql_graph_contract_ready` / `sql_graph_contract_degraded_reason` を反映するため、stale な SQL 行が authoritative なヒットや 0 件応答に見えてしまうのを防ぎつつ、mixed-language index 内の純粋な非SQL結果を誤って degraded 扱いしない。`files` はファイルごとの checksum・modified・indexed timestamp を返す。古いDBに対する file 列の移行は可能なら自動で行い、その場移行できない場合でも読み取り経路がクラッシュしないようにする。CLI と MCP の 0 件 JSON レスポンスは `indexed_file_count`、`indexed_at`、`freshness_available` を含む。`freshness_available=true` で `indexed_at:null` なら空インデックス、`freshness_available=false` なら legacy/read-only DB で鮮度 timestamp を取得できず、理由は `freshness_degraded_reason` に入る。**HEAD 起点の stale 検知**: `cdidx index` の full scan が成功するたびに、現時点の `git HEAD` を `codeindex_meta` に stamp し、後続実行で workspace HEAD と比較できるようにする。`--rebuild` 指定なしに両者が異なる場合、CLI は `cdidx index --rebuild` を勧める `head_changed` 警告を表示し、`index --json` に `head_changed` / `prior_indexed_head_commit` / `current_head_commit` / `head_change_notice` を出力する。`status --check` も同じ比較を `workspace_check.head_changed` として公開し、差分時には `indexed_head_commit` / `workspace_head_commit` も併記するため、鮮度 gate ですでに `status --check` を通している AI クライアントは `git switch ` 後の既定の incremental scan を別クエリなしで拒否できる。`--commits` / `--files` の部分更新は意図的に記録 HEAD を維持し、次の full scan が worktree を再インデックスするまで stale 通知が継続する。非 Git workspace と HEAD を記録していない legacy DB は比較自体をスキップし、false-positive な警告を出さない。 -文書化された `status --json` trust contract は `fold_ready`、`fold_ready_reason`、`graph_table_available`、`issues_table_available`、`sql_graph_contract_ready`、`sql_graph_contract_degraded_reason`、`hotspot_family_ready`、`hotspot_family_degraded_reason`、`csharp_symbol_name_ready`、`csharp_metadata_target_ready`、`indexed_head_commit`、`worktree_head_changed`、`indexed_head_sha`、`indexed_head_branch`、`indexed_head_timestamp`、`commits_ahead_of_indexed_head`、`index_writer_version`、`index_newer_than_reader`、`index_newer_than_reader_reason`、`unknown_extension_file_count`、`path_case_sensitive`、`stale_after_seconds`、`index_age_seconds`、および fold-only remediation field の `degraded_reason`、`recommended_action`、`alternative_action` を対象にします。この一覧は `README.md` と `AGENT_GUIDE.md` に同期してください。いずれかの必須 field がこれらの docs から漏れると `DocumentationStatusContractTests` が失敗します。 +文書化された `status --json` trust contract は `fold_ready`、`fold_ready_reason`、`graph_table_available`、`issues_table_available`、`sql_graph_contract_ready`、`sql_graph_contract_degraded_reason`、`hotspot_family_ready`、`hotspot_family_degraded_reason`、`csharp_symbol_name_ready`、`csharp_metadata_target_ready`、`indexed_head_commit`、`worktree_head_changed`、`indexed_head_sha`、`indexed_head_branch`、`indexed_head_timestamp`、`commits_ahead_of_indexed_head`、`index_writer_version`、`index_newer_than_reader`、`index_newer_than_reader_reason`、`unknown_extension_file_count`、`path_case_sensitive`、`stale_after_seconds`、`index_age_seconds`、および fold-only remediation field の `degraded_reason`、`recommended_action`、`alternative_action`、および MCP 専用の `mcp_session` を対象にします。MCP `mcp_session` は永続化された DB 状態ではなく、セッション単位の診断情報で、`log_level`、`roots`、任意の `client_capabilities` を含みます。この一覧は `README.md` と `AGENT_GUIDE.md` に同期してください。いずれかの必須 field がこれらの docs から漏れると `DocumentationStatusContractTests` が失敗します。 - **再解析不要の folded-key アップグレード** — `backfill-fold` と MCP `backfill_fold` は、既存 DB 行から `name_folded` / `*_folded` を直接再計算し、必要な folded 値に NULL が残っていないことを検証してから `FoldReadyFlag` を stamp する。これにより、pre-#86 DB から AI クライアントやユーザーが低コストで Unicode `--exact` へ上がれる。さらに `fold_key_version` が未記録または不一致なら全 folded 行を再生成するため、将来の `NameFold.Version` 変更後に古い key を silent に再 stamp してしまうことも防ぐ。 - **まとめて取るシンボル分析** — `inspect` と MCP の `analyze_symbol` は、定義、近傍シンボル、参照、caller、callee、ファイルメタデータ、ワークスペース信頼メタデータ、graph 対応メタデータを1回で返し、AIクライアントが一般的なシンボル調査を少ない往復で終えやすくする。 - **言語考慮の参照抽出** — `references`、`callers`、`callees` は、正規表現ベースの call/reference 抽出が意味を持つ言語だけに対してインデックス化された参照テーブルで支える。未対応言語では、低信頼な疑似グラフ結果を返す代わりにテキスト検索へ戻る前提で設計する。**nested generic 呼び出し**: `new Dictionary>()` のような C#/Java のコンストラクタ呼び出しと、`Helper.DoWork>()` のような C# generic method call は、平坦な regex fast-path で `>>` を釣り合わせられなくても depth-aware fallback scanner で拾い直し、外側 target を参照テーブルへ残す。**コンストラクタ連鎖呼び出し**: C# の `: this(...)` / `: base(...)` イニシャライザと、Java のコンストラクタ本体冒頭文 `this(...)` / `super(...)` は、汎用 call regex とは別に検出し、呼び先が実際のコンストラクタとなるように書き換える(`this` は外側の class/record、`base` / `super` は外側クラスのシグネチャから解析した基底型)。C# のクロス行イニシャライザは外側クラスではなく、そのコンストラクタに紐付ける。基底型の解析は generic 引数、record のプライマリコンストラクタ引数、`where` 制約、`global::` やドット付きの namespace 修飾を剥がす。Java の `super.method()` は通常のメソッド呼び出しのまま扱う。**型位置の依存エッジ**: C#/Java の継承リスト、宣言型、generic 制約、`throws`、`is` / `as` / `instanceof`、および実際の C# XML doc `///` `cref` は `type_reference` 行として索引し、既定の `callers` / `callees` が見せる動的 call graph を汚さずに、`references` / `impact` から compile-time rename 依存を辿れるようにする。**SQL qualified-name alignment**: SQL の graph/dependency reader は、各 reference 行の source-line context、記録済み call 列位置、enclosing container から SQL 参照名を復元して定義と照合するため、qualified な `references` / `callers` / `impact` query は exact / non-exact を問わず sibling schema へ widen しない。source 側が genuinely unqualified な場合にだけ bare leaf fallback を許可するので、qualified call を含む `deps` / `unused` / `hotspots` も schema 単位で整合し、`EXEC dbo.fn_Target; EXEC sales.fn_Target;` のような同一行 multi-call も二重計上しない。列位置が記録されている row は、その列に qualified token が見つからなければ whole-line の別 qualified token へ昇格させないため、行末コメント・文字列リテラル・後続の別 call が先頭の unqualified edge を横取りすることもない。qualified な `callees` query でも caller query 自体が unqualified なとき以外は leaf fallback を無効化したため、`callees sales.Caller` が `dbo.Caller` へ広がらない。SQL extractor は qualified-name の `.` 前後空白も許容し、definition 系 reader は quoted qualified SQL name (`[dbo].[fn_X]` → `dbo.fn_X`) を正規化してから照合する。さらに exact SQL 定義照合は segment 数を保持し、SQL の exact graph leaf fallback は Unicode folded exact path を維持する。SQL CTE 本体内の source 行は raw `cte_body_reference` kind を使うため、`references --kind cte_body_reference` で anchor/recursive member 内部を outer query の table reference と区別できる。そのため、quoted single identifier の衝突や Unicode exact lookup の ASCII-only `NOCASE` 退行も防ぐ。 diff --git a/README.md b/README.md index 87df9e57bc..f85fea9523 100644 --- a/README.md +++ b/README.md @@ -117,10 +117,12 @@ The documented `status --json` trust contract covers these fields: unknown_extension_file_countpath_case_sensitivedata_dirdata_dir_source data_dir_modemac_profiledb_pragma_settingshooks stale_after_secondsindex_age_secondsdegraded_reasonrecommended_action -alternative_action +alternative_actionmcp_session +For MCP `status`, `mcp_session` is session-scoped diagnostic data rather than persisted index state. It includes `log_level`, `roots`, and optional `client_capabilities`. + `hotspot_family_degraded_reason` uses these values: | Value | Meaning | @@ -299,10 +301,12 @@ cdidx mcp unknown_extension_file_countpath_case_sensitivedata_dirdata_dir_source data_dir_modemac_profiledb_pragma_settingshooks stale_after_secondsindex_age_secondsdegraded_reasonrecommended_action -alternative_action +alternative_actionmcp_session +MCP `status` の `mcp_session` は永続化された index 状態ではなく、セッション単位の診断情報です。`log_level`、`roots`、任意の `client_capabilities` を含みます。 + `hotspot_family_degraded_reason` は次の値を使います。 | 値 | 意味 | From d408005cadb2ff003229be8462700adb136b8f9d Mon Sep 17 00:00:00 2001 From: Widthdom Date: Mon, 25 May 2026 09:37:38 +0900 Subject: [PATCH 8/8] Fix changelog fragment filename (#1836 #1837 #1840 #1895) --- .../{1836-1837-1840-1895.fixed.md => 1836-1895.fixed.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/unreleased/{1836-1837-1840-1895.fixed.md => 1836-1895.fixed.md} (100%) diff --git a/changelog.d/unreleased/1836-1837-1840-1895.fixed.md b/changelog.d/unreleased/1836-1895.fixed.md similarity index 100% rename from changelog.d/unreleased/1836-1837-1840-1895.fixed.md rename to changelog.d/unreleased/1836-1895.fixed.md