Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <path>` to the `cdidx mcp` invocation to enable it. The destination file is opened append-only and rotated through `<path>.1` and `<path>.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 <path>` | (off) | Enable audit emission and write JSONL records to `<path>`. The parent directory is created if missing. |
Expand Down Expand Up @@ -6030,6 +6032,8 @@ CLI metrics には JSON-RPC id がないため、この 3 field をすべて省

`cdidx mcp` に `--audit-log <path>` を渡すと、ツール呼び出しごとに 1 レコードの JSONL 監査ログを出力できます。「誰が・どんな引数形で・いつ呼び出して失敗したか」を後追いするためのコンプライアンス用途を想定しており、既定では無効です。出力先は append 専用で開かれ、サイズ上限を超えると `<path>.1` → `<path>.2` の順にローテーションされ、最古スロットは破棄されます(`<path>.3` 以降は決して残りません)。

すべての公開 MCP オプションと依存関係は `cdidx help mcp` で確認できます。同じオプション一覧から Bash、Zsh、Fish、PowerShell の補完も生成されます。

| フラグ | 既定 | 効果 |
|---|---|---|
| `--audit-log <path>` | (無効) | 監査出力を有効化し `<path>` に JSONL を書き出す。親ディレクトリは無ければ自動作成 |
Expand Down
21 changes: 21 additions & 0 deletions changelog.d/unreleased/5096.fixed.md
Original file line number Diff line number Diff line change
@@ -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` 依存関係を提示します。
8 changes: 7 additions & 1 deletion src/CodeIndex/Cli/CliFlagSchema.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using CodeIndex.Mcp;

namespace CodeIndex.Cli;

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -686,6 +687,11 @@ private static IReadOnlyList<CliFlag> BuildAll()
new() { Name = "--transport", ValueDomain = Values(["stdio", "http"]) with { CompletionLabel = "transport" }, Description = "MCP transport", PrimaryCommands = Set("mcp") },
new() { Name = "--http-listen", ValuePlaceholder = "<host:port>", 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 = "<path>", 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 = "<n>", 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") },
};
}

Expand Down
2 changes: 1 addition & 1 deletion src/CodeIndex/Cli/ConsoleUi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ private static readonly (string Command, string Usage)[] CommandUsageLines =
("hooks-install", "cdidx hooks install [--project <path>] [--force] [--dry-run] [--json]"),
("hooks-uninstall", "cdidx hooks uninstall [--project <path>] [--force] [--json]"),
("hooks-status", "cdidx hooks status [--project <path>] [--json]"),
("mcp", "cdidx mcp [--db <path>] [--transport stdio|http] [--http-listen <host:port>] [--allow-unauthenticated-http] [--audit-log <path>] [--audit-log-include-values] [--audit-log-max-bytes <n>] [--suggestion-dedup-threshold <0..1>]"),
("mcp", "cdidx mcp [--db <path>] [--transport stdio|http] [--http-listen <host:port>] [--allow-unauthenticated-http] [--audit-log <path>] [--audit-log-include-values] [--audit-log-max-bytes <n>] [--audit-log-strict] [--suggestion-dedup-threshold <0..1>]"),
("lsp", "cdidx lsp [--db <path>]"),
("completions", "cdidx completions <shell>"),
("--completions", "cdidx --completions <shell>"),
Expand Down
2 changes: 1 addition & 1 deletion src/CodeIndex/Cli/ProgramRunner.Mcp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,7 @@ private static string FormatLogValue(string? value)

private static void PrintMcpUsage()
{
CommandErrorWriter.WriteStderr($"Usage: cdidx mcp [--db <path>] [--transport stdio|http] [--http-listen <host:port>] [{AllowUnauthenticatedHttpFlag}] [--audit-log <path>] [--audit-log-include-values] [--audit-log-max-bytes <n>] [--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.");
Expand Down
61 changes: 61 additions & 0 deletions tests/CodeIndex.Tests/CliFlagSchemaTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string[]>(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()
{
Expand Down
35 changes: 34 additions & 1 deletion tests/CodeIndex.Tests/ConsoleUiTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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<string>(
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<string>(
Regex.Matches(powerShellBranch, @"'--(?<name>[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, @"(?<![a-z0-9])audit-log-strict(?![a-z0-9-])")
.Cast<Match>());
}
}

private static (SortedSet<string> Bash, SortedSet<string> Zsh, SortedSet<string> Fish, string BashScript, string ZshScript, string FishScript)
ExtractComparableSubcommandFlagSets(string subcommand, string nextSubcommand)
{
Expand Down
11 changes: 9 additions & 2 deletions tests/CodeIndex.Tests/ProgramRunnerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Loading