Skip to content

Implemented MCP Set Log Level#3419

Open
anushakolan wants to merge 2 commits intomainfrom
dev/anushakolan/set-log-level
Open

Implemented MCP Set Log Level#3419
anushakolan wants to merge 2 commits intomainfrom
dev/anushakolan/set-log-level

Conversation

@anushakolan
Copy link
Copy Markdown
Contributor

@anushakolan anushakolan commented Apr 8, 2026

PR Description

Note: This PR includes changes from PR #3420 which has been merged into this branch.

Why make this change?

Closes #3274 - MCP Server returns "Method not found: logging/setLevel" error when clients send the standard MCP logging/setLevel request.

Closes #3275 - Control output in MCP stdio mode (default to LogLevel.None, redirect/suppress console output).

What is this change?

MCP logging/setLevel Handler

  • Added handler for logging/setLevel JSON-RPC method in McpStdioServer.cs
  • Implemented DynamicLogLevelProvider with ILogLevelController interface to allow MCP to update log levels dynamically
  • Added IsCliOverridden and IsConfigOverridden properties to enforce precedence rules

Log Level Precedence System

Precedence (highest to lowest):

  1. CLI --LogLevel flag - cannot be changed by MCP
  2. Config runtime.telemetry.log-level - cannot be changed by MCP
  3. MCP logging/setLevel - only works if neither CLI nor config set a level

If CLI or config set a level, MCP requests are accepted but silently ignored (no error returned per MCP spec).

Early Config Reading for MCP Mode

  • Added TryGetLogLevelFromConfig() in Program.cs to read config file early (before host build)
  • This ensures config log level is detected before Console redirect decision
  • Console redirect for MCP stdio mode now respects config log level

CLI Log Level Handling

  • Added Utils.CliLogLevel property to track the parsed --LogLevel value
  • CLI's CustomLoggerProvider now respects the --LogLevel value for its own logging

Config Helpers

  • Added HasExplicitLogLevel() helper to RuntimeConfig to correctly detect when config actually pins a log level
  • This properly handles null values in telemetry section (null values don't count as explicit override)

How was this tested?

  • Unit Tests (DynamicLogLevelProviderTests - 5 tests)
  • Manual Testing

Manual Test 1: No override (MCP can change level)

  1. Start MCP server without --LogLevel and without config log-level
  2. MCP sends logging/setLevel with level: info
  3. Result: Log level changes to info

Manual Test 2: CLI override (MCP blocked)

  1. Start MCP server with --LogLevel Warning
  2. MCP sends logging/setLevel with level: info
  3. Result: Log level stays at Warning, MCP request accepted silently

Manual Test 3: Config override (MCP blocked)

  1. Add "telemetry": { "log-level": { "default": "Warning" } } to config
  2. Start MCP server without --LogLevel
  3. MCP sends logging/setLevel with level: info
  4. Result: Log level stays at Warning, MCP request accepted silently

Manual Test 4: Config with null values (MCP can change level)

  1. Add "telemetry": { "log-level": { "default": null } } to config
  2. Start MCP server without --LogLevel
  3. MCP sends logging/setLevel with level: info
  4. Result: Log level changes to info (null values don't count as override)

Manual Test 5: CLI --LogLevel Trace shows verbose logs

  1. Start MCP server with --LogLevel Trace
  2. Result: Trace/Debug level logs visible in stderr

Manual Test 6: CLI --LogLevel None suppresses all output

  1. Start MCP server with --LogLevel None
  2. Result: Zero output to stderr, stdout clean for JSON-RPC

Manual Test 7: Config log level respected at startup

  1. Add "telemetry": { "log-level": { "default": "Error" } } to config
  2. Start MCP server without --LogLevel
  3. Result: Only Error/Critical logs shown (no Info/Debug startup noise)

Sample Request(s)

MCP client sends:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "logging/setLevel",
  "params": {
    "level": "info"
  }
}

Server responds with empty result (success per MCP spec) and updates log level if no CLI/config override is active.

@anushakolan
Copy link
Copy Markdown
Contributor Author

/azp run

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses MCP stdio clients receiving Method not found: logging/setLevel by adding support for the logging/setLevel JSON-RPC method and wiring it to DAB’s dynamic log-level infrastructure with explicit precedence rules (CLI > config > MCP).

Changes:

  • Added logging/setLevel dispatch + handler in McpStdioServer to accept the standard MCP request and (when allowed) update runtime log level.
  • Introduced ILogLevelController and implemented it in DynamicLogLevelProvider, including CLI/config precedence tracking.
  • Updated CLI engine-launch argument construction to only pass --LogLevel when explicitly provided, plus added unit tests for MCP log-level updates.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/Service/Telemetry/DynamicLogLevelProvider.cs Implements ILogLevelController, adds MCP level mapping and precedence handling (CLI/config/MCP).
src/Service/Program.cs Registers ILogLevelController in DI so MCP can resolve the controller.
src/Service.Tests/UnitTests/DynamicLogLevelProviderTests.cs Adds unit tests for MCP-driven log-level changes and override behavior.
src/Core/Telemetry/ILogLevelController.cs New interface to decouple MCP from the concrete log-level provider.
src/Cli/ConfigGenerator.cs Only forwards --LogLevel when explicitly set by the user (to avoid treating defaults as CLI overrides).
src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs Adds logging/setLevel handler that calls into ILogLevelController.

Comment on lines 46 to +52
if (!IsCliOverridden)
{
CurrentLogLevel = runtimeConfig.GetConfiguredLogLevel();

// Track if config explicitly set a log level (not just using defaults)
IsConfigOverridden = !runtimeConfig.IsLogLevelNull();
}
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IsConfigOverridden is derived from !runtimeConfig.IsLogLevelNull(), but the schema allows runtime.telemetry.log-level values to be null (meaning “use host-mode defaults”). In that case IsLogLevelNull() returns false (because the dictionary exists), causing MCP logging/setLevel to be blocked even though config did not actually pin a log level. Consider treating config as “overridden” only when at least one configured log-level value is non-null (e.g., any entry value != null), or add a dedicated RuntimeConfig helper for this distinction.

Copilot uses AI. Check for mistakes.
minimumLogLevel = deserializedRuntimeConfig.GetConfiguredLogLevel();
HostMode hostModeType = deserializedRuntimeConfig.IsDevelopmentMode() ? HostMode.Development : HostMode.Production;

_logger.LogInformation($"Setting default minimum LogLevel: {minimumLogLevel} for {hostModeType} mode.", minimumLogLevel, hostModeType);
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the default log-level branch, _logger.LogInformation($"Setting default minimum LogLevel: {minimumLogLevel} for {hostModeType} mode.", minimumLogLevel, hostModeType); uses an interpolated string while also passing structured args—those args won’t be captured as structured fields. Use a message template without $"..." (or remove the extra args) so logging behaves as intended.

Suggested change
_logger.LogInformation($"Setting default minimum LogLevel: {minimumLogLevel} for {hostModeType} mode.", minimumLogLevel, hostModeType);
_logger.LogInformation("Setting default minimum LogLevel: {minimumLogLevel} for {hostModeType} mode.", minimumLogLevel, hostModeType);

Copilot uses AI. Check for mistakes.
Comment on lines 2585 to +2616
@@ -2597,17 +2598,22 @@ public static bool TryStartEngineWithOptions(StartOptions options, FileSystemRun

minimumLogLevel = (LogLevel)options.LogLevel;
_logger.LogInformation("Setting minimum LogLevel: {minimumLogLevel}.", minimumLogLevel);

// Only add --LogLevel 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(minimumLogLevel.ToString());
}
else
{
minimumLogLevel = deserializedRuntimeConfig.GetConfiguredLogLevel();
HostMode hostModeType = deserializedRuntimeConfig.IsDevelopmentMode() ? HostMode.Development : HostMode.Production;

_logger.LogInformation($"Setting default minimum LogLevel: {minimumLogLevel} for {hostModeType} mode.", minimumLogLevel, hostModeType);
}

args.Add("--LogLevel");
args.Add(minimumLogLevel.ToString());
// Don't add --LogLevel arg since user didn't explicitly set it.
// Service will determine default log level based on config or host mode.
}
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After this change, when options.LogLevel is not provided the CLI no longer passes --LogLevel to the engine. The Service defaults to LogLevel.Error when --LogLevel is absent (Program.GetLogLevelFromCommandLineArgs), so early startup logs will be suppressed even in Development until DynamicLogLevelProvider.UpdateFromRuntimeConfig(...) runs. If the intent is to keep the existing “Debug in Development / Error in Production” default behavior while still allowing MCP to change the level, consider setting the initial log level from the loaded config earlier in host construction (or introducing a non-override mechanism distinct from the CLI override flag).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator

@Aniruddh25 Aniruddh25 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dont understand how the log level set by MCP tool call takes into effect. Can you show a working demo?

bool changed = logLevelController.UpdateFromMcp(level);
if (changed)
{
Console.Error.WriteLine($"[MCP DEBUG] Log level changed to: {level}");
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where are these debug messages output? Will those confuse the agent?
Also, what if the LogLevel set by CLI is None. Do these debug messages still be shown? If not, how do you prevent them if you send them to Console.Error.WriteLine?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need these console messages?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No removed them

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The resolution comment says - removed them, but I dont see the change pushed to remote yet. Reopening this comment so that this is not forgotten.

Please resolve a comment only "after" pushing to remote.

Comment thread src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs
Copy link
Copy Markdown
Collaborator

@Aniruddh25 Aniruddh25 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Manual Test 2: CLI override (MCP blocked)
Start MCP server with --LogLevel None
MCP sends logging/setLevel with level: info
Result: Log level stays at None, debug message shows "Log level not changed (CLI override active)"

As far as I understand, there should be no debug message altogether given that LogLevel has been defined as None through CLI.
So, why does the description above say Result shows "Log level not changed"?

@anushakolan
Copy link
Copy Markdown
Contributor Author

Manual Test 2: CLI override (MCP blocked)
Start MCP server with --LogLevel None
MCP sends logging/setLevel with level: info
Result: Log level stays at None, debug message shows "Log level not changed (CLI override active)"

As far as I understand, there should be no debug message altogether given that LogLevel has been defined as None through CLI. So, why does the description above say Result shows "Log level not changed"?

Because it was writing directly to stderr. Those [[MCP DEBUG]] messages would still appear because they bypassed ILoggerfiltering and wrote directly to stderr. That's why we removed them.

@Aniruddh25
Copy link
Copy Markdown
Collaborator

Note: This PR includes changes from #3420 which has been merged into this branch.

PR #3420 has not been merged into this branch, has it?

/// Log level precedence (highest to lowest):
/// 1. CLI --LogLevel flag - cannot be overridden
/// 2. Config runtime.telemetry.log-level - cannot be overridden by MCP
/// 3. MCP logging/setLevel - only works if neither CLI nor Config explicitly set a level
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I would also add as a 4th log level precedence that the log level is set depending on whether DAB is started in Production/Development mode

/// 2. Config runtime.telemetry.log-level - cannot be overridden by MCP
/// 3. MCP logging/setLevel - only works if neither CLI nor Config explicitly set a level
///
/// If CLI or Config set the log level, this method accepts the request but silently ignores it.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be better to throw a warning that the request was ignored? I think it would be more beneficial to the user.

if (logLevelController is null)
{
// Log level controller not available - still accept request per MCP spec
Console.Error.WriteLine("[MCP DEBUG] ILogLevelController not available, logging/setLevel ignored.");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just confirming, this is something that will need to be changed so it is buffered in my PR right?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just saw Ani's comment below, will this be removed as well?

Comment on lines +2602 to +2604
// Only add --LogLevel when user explicitly specified it via CLI.
// This allows MCP logging/setLevel to work when no CLI override is present.
args.Add("--LogLevel");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that some of the changes in the PR #3420 are included here, I think it would be better to have it only in one, so it is not confusing.

Comment thread src/Service/Program.cs
.ConfigureServices((context, services) =>
{
services.AddSingleton(LogLevelProvider);
services.AddSingleton<ILogLevelController>(LogLevelProvider);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why wouldn't we want only 1 signleton?

{
/// <summary>
/// Maps MCP log level strings to Microsoft.Extensions.Logging.LogLevel.
/// MCP levels: debug, info, notice, warning, error, critical, alert, emergency.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does MCP not have a none log level?

return false;
}

if (string.IsNullOrWhiteSpace(mcpLevel))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the way that you use this function in the McpStdioServer.cs, the mcpLevel shouldn't even have the possibility of being empty or null. I think it would be better to throw an error for this case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: When -mcp-stdio is used, control the output [Bug]: when using --mcp-stdio failure with "logging/setlevel"

5 participants