diff --git a/src/Cli/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs
index 497d387e72..2fddad734c 100644
--- a/src/Cli/ConfigGenerator.cs
+++ b/src/Cli/ConfigGenerator.cs
@@ -2606,13 +2606,38 @@ public static bool TryStartEngineWithOptions(StartOptions options, FileSystemRun
}
else
{
- minimumLogLevel = deserializedRuntimeConfig.GetConfiguredLogLevel();
- HostMode hostModeType = deserializedRuntimeConfig.IsDevelopmentMode() ? HostMode.Development : HostMode.Production;
+ // When --mcp-stdio is used without explicit --LogLevel:
+ // 1. Check if config has log-level set - use that (Config has priority 2)
+ // 2. Otherwise default to None for clean MCP stdio output
+ if (options.McpStdio)
+ {
+ // Check if config explicitly sets a log level
+ if (!deserializedRuntimeConfig.IsLogLevelNull())
+ {
+ minimumLogLevel = deserializedRuntimeConfig.GetConfiguredLogLevel();
+ _logger.LogInformation("MCP stdio mode: Using config log-level: {minimumLogLevel}.", minimumLogLevel);
+ // Pass --LogLevel to Service with special marker to indicate it's from config, not CLI.
+ // This allows MCP logging/setLevel to be blocked by config override.
+ args.Add("--LogLevel");
+ args.Add(minimumLogLevel.ToString());
+ args.Add("--LogLevelFromConfig");
+ }
+ else
+ {
+ _logger.LogInformation("MCP stdio mode: Defaulting to LogLevel.None (no config override).");
+ // Don't add --LogLevel to args - let Service handle the default.
+ }
+ }
+ else
+ {
+ minimumLogLevel = deserializedRuntimeConfig.GetConfiguredLogLevel();
+ HostMode hostModeType = deserializedRuntimeConfig.IsDevelopmentMode() ? HostMode.Development : HostMode.Production;
- _logger.LogInformation($"Setting default minimum LogLevel: {minimumLogLevel} for {hostModeType} mode.", minimumLogLevel, hostModeType);
+ _logger.LogInformation($"Setting default minimum LogLevel: {minimumLogLevel} for {hostModeType} mode.", minimumLogLevel, hostModeType);
- // Don't add --LogLevel arg since user didn't explicitly set it.
- // Service will determine default log level based on config or host mode.
+ // Don't add --LogLevel arg since user didn't explicitly set it.
+ // Service will determine default log level based on config or host mode.
+ }
}
// This will add args to disable automatic redirects to https if specified by user
diff --git a/src/Cli/CustomLoggerProvider.cs b/src/Cli/CustomLoggerProvider.cs
index 0f7a881da8..89836c73ee 100644
--- a/src/Cli/CustomLoggerProvider.cs
+++ b/src/Cli/CustomLoggerProvider.cs
@@ -71,9 +71,16 @@ public class CustomConsoleLogger : ILogger
///
/// Creates Log message by setting console message color based on LogLevel.
+ /// Skips logging when in MCP stdio mode to keep stdout clean for JSON-RPC protocol.
///
public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter)
{
+ // In MCP stdio mode, suppress all CLI logging to keep stdout clean for JSON-RPC.
+ if (Cli.Utils.IsMcpStdioMode)
+ {
+ return;
+ }
+
if (!IsEnabled(logLevel) || logLevel < _minimumLogLevel)
{
return;
diff --git a/src/Cli/Program.cs b/src/Cli/Program.cs
index de16ed27f5..0a139bf1bf 100644
--- a/src/Cli/Program.cs
+++ b/src/Cli/Program.cs
@@ -26,6 +26,9 @@ public static int Main(string[] args)
// Load environment variables from .env file if present.
DotNetEnv.Env.Load();
+ // Check if MCP stdio mode is requested - suppress CLI logging to keep stdout clean for JSON-RPC.
+ Utils.IsMcpStdioMode = args.Any(a => string.Equals(a, "--mcp-stdio", StringComparison.OrdinalIgnoreCase));
+
// Logger setup and configuration
ILoggerFactory loggerFactory = Utils.LoggerFactoryForCli;
ILogger cliLogger = loggerFactory.CreateLogger();
diff --git a/src/Cli/Utils.cs b/src/Cli/Utils.cs
index c1ff7f2a99..3ef475a9e4 100644
--- a/src/Cli/Utils.cs
+++ b/src/Cli/Utils.cs
@@ -23,6 +23,11 @@ public class Utils
public const string WILDCARD = "*";
public static readonly string SEPARATOR = ":";
+ ///
+ /// When true, CLI logging to stdout is suppressed to keep the MCP stdio channel clean.
+ ///
+ public static bool IsMcpStdioMode { get; set; }
+
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
private static ILogger _logger;
#pragma warning restore CS8618
diff --git a/src/Service/Program.cs b/src/Service/Program.cs
index 61829238b6..c429069050 100644
--- a/src/Service/Program.cs
+++ b/src/Service/Program.cs
@@ -4,6 +4,7 @@
using System;
using System.CommandLine;
using System.CommandLine.Parsing;
+using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
@@ -63,6 +64,28 @@ public static bool StartEngine(string[] args, bool runMcpStdio, string? mcpRole)
{
try
{
+ // Initialize log level EARLY, before building the host.
+ // This ensures logging filters are effective during the entire host build process.
+ LogLevel initialLogLevel = GetLogLevelFromCommandLineArgs(args, runMcpStdio, out bool isCliOverridden, out bool isConfigOverridden);
+ LogLevelProvider.SetInitialLogLevel(initialLogLevel, isCliOverridden, isConfigOverridden);
+
+ // For MCP stdio mode, redirect Console.Out to keep stdout clean for JSON-RPC.
+ // MCP SDK uses Console.OpenStandardOutput() which gets the real stdout, unaffected by this redirect.
+ if (runMcpStdio)
+ {
+ // When LogLevel.None, redirect to null stream for ZERO output.
+ // Otherwise redirect to stderr so logs don't pollute JSON-RPC.
+ if (initialLogLevel == LogLevel.None)
+ {
+ Console.SetOut(TextWriter.Null);
+ Console.SetError(TextWriter.Null);
+ }
+ else
+ {
+ Console.SetOut(Console.Error);
+ }
+ }
+
IHost host = CreateHostBuilder(args, runMcpStdio, mcpRole).Build();
if (runMcpStdio)
@@ -115,13 +138,20 @@ public static IHostBuilder CreateHostBuilder(string[] args, bool runMcpStdio, st
})
.ConfigureLogging(logging =>
{
- logging.AddFilter("Microsoft", logLevel => LogLevelProvider.ShouldLog(logLevel));
- logging.AddFilter("Microsoft.Hosting.Lifetime", logLevel => LogLevelProvider.ShouldLog(logLevel));
+ // Set minimum level at the framework level - this affects all loggers.
+ // For MCP stdio mode, Console.Out is redirected to stderr in Main(),
+ // so any logging output goes to stderr and doesn't pollute the JSON-RPC channel.
+ logging.SetMinimumLevel(LogLevelProvider.CurrentLogLevel);
+
+ // Add filter for dynamic log level changes (e.g., via MCP logging/setLevel)
+ logging.AddFilter(logLevel => LogLevelProvider.ShouldLog(logLevel));
})
.ConfigureWebHostDefaults(webBuilder =>
{
- Startup.MinimumLogLevel = GetLogLevelFromCommandLineArgs(args, out Startup.IsLogLevelOverriddenByCli);
- LogLevelProvider.SetInitialLogLevel(Startup.MinimumLogLevel, Startup.IsLogLevelOverriddenByCli);
+ // LogLevelProvider was already initialized in StartEngine before CreateHostBuilder.
+ // Use the already-set values to avoid re-parsing args.
+ Startup.MinimumLogLevel = LogLevelProvider.CurrentLogLevel;
+ Startup.IsLogLevelOverriddenByCli = LogLevelProvider.IsCliOverridden;
ILoggerFactory loggerFactory = GetLoggerFactoryForLogLevel(Startup.MinimumLogLevel, stdio: runMcpStdio);
ILogger startupLogger = loggerFactory.CreateLogger();
DisableHttpsRedirectionIfNeeded(args);
@@ -133,19 +163,59 @@ public static IHostBuilder CreateHostBuilder(string[] args, bool runMcpStdio, st
/// Using System.CommandLine Parser to parse args and return
/// the correct log level. We save if there is a log level in args through
/// the out param. For log level out of range we throw an exception.
+ /// When in MCP stdio mode without explicit --LogLevel, defaults to None without CLI override.
///
/// array that may contain log level information.
- /// sets if log level is found in the args.
+ /// whether running in MCP stdio mode.
+ /// sets if log level is found in the args from CLI.
+ /// sets if log level came from config file.
/// Appropriate log level.
- private static LogLevel GetLogLevelFromCommandLineArgs(string[] args, out bool isLogLevelOverridenByCli)
+ private static LogLevel GetLogLevelFromCommandLineArgs(string[] args, bool runMcpStdio, out bool isLogLevelOverridenByCli, out bool isLogLevelOverridenByConfig)
{
Command cmd = new(name: "start");
Option logLevelOption = new(name: "--LogLevel");
+ Option logLevelFromConfigOption = new(name: "--LogLevelFromConfig");
cmd.AddOption(logLevelOption);
+ cmd.AddOption(logLevelFromConfigOption);
ParseResult result = GetParseResult(cmd, args);
bool matchedToken = result.Tokens.Count - result.UnmatchedTokens.Count - result.UnparsedTokens.Count > 1;
- LogLevel logLevel = matchedToken ? result.GetValueForOption(logLevelOption) : LogLevel.Error;
- isLogLevelOverridenByCli = matchedToken;
+
+ // Check if --LogLevelFromConfig flag is present (indicates config override, not CLI)
+ bool isFromConfig = result.GetValueForOption(logLevelFromConfigOption);
+
+ LogLevel logLevel;
+ if (matchedToken)
+ {
+ logLevel = result.GetValueForOption(logLevelOption);
+
+ if (isFromConfig)
+ {
+ // Log level came from config file (passed by CLI with --LogLevelFromConfig marker)
+ isLogLevelOverridenByCli = false;
+ isLogLevelOverridenByConfig = true;
+ }
+ else
+ {
+ // User explicitly set --LogLevel via CLI (highest priority)
+ isLogLevelOverridenByCli = true;
+ isLogLevelOverridenByConfig = false;
+ }
+ }
+ else if (runMcpStdio)
+ {
+ // MCP stdio mode without explicit --LogLevel: default to None to keep stdout clean.
+ // This is NOT a CLI or config override, so MCP logging/setLevel can still change it.
+ logLevel = LogLevel.None;
+ isLogLevelOverridenByCli = false;
+ isLogLevelOverridenByConfig = false;
+ }
+ else
+ {
+ // Normal mode without explicit --LogLevel
+ logLevel = LogLevel.Error;
+ isLogLevelOverridenByCli = false;
+ isLogLevelOverridenByConfig = false;
+ }
if (logLevel is > LogLevel.None or < LogLevel.Trace)
{