Skip to content
6 changes: 3 additions & 3 deletions src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ private void HandleListTools(JsonElement? id)
/// <remarks>
/// Log level precedence (highest to lowest):
/// 1. MCP <c>logging/setLevel</c> (Agent) - always wins, overrides CLI and Config.
/// 2. CLI <c>--LogLevel</c> flag.
/// 2. CLI <c>--log-level</c> flag.
/// 3. Config <c>runtime.telemetry.log-level</c>.
/// 4. Default: <c>None</c> for MCP stdio mode (silent by default to keep stdout clean for JSON-RPC),
/// <c>Error</c> in Production, <c>Debug</c> in Development.
Expand All @@ -334,7 +334,7 @@ private void HandleListTools(JsonElement? id)
/// hot-reloads do not overwrite the agent's choice.
/// 3. Restore <see cref="Console.Error"/> to the real stderr stream when logging is enabled,
/// in case startup redirected it to <see cref="TextWriter.Null"/> (default for
/// <c>--mcp-stdio</c> or <c>--LogLevel none</c>).
/// <c>--mcp-stdio</c> or <c>--log-level none</c>).
/// </remarks>
private void HandleSetLogLevel(JsonElement? id, JsonElement root)
{
Expand Down Expand Up @@ -393,7 +393,7 @@ private void HandleSetLogLevel(JsonElement? id, JsonElement root)
bool updated = logLevelController.UpdateFromMcp(level);

// Restore stderr if the agent successfully turned logging on. When `--mcp-stdio` (or
// `--LogLevel none`) was the startup default, stderr was redirected to TextWriter.Null;
// `--log-level none`) was the startup default, stderr was redirected to TextWriter.Null;
// re-enable it now so subsequent logs flow.
if (updated && isLoggingEnabled)
{
Expand Down
6 changes: 3 additions & 3 deletions src/Cli.Tests/CustomLoggerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Cli.Tests;
/// <summary>
/// Tests for <see cref="CustomLoggerProvider"/> covering both the standard CLI
/// path (writes to stdout/stderr with abbreviated labels) and the MCP stdio path
/// (suppressed by default, opt-in via either CLI <c>--LogLevel</c> or the
/// (suppressed by default, opt-in via either CLI <c>--log-level</c> or the
/// runtime config's <c>log-level</c>, always routed to stderr to keep the
/// JSON-RPC channel on stdout uncorrupted).
/// </summary>
Expand Down Expand Up @@ -85,7 +85,7 @@ public void LogOutput_UsesAbbreviatedLogLevelLabels(LogLevel logLevel, string ex
}

/// <summary>
/// MCP stdio mode with no overrides (neither CLI <c>--LogLevel</c> nor
/// MCP stdio mode with no overrides (neither CLI <c>--log-level</c> nor
/// config <c>log-level</c>): all output must be suppressed so the JSON-RPC
/// channel stays clean.
/// </summary>
Expand All @@ -106,7 +106,7 @@ public void Mcp_NoOverrides_SuppressesAllOutput()
}

/// <summary>
/// MCP stdio mode with a CLI-supplied <c>--LogLevel</c>: logs must always
/// MCP stdio mode with a CLI-supplied <c>--log-level</c>: logs must always
/// go to stderr (never stdout) and the level threshold from
/// <see cref="Cli.Utils.CliLogLevel"/> must be honored.
/// </summary>
Expand Down
39 changes: 21 additions & 18 deletions src/Cli.Tests/EndToEndTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -814,23 +814,25 @@ public Task TestUpdatingStoredProcedureWithRestMethods()
}

/// <summary>
/// Test to validate that the engine starts successfully when --verbose and --LogLevel
/// Test to validate that the engine starts successfully when --verbose and --log-level
/// options are used with the start command
/// This test does not validate whether the engine logs messages at the specified log level
/// </summary>
/// <param name="logLevelOption">Log level options</param>
[DataTestMethod]
[DataRow("", DisplayName = "No logging from command line.")]
[DataRow("--verbose", DisplayName = "Verbose logging from command line.")]
[DataRow("--LogLevel 0", DisplayName = "LogLevel 0 from command line.")]
[DataRow("--LogLevel 1", DisplayName = "LogLevel 1 from command line.")]
[DataRow("--LogLevel 2", DisplayName = "LogLevel 2 from command line.")]
[DataRow("--LogLevel Trace", DisplayName = "LogLevel Trace from command line.")]
[DataRow("--LogLevel Debug", DisplayName = "LogLevel Debug from command line.")]
[DataRow("--LogLevel Information", DisplayName = "LogLevel Information from command line.")]
[DataRow("--LogLevel tRace", DisplayName = "Case sensitivity: LogLevel Trace from command line.")]
[DataRow("--LogLevel DebUG", DisplayName = "Case sensitivity: LogLevel Debug from command line.")]
[DataRow("--LogLevel information", DisplayName = "Case sensitivity: LogLevel Information from command line.")]
[DataRow("--log-level 0", DisplayName = "LogLevel 0 from command line.")]
[DataRow("--log-level 1", DisplayName = "LogLevel 1 from command line.")]
[DataRow("--log-level 2", DisplayName = "LogLevel 2 from command line.")]
[DataRow("--log-level Trace", DisplayName = "LogLevel Trace from command line.")]
[DataRow("--log-level Debug", DisplayName = "LogLevel Debug from command line.")]
[DataRow("--log-level Information", DisplayName = "LogLevel Information from command line.")]
[DataRow("--log-level tRace", DisplayName = "Case sensitivity: LogLevel Trace from command line.")]
[DataRow("--log-level DebUG", DisplayName = "Case sensitivity: LogLevel Debug from command line.")]
[DataRow("--log-level information", DisplayName = "Case sensitivity: LogLevel Information from command line.")]
[DataRow("--LogLevel 0", DisplayName = "Case sensitivity: LogLevel 0 legacy from command line.")]
[DataRow("--LogLevel information", DisplayName = "Case sensitivity: LogLevel Information legacy from command line.")]
public void TestEngineStartUpWithVerboseAndLogLevelOptions(string logLevelOption)
{
_fileSystem!.File.WriteAllText(TEST_RUNTIME_CONFIG_FILE, INITIAL_CONFIG);
Expand All @@ -850,7 +852,7 @@ public void TestEngineStartUpWithVerboseAndLogLevelOptions(string logLevelOption
}

/// <summary>
/// Test to validate that the engine starts successfully when --LogLevel is set to Warning
/// Test to validate that the engine starts successfully when --log-level is set to Warning
/// or above. At these levels, CLI phase messages (logged at Information) are suppressed,
/// so no stdout output with message 'info' is expected during the CLI phase.
/// </summary>
Expand All @@ -871,7 +873,7 @@ public async Task TestEngineStartUpWithHighLogLevelOptions(string logLevelOption
StringWriter consoleOutput = new();
Console.SetOut(consoleOutput);

string[] args = { "start", "--config", TEST_RUNTIME_CONFIG_FILE, "--LogLevel", logLevelOption };
string[] args = { "start", "--config", TEST_RUNTIME_CONFIG_FILE, "--log-level", logLevelOption };
_fileSystem!.File.WriteAllText(TEST_RUNTIME_CONFIG_FILE, INITIAL_CONFIG);

// Run Program.Execute on a background task because StartEngine blocks until the host shuts down.
Expand All @@ -886,7 +888,7 @@ public async Task TestEngineStartUpWithHighLogLevelOptions(string logLevelOption
}

/// <summary>
/// Test to validate that the engine starts successfully when --LogLevel is set to None.
/// Test to validate that the engine starts successfully when --log-level is set to None.
/// At these levels, CLI phase messages (logged at Information) are suppressed,
/// so no stdout output is expected during the CLI phase.
/// </summary>
Expand All @@ -901,7 +903,7 @@ public async Task TestEngineStartUpWithLogLevelNone(string logLevelOption)
StringWriter consoleOutput = new();
Console.SetOut(consoleOutput);

string[] args = { "start", "--config", TEST_RUNTIME_CONFIG_FILE, "--LogLevel", logLevelOption };
string[] args = { "start", "--config", TEST_RUNTIME_CONFIG_FILE, "--log-level", logLevelOption };
_fileSystem!.File.WriteAllText(TEST_RUNTIME_CONFIG_FILE, INITIAL_CONFIG);

// Run Program.Execute on a background task because StartEngine blocks until the host shuts down.
Expand All @@ -915,12 +917,12 @@ public async Task TestEngineStartUpWithLogLevelNone(string logLevelOption)
}

/// Validates that `dab start` correctly sets <see cref="Startup.IsCliOverriding"/>
/// based on whether the --LogLevel CLI flag is provided.
/// based on whether the --log-level CLI flag is provided.
///
/// When the --LogLevel flag is provided, IsCliOverriding should be true.
/// When the --LogLevel flag is omitted (log level comes from the config file), IsCliOverriding should be false.
/// When the --log-level flag is provided, IsCliOverriding should be true.
/// When the --log-level flag is omitted (log level comes from the config file), IsCliOverriding should be false.
/// </summary>
/// <param name="cliLogLevel">The --LogLevel CLI flag value, or null to omit the flag.</param>
/// <param name="cliLogLevel">The --log-level CLI flag value, or null to omit the flag.</param>
/// <param name="expectedIsOverridden">Expected value of Startup.IsCliOverriding.</param>
[DataTestMethod]
[DataRow(null, false, DisplayName = "IsCliOverriding is false")]
Expand Down Expand Up @@ -978,6 +980,7 @@ public async Task TestStartCommandResolvesLogLevelFromConfigOrFlag(
isHttpsRedirectionDisabled: false,
mcpStdio: false,
mcpRole: null,
logLevelLegacy: null,
config: TEST_RUNTIME_CONFIG_FILE);

// Run TryStartEngineWithOptions on a background task because StartEngine blocks until the host shuts down.
Expand Down
8 changes: 6 additions & 2 deletions src/Cli/Commands/StartOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ public class StartOptions : Options

public LogBuffer CliBuffer { get; }

public StartOptions(bool verbose, LogLevel? logLevel, bool isHttpsRedirectionDisabled, bool mcpStdio, string? mcpRole, string config)
public StartOptions(bool verbose, LogLevel? logLevel, bool isHttpsRedirectionDisabled, bool mcpStdio, string? mcpRole, LogLevel? logLevelLegacy, string config)
: base(config)
{
// When verbose is true we set LogLevel to information.
LogLevel = verbose is true ? Microsoft.Extensions.Logging.LogLevel.Information : logLevel;
LogLevelLegacy = logLevelLegacy;
IsHttpsRedirectionDisabled = isHttpsRedirectionDisabled;
McpStdio = mcpStdio;
McpRole = mcpRole;
Expand All @@ -37,7 +38,7 @@ public StartOptions(bool verbose, LogLevel? logLevel, bool isHttpsRedirectionDis
[Option("verbose", SetName = "verbose", Required = false, HelpText = "Specifies logging level as informational.")]
public bool Verbose { get; }

[Option("LogLevel", SetName = "LogLevel", Required = false, HelpText = LOGLEVEL_HELPTEXT)]
[Option("log-level", SetName = "loglevel", Required = false, HelpText = LOGLEVEL_HELPTEXT)]
public LogLevel? LogLevel { get; }

[Option("no-https-redirect", Required = false, HelpText = "Disables automatic https redirects.")]
Expand All @@ -49,6 +50,9 @@ public StartOptions(bool verbose, LogLevel? logLevel, bool isHttpsRedirectionDis
[Value(0, MetaName = "role", Required = false, HelpText = "Optional MCP permissions role, e.g. role:anonymous. If omitted, defaults to anonymous.")]
public string? McpRole { get; }

[Option("LogLevel", SetName = "LogLevel", Required = false, HelpText = LOGLEVEL_HELPTEXT, Hidden = true)]
public LogLevel? LogLevelLegacy { get; }

public int Handler(ILogger logger, FileSystemRuntimeConfigLoader loader, IFileSystem fileSystem)
{
CliBuffer.BufferLog(Microsoft.Extensions.Logging.LogLevel.Information, $"{PRODUCT_NAME} {ProductInfo.GetProductVersion()}");
Expand Down
27 changes: 19 additions & 8 deletions src/Cli/ConfigGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3066,10 +3066,10 @@ public static bool TryStartEngineWithOptions(StartOptions options, FileSystemRun
List<string> args = new()
{ "--ConfigFileName", runtimeConfigFile };

/// Add arguments for LogLevel. Only pass --LogLevel when user explicitly specified it,
/// Add arguments for LogLevel. Only pass --log-level when user explicitly specified it,
/// so that MCP logging/setLevel can still adjust the level when no CLI override is present.
///
/// When --LogLevel is NOT specified:
/// When --log-level is NOT specified:
/// - MCP stdio mode: Service defaults to None for clean stdout output
/// - Non-MCP mode: Service defaults to Debug (Development) or Error (Production) based on config
LogLevel minimumLogLevel;
Expand All @@ -3079,19 +3079,30 @@ public static bool TryStartEngineWithOptions(StartOptions options, FileSystemRun
Utils.IsConfigOverriding = false;
Utils.ConfigLogLevel = LogLevel.Information;

LogLevel? logLevel = null;
if (options.LogLevel is not null)
{
if (options.LogLevel is < LogLevel.Trace or > LogLevel.None)
logLevel = options.LogLevel;
}
else if (options.LogLevelLegacy is not null)
{
options.CliBuffer.BufferLog(LogLevel.Warning, $"--LogLevel is deprecated, please use --log-level instead.");
logLevel = options.LogLevelLegacy;
}
Comment thread
RubenCerna2079 marked this conversation as resolved.

if (logLevel is not null)
{
if (logLevel is < LogLevel.Trace or > LogLevel.None)
{
options.CliBuffer.BufferLog(LogLevel.Error,
$"LogLevel's valid range is 0 to 6, your value: {options.LogLevel}, see: https://learn.microsoft.com/dotnet/api/microsoft.extensions.logging.loglevel");
$"LogLevel's valid range is 0 to 6, your value: {logLevel}, see: https://learn.microsoft.com/dotnet/api/microsoft.extensions.logging.loglevel");
return false;
}

minimumLogLevel = (LogLevel)options.LogLevel;
// Only add --LogLevel when user explicitly specified it via CLI.
minimumLogLevel = (LogLevel)logLevel;
// Only add --log-level when user explicitly specified it via CLI.
// This allows MCP logging/setLevel to work when no CLI override is present.
args.Add("--LogLevel");
args.Add("--log-level");
args.Add(minimumLogLevel.ToString());
}
else
Expand All @@ -3100,7 +3111,7 @@ public static bool TryStartEngineWithOptions(StartOptions options, FileSystemRun

// Track whether config explicitly set a log level. In MCP stdio mode this
// allows CLI logs to be emitted to stderr (instead of being suppressed)
// when the user expressed intent via the config file rather than --LogLevel.
// when the user expressed intent via the config file rather than --log-level.
if (deserializedRuntimeConfig.HasExplicitLogLevel())
{
Utils.IsConfigOverriding = true;
Expand Down
8 changes: 4 additions & 4 deletions src/Cli/CustomLoggerProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ public class CustomConsoleLogger : ILogger
private readonly LogLevel _minimumLogLevel;

// Minimum LogLevel for CLI output.
// For MCP mode: prefer CLI's --LogLevel, fall back to config's log-level, otherwise suppress all.
// For MCP mode: prefer CLI's --log-level, fall back to config's log-level, otherwise suppress all.
// For non-MCP mode: always use the level passed to the constructor.
// Note: --LogLevel is meant for the ENGINE's log level, not CLI's output.
// Note: --log-level is meant for the ENGINE's log level, not CLI's output.
public CustomConsoleLogger(LogLevel minimumLogLevel = LogLevel.Information)
{
_minimumLogLevel = Cli.Utils.IsMcpStdioMode
Expand Down Expand Up @@ -93,13 +93,13 @@ public CustomConsoleLogger(LogLevel minimumLogLevel = LogLevel.Information)
/// <summary>
/// Creates Log message by setting console message color based on LogLevel.
/// In MCP stdio mode:
/// - If user explicitly set --LogLevel (CLI) or log-level (config): write to stderr (colored output)
/// - If user explicitly set --log-level (CLI) or log-level (config): write to stderr (colored output)
/// - Otherwise: suppress entirely to keep stdout clean for JSON-RPC protocol.
/// </summary>
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
// In MCP stdio mode, only output logs if user explicitly requested a log level
// via either the CLI --LogLevel flag or the runtime config file's log-level.
// via either the CLI --log-level flag or the runtime config file's log-level.
// In that case, write to stderr to keep stdout clean for JSON-RPC.
if (Cli.Utils.IsMcpStdioMode)
{
Expand Down
3 changes: 2 additions & 1 deletion src/Cli/Exporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ private static async Task ExportGraphQL(
isHttpsRedirectionDisabled: false,
config: options.Config!,
mcpStdio: false,
mcpRole: null);
mcpRole: null,
logLevelLegacy: null);

Task dabService = Task.Run(() =>
{
Expand Down
2 changes: 1 addition & 1 deletion src/Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ private static void ParseEarlyFlags(string[] args)
{
Utils.IsMcpStdioMode = true;
}
else if (string.Equals(arg, "--LogLevel", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
else if (string.Equals(arg, "--log-level", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
{
Utils.IsCliOverriding = true;
if (Enum.TryParse<LogLevel>(args[i + 1], ignoreCase: true, out LogLevel cliLogLevel))
Expand Down
6 changes: 3 additions & 3 deletions src/Cli/Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,21 @@ public class Utils
public static bool IsMcpStdioMode { get; set; }

/// <summary>
/// When true, the CLI is the source overriding the log level (i.e., <c>--LogLevel</c> was supplied).
/// When true, the CLI is the source overriding the log level (i.e., <c>--log-level</c> was supplied).
/// This allows logs to be written to stderr instead of being completely suppressed.
/// </summary>
public static bool IsCliOverriding { get; set; }

/// <summary>
/// The log level specified via CLI --LogLevel flag.
/// The log level specified via CLI --log-level flag.
/// Only valid when IsCliOverriding is true.
/// </summary>
public static LogLevel CliLogLevel { get; set; } = LogLevel.Information;

/// <summary>
/// When true, the runtime config is the source overriding the log level
/// (i.e., <c>runtime.telemetry.log-level</c> was explicitly set).
/// This allows CLI logs to be written to stderr in MCP mode even when no --LogLevel flag was provided.
/// This allows CLI logs to be written to stderr in MCP mode even when no --log-level flag was provided.
/// </summary>
public static bool IsConfigOverriding { get; set; }

Expand Down
4 changes: 2 additions & 2 deletions src/Core/Telemetry/ILogLevelController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public interface ILogLevelController
{
/// <summary>
/// Gets a value indicating whether the CLI is the source overriding the log level
/// (i.e., <c>--LogLevel</c> was supplied). When true, runtime-config (hot-reload)
/// (i.e., <c>--log-level</c> was supplied). When true, runtime-config (hot-reload)
/// updates are ignored.
/// </summary>
bool IsCliOverriding { get; }
Expand All @@ -35,7 +35,7 @@ public interface ILogLevelController
/// The MCP level string is mapped to the appropriate LogLevel.
/// Log-level precedence (highest to lowest):
/// 1. Agent (MCP <c>logging/setLevel</c>) — always wins.
/// 2. CLI <c>--LogLevel</c> flag.
/// 2. CLI <c>--log-level</c> flag.
/// 3. Config <c>runtime.telemetry.log-level</c>.
/// 4. Defaults.
/// </summary>
Expand Down
Loading
Loading