-
Notifications
You must be signed in to change notification settings - Fork 332
Implemented MCP Set Log Level #3419
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ | |
| using Azure.DataApiBuilder.Config.ObjectModel; | ||
| using Azure.DataApiBuilder.Core.AuthenticationHelpers.AuthenticationSimulator; | ||
| using Azure.DataApiBuilder.Core.Configurations; | ||
| using Azure.DataApiBuilder.Core.Telemetry; | ||
| using Azure.DataApiBuilder.Mcp.Model; | ||
| using Azure.DataApiBuilder.Mcp.Utils; | ||
| using Microsoft.AspNetCore.Http; | ||
|
|
@@ -131,6 +132,10 @@ public async Task RunAsync(CancellationToken cancellationToken) | |
| WriteResult(id, new { ok = true }); | ||
| break; | ||
|
|
||
| case "logging/setLevel": | ||
| HandleSetLogLevel(id, root); | ||
| break; | ||
|
|
||
| case "shutdown": | ||
| WriteResult(id, new { ok = true }); | ||
| return; | ||
|
|
@@ -228,6 +233,71 @@ private void HandleListTools(JsonElement? id) | |
| WriteResult(id, new { tools = toolsWire }); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Handles the "logging/setLevel" JSON-RPC method by updating the runtime log level. | ||
| /// </summary> | ||
| /// <param name="id">The request identifier extracted from the incoming JSON-RPC request.</param> | ||
| /// <param name="root">The root JSON element of the incoming JSON-RPC request.</param> | ||
| /// <remarks> | ||
| /// 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 | ||
| /// | ||
| /// If CLI or Config set the log level, this method accepts the request but silently ignores it. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| /// The client won't get an error, but CLI/Config wins. | ||
| /// </remarks> | ||
| private void HandleSetLogLevel(JsonElement? id, JsonElement root) | ||
| { | ||
| // Extract the level parameter from the request | ||
| string? level = null; | ||
| if (root.TryGetProperty("params", out JsonElement paramsEl) && | ||
| paramsEl.TryGetProperty("level", out JsonElement levelEl) && | ||
| levelEl.ValueKind == JsonValueKind.String) | ||
| { | ||
| level = levelEl.GetString(); | ||
| } | ||
|
|
||
| if (string.IsNullOrWhiteSpace(level)) | ||
| { | ||
| WriteError(id, McpStdioJsonRpcErrorCodes.INVALID_PARAMS, "Missing or invalid 'level' parameter"); | ||
| return; | ||
| } | ||
|
|
||
| // Get the ILogLevelController from service provider | ||
| ILogLevelController? logLevelController = _serviceProvider.GetService<ILogLevelController>(); | ||
| 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."); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just saw Ani's comment below, will this be removed as well? |
||
| WriteResult(id, new { }); | ||
| return; | ||
| } | ||
|
|
||
| // Attempt to update the log level | ||
| // If CLI or Config overrode, this returns false but we still return success to the client | ||
| bool changed = logLevelController.UpdateFromMcp(level); | ||
|
anushakolan marked this conversation as resolved.
|
||
| if (changed) | ||
| { | ||
| Console.Error.WriteLine($"[MCP DEBUG] Log level changed to: {level}"); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Where are these debug messages output? Will those confuse the agent?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we really need these console messages?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No removed them
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| } | ||
| else if (logLevelController.IsCliOverridden) | ||
| { | ||
| Console.Error.WriteLine($"[MCP DEBUG] Log level not changed (CLI override active), requested: {level}"); | ||
| } | ||
| else if (logLevelController.IsConfigOverridden) | ||
| { | ||
| Console.Error.WriteLine($"[MCP DEBUG] Log level not changed (Config override active), requested: {level}"); | ||
| } | ||
| else | ||
| { | ||
| Console.Error.WriteLine($"[MCP DEBUG] Log level not changed, invalid level: {level}"); | ||
| } | ||
|
|
||
| // Always return success (empty result object) per MCP spec | ||
| WriteResult(id, new { }); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Handles the "tools/call" JSON-RPC method by executing the specified tool with the provided arguments. | ||
| /// </summary> | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -2582,7 +2582,8 @@ public static bool TryStartEngineWithOptions(StartOptions options, FileSystemRun | |||||
| List<string> args = new() | ||||||
| { "--ConfigFileName", runtimeConfigFile }; | ||||||
|
|
||||||
| /// Add arguments for LogLevel. Checks if LogLevel is overridden with option `--LogLevel`. | ||||||
| /// Add arguments for LogLevel. Only pass --LogLevel when user explicitly specified it, | ||||||
| /// so that MCP logging/setLevel can still adjust the level when no CLI override is present. | ||||||
| /// If not provided, Default minimum LogLevel is Debug for Development mode and Error for Production mode. | ||||||
| LogLevel minimumLogLevel; | ||||||
| if (options.LogLevel is not null) | ||||||
|
|
@@ -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"); | ||||||
|
Comment on lines
+2602
to
+2604
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||||||
| 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); | ||||||
|
||||||
| _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
AI
Apr 8, 2026
There was a problem hiding this comment.
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).
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| namespace Azure.DataApiBuilder.Core.Telemetry | ||
| { | ||
| /// <summary> | ||
| /// Interface for controlling log levels dynamically at runtime. | ||
| /// This allows MCP and other components to adjust logging without | ||
| /// direct coupling to the concrete implementation. | ||
| /// </summary> | ||
| public interface ILogLevelController | ||
| { | ||
| /// <summary> | ||
| /// Gets a value indicating whether the log level was overridden by CLI arguments. | ||
| /// When true, MCP and config-based log level changes are ignored. | ||
| /// </summary> | ||
| bool IsCliOverridden { get; } | ||
|
|
||
| /// <summary> | ||
| /// Gets a value indicating whether the log level was explicitly set in the config file. | ||
| /// When true along with IsCliOverridden being false, MCP log level changes are ignored. | ||
| /// </summary> | ||
| bool IsConfigOverridden { get; } | ||
|
|
||
| /// <summary> | ||
| /// Updates the log level from an MCP logging/setLevel request. | ||
| /// The MCP level string is mapped to the appropriate LogLevel. | ||
| /// Log level precedence (highest to lowest): | ||
| /// 1. CLI --LogLevel flag (IsCliOverridden = true) | ||
| /// 2. Config runtime.telemetry.log-level (IsConfigOverridden = true) | ||
| /// 3. MCP logging/setLevel (only works if neither CLI nor Config set a level) | ||
| /// </summary> | ||
| /// <param name="mcpLevel">The MCP log level string (e.g., "debug", "info", "warning", "error").</param> | ||
| /// <returns>True if the level was changed; false if CLI or Config override prevented the change.</returns> | ||
| bool UpdateFromMcp(string mcpLevel); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| #nullable enable | ||
|
|
||
| using Azure.DataApiBuilder.Service.Telemetry; | ||
| using Microsoft.Extensions.Logging; | ||
| using Microsoft.VisualStudio.TestTools.UnitTesting; | ||
|
|
||
| namespace Azure.DataApiBuilder.Service.Tests.UnitTests | ||
| { | ||
| /// <summary> | ||
| /// Unit tests for the DynamicLogLevelProvider class. | ||
| /// Tests the MCP logging/setLevel support. | ||
| /// </summary> | ||
| [TestClass] | ||
| public class DynamicLogLevelProviderTests | ||
| { | ||
| [TestMethod] | ||
| public void UpdateFromMcp_ValidLevel_ChangesLogLevel() | ||
| { | ||
| // Arrange | ||
| DynamicLogLevelProvider provider = new(); | ||
| provider.SetInitialLogLevel(LogLevel.Error, isCliOverridden: false); | ||
|
|
||
| // Act | ||
| bool result = provider.UpdateFromMcp("debug"); | ||
|
|
||
| // Assert | ||
| Assert.IsTrue(result); | ||
| Assert.AreEqual(LogLevel.Debug, provider.CurrentLogLevel); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void UpdateFromMcp_CliOverridden_DoesNotChangeLogLevel() | ||
| { | ||
| // Arrange | ||
| DynamicLogLevelProvider provider = new(); | ||
| provider.SetInitialLogLevel(LogLevel.Error, isCliOverridden: true); | ||
|
|
||
| // Act | ||
| bool result = provider.UpdateFromMcp("debug"); | ||
|
|
||
| // Assert | ||
| Assert.IsFalse(result); | ||
| Assert.AreEqual(LogLevel.Error, provider.CurrentLogLevel); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void UpdateFromMcp_ConfigOverridden_DoesNotChangeLogLevel() | ||
| { | ||
| // Arrange | ||
| DynamicLogLevelProvider provider = new(); | ||
| provider.SetInitialLogLevel(LogLevel.Warning, isCliOverridden: false, isConfigOverridden: true); | ||
|
|
||
| // Act | ||
| bool result = provider.UpdateFromMcp("debug"); | ||
|
|
||
| // Assert | ||
| Assert.IsFalse(result); | ||
| Assert.AreEqual(LogLevel.Warning, provider.CurrentLogLevel); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void UpdateFromMcp_InvalidLevel_ReturnsFalse() | ||
| { | ||
| // Arrange | ||
| DynamicLogLevelProvider provider = new(); | ||
| provider.SetInitialLogLevel(LogLevel.Error, isCliOverridden: false); | ||
|
|
||
| // Act | ||
| bool result = provider.UpdateFromMcp("invalid"); | ||
|
|
||
| // Assert | ||
| Assert.IsFalse(result); | ||
| Assert.AreEqual(LogLevel.Error, provider.CurrentLogLevel); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void ShouldLog_ReturnsCorrectResult() | ||
| { | ||
| // Arrange | ||
| DynamicLogLevelProvider provider = new(); | ||
| provider.SetInitialLogLevel(LogLevel.Warning, isCliOverridden: false); | ||
|
|
||
| // Assert - logs at or above Warning should pass | ||
| Assert.IsTrue(provider.ShouldLog(LogLevel.Warning)); | ||
| Assert.IsTrue(provider.ShouldLog(LogLevel.Error)); | ||
| Assert.IsFalse(provider.ShouldLog(LogLevel.Debug)); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,7 @@ | |
| using System.Text.RegularExpressions; | ||
| using System.Threading.Tasks; | ||
| using Azure.DataApiBuilder.Config; | ||
| using Azure.DataApiBuilder.Core.Telemetry; | ||
| using Azure.DataApiBuilder.Service.Exceptions; | ||
| using Azure.DataApiBuilder.Service.Telemetry; | ||
| using Azure.DataApiBuilder.Service.Utilities; | ||
|
|
@@ -110,6 +111,7 @@ public static IHostBuilder CreateHostBuilder(string[] args, bool runMcpStdio, st | |
| .ConfigureServices((context, services) => | ||
| { | ||
| services.AddSingleton(LogLevelProvider); | ||
| services.AddSingleton<ILogLevelController>(LogLevelProvider); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why wouldn't we want only 1 signleton? |
||
| }) | ||
| .ConfigureLogging(logging => | ||
| { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,17 +1,43 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using Azure.DataApiBuilder.Config.ObjectModel; | ||
| using Azure.DataApiBuilder.Core.Telemetry; | ||
| using Microsoft.Extensions.Logging; | ||
|
|
||
| namespace Azure.DataApiBuilder.Service.Telemetry | ||
| { | ||
| public class DynamicLogLevelProvider | ||
| /// <summary> | ||
| /// Provides dynamic log level control with support for CLI override, runtime config, and MCP. | ||
| /// </summary> | ||
| public class DynamicLogLevelProvider : ILogLevelController | ||
| { | ||
| /// <summary> | ||
| /// Maps MCP log level strings to Microsoft.Extensions.Logging.LogLevel. | ||
| /// MCP levels: debug, info, notice, warning, error, critical, alert, emergency. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does MCP not have a |
||
| /// </summary> | ||
| private static readonly Dictionary<string, LogLevel> _mcpLevelMapping = new(StringComparer.OrdinalIgnoreCase) | ||
| { | ||
| ["debug"] = LogLevel.Debug, | ||
| ["info"] = LogLevel.Information, | ||
| ["notice"] = LogLevel.Information, // MCP "notice" maps to Information (no direct equivalent) | ||
| ["warning"] = LogLevel.Warning, | ||
| ["error"] = LogLevel.Error, | ||
| ["critical"] = LogLevel.Critical, | ||
| ["alert"] = LogLevel.Critical, // MCP "alert" maps to Critical | ||
| ["emergency"] = LogLevel.Critical // MCP "emergency" maps to Critical | ||
| }; | ||
|
|
||
| public LogLevel CurrentLogLevel { get; private set; } | ||
|
|
||
| public bool IsCliOverridden { get; private set; } | ||
|
|
||
| public void SetInitialLogLevel(LogLevel logLevel = LogLevel.Error, bool isCliOverridden = false) | ||
| public bool IsConfigOverridden { get; private set; } | ||
|
|
||
| public void SetInitialLogLevel(LogLevel logLevel = LogLevel.Error, bool isCliOverridden = false, bool isConfigOverridden = false) | ||
| { | ||
| CurrentLogLevel = logLevel; | ||
| IsCliOverridden = isCliOverridden; | ||
| IsConfigOverridden = isConfigOverridden; | ||
| } | ||
|
|
||
| public void UpdateFromRuntimeConfig(RuntimeConfig runtimeConfig) | ||
|
|
@@ -20,7 +46,52 @@ public void UpdateFromRuntimeConfig(RuntimeConfig runtimeConfig) | |
| if (!IsCliOverridden) | ||
| { | ||
| CurrentLogLevel = runtimeConfig.GetConfiguredLogLevel(); | ||
|
|
||
| // Track if config explicitly set a log level (not just using defaults) | ||
| IsConfigOverridden = !runtimeConfig.IsLogLevelNull(); | ||
| } | ||
|
Comment on lines
46
to
+52
|
||
| } | ||
|
|
||
| /// <summary> | ||
| /// Updates the log level from an MCP logging/setLevel request. | ||
| /// Precedence (highest to lowest): | ||
| /// 1. CLI --LogLevel flag (IsCliOverridden = true) | ||
| /// 2. Config runtime.telemetry.log-level (IsConfigOverridden = true) | ||
| /// 3. MCP logging/setLevel | ||
| /// | ||
| /// If CLI or Config overrode, this method accepts the request silently but does not change the level. | ||
| /// </summary> | ||
| /// <param name="mcpLevel">The MCP log level string (e.g., "debug", "info", "warning", "error").</param> | ||
| /// <returns>True if the level was changed; false if CLI/Config override prevented the change or level was invalid.</returns> | ||
| public bool UpdateFromMcp(string mcpLevel) | ||
| { | ||
| // If CLI overrode the log level, accept the request but don't change anything. | ||
| // This prevents MCP clients from getting errors, but CLI wins. | ||
| if (IsCliOverridden) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| // If Config explicitly set the log level, accept the request but don't change anything. | ||
| // Config has second precedence after CLI. | ||
| if (IsConfigOverridden) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| if (string.IsNullOrWhiteSpace(mcpLevel)) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From the way that you use this function in the |
||
| { | ||
| return false; | ||
| } | ||
|
|
||
| if (_mcpLevelMapping.TryGetValue(mcpLevel, out LogLevel logLevel)) | ||
| { | ||
| CurrentLogLevel = logLevel; | ||
| return true; | ||
| } | ||
|
|
||
| // Unknown level - don't change, but don't fail either | ||
| return false; | ||
| } | ||
|
|
||
| public bool ShouldLog(LogLevel logLevel) | ||
|
|
||
There was a problem hiding this comment.
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/Developmentmode