-
Notifications
You must be signed in to change notification settings - Fork 329
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. | ||
| /// 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."); | ||
| 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); | ||
| 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? |
||
| } | ||
| 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"); | ||||||
| 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 |
|---|---|---|
| @@ -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. | ||
| /// </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)) | ||
| { | ||
| 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.
Even though the level is modified using the level controller, how does it take into effect in STDIO mode? HandleToolCallAsync seems to be writing to the console using WriteError function regardless of the logging level defined by the configuration/CLI/even the logging/setLevel call.