From 814c44d0eae6eeeb370ae4b1c04c0209419e67f5 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 21 Aug 2026 02:32:00 +0900 Subject: [PATCH] Fix MCP audit option discoverability (#5096) --- USER_GUIDE.md | 4 ++ changelog.d/unreleased/5096.fixed.md | 21 +++++++ src/CodeIndex/Cli/CliFlagSchema.cs | 8 ++- src/CodeIndex/Cli/ConsoleUi.cs | 2 +- src/CodeIndex/Cli/ProgramRunner.Mcp.cs | 2 +- tests/CodeIndex.Tests/CliFlagSchemaTests.cs | 61 +++++++++++++++++++++ tests/CodeIndex.Tests/ConsoleUiTests.cs | 35 +++++++++++- tests/CodeIndex.Tests/ProgramRunnerTests.cs | 11 +++- 8 files changed, 138 insertions(+), 6 deletions(-) create mode 100644 changelog.d/unreleased/5096.fixed.md diff --git a/USER_GUIDE.md b/USER_GUIDE.md index f423fc28b..09a97a989 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -2532,6 +2532,8 @@ three request-id fields. `cdidx mcp` can opt in to a per-tool-call audit log so compliance reviewers can answer *"who called which tool with what shape of arguments and when did it fail?"* without re-running the index. Audit emission is off by default; pass `--audit-log ` to the `cdidx mcp` invocation to enable it. The destination file is opened append-only and rotated through `.1` and `.2` when the active file exceeds the configured size cap, dropping the oldest slot rather than spilling further. +Run `cdidx help mcp` to review every public MCP option and its dependencies. The same option inventory drives the generated Bash, Zsh, Fish, and PowerShell completions. + | Flag | Default | Effect | |---|---|---| | `--audit-log ` | (off) | Enable audit emission and write JSONL records to ``. The parent directory is created if missing. | @@ -6030,6 +6032,8 @@ CLI metrics には JSON-RPC id がないため、この 3 field をすべて省 `cdidx mcp` に `--audit-log ` を渡すと、ツール呼び出しごとに 1 レコードの JSONL 監査ログを出力できます。「誰が・どんな引数形で・いつ呼び出して失敗したか」を後追いするためのコンプライアンス用途を想定しており、既定では無効です。出力先は append 専用で開かれ、サイズ上限を超えると `.1` → `.2` の順にローテーションされ、最古スロットは破棄されます(`.3` 以降は決して残りません)。 +すべての公開 MCP オプションと依存関係は `cdidx help mcp` で確認できます。同じオプション一覧から Bash、Zsh、Fish、PowerShell の補完も生成されます。 + | フラグ | 既定 | 効果 | |---|---|---| | `--audit-log ` | (無効) | 監査出力を有効化し `` に JSONL を書き出す。親ディレクトリは無ければ自動作成 | diff --git a/changelog.d/unreleased/5096.fixed.md b/changelog.d/unreleased/5096.fixed.md new file mode 100644 index 000000000..9d149fa3f --- /dev/null +++ b/changelog.d/unreleased/5096.fixed.md @@ -0,0 +1,21 @@ +--- +category: fixed +issues: + - 5096 +affected: + - src/CodeIndex/Cli/CliFlagSchema.cs + - src/CodeIndex/Cli/ConsoleUi.cs + - src/CodeIndex/Cli/ProgramRunner.Mcp.cs + - tests/CodeIndex.Tests/CliFlagSchemaTests.cs + - tests/CodeIndex.Tests/ConsoleUiTests.cs + - tests/CodeIndex.Tests/ProgramRunnerTests.cs + - USER_GUIDE.md +--- + +## English + +- **MCP strict audit logging is discoverable in help and shell completions (#5096)** — `cdidx help mcp`, validation-error usage, and generated Bash, Zsh, Fish, and PowerShell completions now share the public MCP option inventory, including `--audit-log-strict` and its `--audit-log` dependency. + +## 日本語 + +- **MCP の厳格な監査ログ設定をヘルプとシェル補完から確認できるようになりました (#5096)** — `cdidx help mcp`、検証エラー時の使用方法、生成される Bash / Zsh / Fish / PowerShell の補完が公開 MCP オプション一覧を共有し、`--audit-log-strict` とその `--audit-log` 依存関係を提示します。 diff --git a/src/CodeIndex/Cli/CliFlagSchema.cs b/src/CodeIndex/Cli/CliFlagSchema.cs index 22edafc1a..23b915553 100644 --- a/src/CodeIndex/Cli/CliFlagSchema.cs +++ b/src/CodeIndex/Cli/CliFlagSchema.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using CodeIndex.Mcp; namespace CodeIndex.Cli; @@ -188,7 +189,7 @@ internal static class CliFlagSchema "index", "hooks", "backfill-fold", "optimize", "vacuum", "search", "recipes", "audit", "definition", "goto", "references", "callers", "callees", "symbols", "files", "find", "excerpt", "map", "inspect", "outline", "status", - "validate", "deps", "impact", "unused", "hotspots", "languages"); + "validate", "deps", "impact", "unused", "hotspots", "languages", "mcp"); public static bool HasAuthoritativeHelpOptions(string command) => AuthoritativeHelpOptionCommands.Contains(command); @@ -686,6 +687,11 @@ private static IReadOnlyList BuildAll() new() { Name = "--transport", ValueDomain = Values(["stdio", "http"]) with { CompletionLabel = "transport" }, Description = "MCP transport", PrimaryCommands = Set("mcp") }, new() { Name = "--http-listen", ValuePlaceholder = "", Description = "MCP HTTP listen address", PrimaryCommands = Set("mcp") }, new() { Name = "--allow-unauthenticated-http", Description = "MCP HTTP: explicitly allow unsafe unauthenticated loopback mode", PrimaryCommands = Set("mcp") }, + new() { Name = "--audit-log", ValuePlaceholder = "", ValueKind = CliOptionValueKind.FilePath, Description = "MCP audit log JSONL path; creates missing parent directories", PrimaryCommands = Set("mcp") }, + new() { Name = "--audit-log-include-values", Description = "MCP audit log: include redacted argument values; requires --audit-log", PrimaryCommands = Set("mcp") }, + new() { Name = "--audit-log-max-bytes", ValuePlaceholder = "", Description = $"MCP audit log rotation threshold in bytes ({AuditLogSink.MinMaxBytes}..{AuditLogSink.MaxMaxBytes}; default {AuditLogSink.DefaultMaxBytes})", PrimaryCommands = Set("mcp") }, + new() { Name = "--audit-log-strict", Description = "MCP audit log: require shutdown flush; requires --audit-log; an incomplete flush changes only an otherwise-successful MCP session to runtime exit code 10 and preserves existing nonzero exits", PrimaryCommands = Set("mcp"), Safety = CliOptionSafety.StrictFailure }, + new() { Name = "--suggestion-dedup-threshold", ValuePlaceholder = "<0..1>", Description = "MCP suggestion deduplication threshold override", PrimaryCommands = Set("mcp") }, }; } diff --git a/src/CodeIndex/Cli/ConsoleUi.cs b/src/CodeIndex/Cli/ConsoleUi.cs index 4cc81666d..664128ed8 100644 --- a/src/CodeIndex/Cli/ConsoleUi.cs +++ b/src/CodeIndex/Cli/ConsoleUi.cs @@ -150,7 +150,7 @@ private static readonly (string Command, string Usage)[] CommandUsageLines = ("hooks-install", "cdidx hooks install [--project ] [--force] [--dry-run] [--json]"), ("hooks-uninstall", "cdidx hooks uninstall [--project ] [--force] [--json]"), ("hooks-status", "cdidx hooks status [--project ] [--json]"), - ("mcp", "cdidx mcp [--db ] [--transport stdio|http] [--http-listen ] [--allow-unauthenticated-http] [--audit-log ] [--audit-log-include-values] [--audit-log-max-bytes ] [--suggestion-dedup-threshold <0..1>]"), + ("mcp", "cdidx mcp [--db ] [--transport stdio|http] [--http-listen ] [--allow-unauthenticated-http] [--audit-log ] [--audit-log-include-values] [--audit-log-max-bytes ] [--audit-log-strict] [--suggestion-dedup-threshold <0..1>]"), ("lsp", "cdidx lsp [--db ]"), ("completions", "cdidx completions "), ("--completions", "cdidx --completions "), diff --git a/src/CodeIndex/Cli/ProgramRunner.Mcp.cs b/src/CodeIndex/Cli/ProgramRunner.Mcp.cs index b3910149b..d16b9af81 100644 --- a/src/CodeIndex/Cli/ProgramRunner.Mcp.cs +++ b/src/CodeIndex/Cli/ProgramRunner.Mcp.cs @@ -519,7 +519,7 @@ private static string FormatLogValue(string? value) private static void PrintMcpUsage() { - CommandErrorWriter.WriteStderr($"Usage: cdidx mcp [--db ] [--transport stdio|http] [--http-listen ] [{AllowUnauthenticatedHttpFlag}] [--audit-log ] [--audit-log-include-values] [--audit-log-max-bytes ] [--audit-log-strict] [--suggestion-dedup-threshold <0..1>]"); + CommandErrorWriter.WriteStderr($"Usage: {ConsoleUi.GetUsageLine("mcp") ?? "cdidx mcp"}"); CommandErrorWriter.WriteStderr("Note: --json is not supported; MCP requests and responses are JSON-RPC over the selected transport."); CommandErrorWriter.WriteStderr("stdio transport: one UTF-8 JSON-RPC object per LF-delimited line, not LSP Content-Length framing; lifecycle diagnostics are written to stderr."); CommandErrorWriter.WriteStderr($"HTTP security: bearer auth is required by default; {AllowUnauthenticatedHttpFlag} is an explicit unsafe loopback-only opt-in. Native clients omit Origin; POST requires UTF-8 application/json."); diff --git a/tests/CodeIndex.Tests/CliFlagSchemaTests.cs b/tests/CodeIndex.Tests/CliFlagSchemaTests.cs index 83c81e611..c07ac6c36 100644 --- a/tests/CodeIndex.Tests/CliFlagSchemaTests.cs +++ b/tests/CodeIndex.Tests/CliFlagSchemaTests.cs @@ -128,6 +128,67 @@ public void GetAcceptedFlagNamesForCommand_UnionsCommandsAndAlsoAcceptedBy() Assert.DoesNotContain(CliFlagSchema.GetCompletionFlagsForCommand("definition"), f => f.Name == "--exact-substring"); } + [Fact] + public void McpPublicOptionInventoryMatchesParserAndAuthoritativeHelp_Issue5096() + { + string[] expectedOptions = + [ + "--db", + "--quiet", + "--silent", + "--no-progress", + "--transport", + "--http-listen", + "--allow-unauthenticated-http", + "--audit-log", + "--audit-log-include-values", + "--audit-log-max-bytes", + "--audit-log-strict", + "--suggestion-dedup-threshold", + ]; + var inventory = CliFlagSchema.GetCompletionFlagsForCommand("mcp") + .Select(flag => flag.Name) + .ToArray(); + + Assert.Equal(expectedOptions, inventory); + Assert.Equal( + expectedOptions.OrderBy(option => option, StringComparer.Ordinal), + CliFlagSchema.GetAcceptedFlagNamesForCommand("mcp").OrderBy(option => option, StringComparer.Ordinal)); + + var parserProbes = new Dictionary(StringComparer.Ordinal) + { + ["--db"] = ["mcp", "--db"], + ["--quiet"] = ["--quiet", "mcp", "--audit-log-strict"], + ["--silent"] = ["--silent", "mcp", "--audit-log-strict"], + ["--no-progress"] = ["--no-progress", "mcp", "--audit-log-strict"], + ["--transport"] = ["mcp", "--transport"], + ["--http-listen"] = ["mcp", "--http-listen"], + ["--allow-unauthenticated-http"] = ["mcp", "--allow-unauthenticated-http"], + ["--audit-log"] = ["mcp", "--audit-log"], + ["--audit-log-include-values"] = ["mcp", "--audit-log-include-values"], + ["--audit-log-max-bytes"] = ["mcp", "--audit-log-max-bytes"], + ["--audit-log-strict"] = ["mcp", "--audit-log-strict"], + ["--suggestion-dedup-threshold"] = ["mcp", "--suggestion-dedup-threshold"], + }; + Assert.Equal( + expectedOptions.OrderBy(option => option, StringComparer.Ordinal), + parserProbes.Keys.OrderBy(option => option, StringComparer.Ordinal)); + foreach (var (option, args) in parserProbes) + { + var (exitCode, _, stderr) = ConsoleCapture.Capture(() => + ProgramRunner.Run(args, appVersion: "1.10.0")); + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.DoesNotContain($"{option} is not supported for mcp", stderr, StringComparison.Ordinal); + } + + var (printed, helpOutput, helpError) = ConsoleCapture.Capture(() => + ConsoleUi.PrintCommandUsage("mcp") ? 1 : 0); + Assert.Equal(1, printed); + Assert.Empty(helpError); + foreach (var option in expectedOptions) + Assert.Contains(option, helpOutput, StringComparison.Ordinal); + } + [Fact] public void Goto_AcceptsDocumentedExcludeFilters_Issue3934() { diff --git a/tests/CodeIndex.Tests/ConsoleUiTests.cs b/tests/CodeIndex.Tests/ConsoleUiTests.cs index 8dfcacb69..606f974fa 100644 --- a/tests/CodeIndex.Tests/ConsoleUiTests.cs +++ b/tests/CodeIndex.Tests/ConsoleUiTests.cs @@ -1100,7 +1100,7 @@ public void CompletionRenderer_BashCompletesFlagValues() var output = ConsoleCompletionRenderer.GetCompletionScript("bash"); Assert.Contains("--db|--workspace-db|--data-dir|--metrics|--path|--project|--solution|--exclude-path", output); - Assert.Contains("--files|--output|-o) COMPREPLY=($(compgen -f -- \"$cur\"))", output); + Assert.Contains("--files|--output|-o|--audit-log) COMPREPLY=($(compgen -f -- \"$cur\"))", output); Assert.Contains("--lang|--language) COMPREPLY=($(compgen -W \"", output); Assert.Contains("csharp", output); Assert.Contains("python", output); @@ -1519,6 +1519,39 @@ public void CompletionRenderer_SuggestionsExposesOutputAcrossShells_Issue4719() Assert.Contains("'--max-json-bytes'", suggestionsBranch, StringComparison.Ordinal); } + [Fact] + public void CompletionRenderer_McpPublicOptionsMatchSchemaAndStrictAppearsOnce_Issue5096() + { + var expected = new SortedSet( + CliFlagSchema.GetCompletionFlagsForCommand("mcp") + .Select(flag => flag.Name.TrimStart('-')), + StringComparer.Ordinal); + var flagSets = ExtractComparableSubcommandFlagSets("mcp", "lsp"); + + Assert.Equal(expected, flagSets.Bash); + Assert.Equal(expected, flagSets.Zsh); + Assert.Equal(expected, flagSets.Fish); + + var powerShell = ConsoleCompletionRenderer.GetCompletionScript("powershell"); + var powerShellBranch = ExtractBetween( + powerShell, + "'mcp' { $flags = @(", + ") }"); + var powerShellFlags = new SortedSet( + Regex.Matches(powerShellBranch, @"'--(?[a-z][a-z0-9-]*)'") + .Select(match => match.Groups["name"].Value), + StringComparer.Ordinal); + powerShellFlags.Remove("help"); + Assert.Equal(expected, powerShellFlags); + + foreach (var script in new[] { flagSets.BashScript, flagSets.ZshScript, flagSets.FishScript, powerShell }) + { + Assert.Single( + Regex.Matches(script, @"(?()); + } + } + private static (SortedSet Bash, SortedSet Zsh, SortedSet Fish, string BashScript, string ZshScript, string FishScript) ExtractComparableSubcommandFlagSets(string subcommand, string nextSubcommand) { diff --git a/tests/CodeIndex.Tests/ProgramRunnerTests.cs b/tests/CodeIndex.Tests/ProgramRunnerTests.cs index f04f9f86c..6535bd43b 100644 --- a/tests/CodeIndex.Tests/ProgramRunnerTests.cs +++ b/tests/CodeIndex.Tests/ProgramRunnerTests.cs @@ -6210,16 +6210,23 @@ public void TryConsumeAuditLogFlags_StrictAfterDoubleDash_IsPreserved() } [Fact] - public void RunMcp_AuditLogStrictWithoutPathPrintsUpdatedUsage_Issue4553() + public void RunMcp_HelpAndStrictValidationUsageExposeAuditLogContract_Issues4553_5096() { + var (helpExitCode, helpStdout, helpStderr) = CaptureConsole(() => ProgramRunner.Run( + ["help", "mcp"], + appVersion: "1.10.0")); var (exitCode, stdout, stderr) = CaptureConsole(() => ProgramRunner.Run( ["mcp", "--audit-log-strict"], appVersion: "1.10.0")); + Assert.Equal(CommandExitCodes.Success, helpExitCode); + Assert.Empty(helpStderr); + Assert.Contains("--audit-log-strict", helpStdout, StringComparison.Ordinal); + Assert.Contains("requires --audit-log", helpStdout, StringComparison.Ordinal); Assert.Equal(CommandExitCodes.UsageError, exitCode); Assert.Empty(stdout); Assert.Contains("--audit-log-strict requires --audit-log", stderr, StringComparison.Ordinal); - Assert.Contains("Usage: cdidx mcp", stderr, StringComparison.Ordinal); + Assert.Contains($"Usage: {ConsoleUi.GetUsageLine("mcp")}", stderr, StringComparison.Ordinal); Assert.Contains("[--audit-log-strict]", stderr, StringComparison.Ordinal); }