diff --git a/src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs b/src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs
index 1ab1c73d05..c6300a09cc 100644
--- a/src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs
+++ b/src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs
@@ -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 });
}
+ ///
+ /// Handles the "logging/setLevel" JSON-RPC method by updating the runtime log level.
+ ///
+ /// The request identifier extracted from the incoming JSON-RPC request.
+ /// The root JSON element of the incoming JSON-RPC request.
+ ///
+ /// 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.
+ ///
+ 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();
+ 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}");
+ }
+ 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 { });
+ }
+
///
/// Handles the "tools/call" JSON-RPC method by executing the specified tool with the provided arguments.
///
diff --git a/src/Cli/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs
index c89da760e4..497d387e72 100644
--- a/src/Cli/ConfigGenerator.cs
+++ b/src/Cli/ConfigGenerator.cs
@@ -2582,7 +2582,8 @@ public static bool TryStartEngineWithOptions(StartOptions options, FileSystemRun
List 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,6 +2598,11 @@ 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
{
@@ -2604,10 +2610,10 @@ public static bool TryStartEngineWithOptions(StartOptions options, FileSystemRun
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.
+ }
// This will add args to disable automatic redirects to https if specified by user
if (options.IsHttpsRedirectionDisabled)
diff --git a/src/Core/Telemetry/ILogLevelController.cs b/src/Core/Telemetry/ILogLevelController.cs
new file mode 100644
index 0000000000..f67424b155
--- /dev/null
+++ b/src/Core/Telemetry/ILogLevelController.cs
@@ -0,0 +1,37 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace Azure.DataApiBuilder.Core.Telemetry
+{
+ ///
+ /// Interface for controlling log levels dynamically at runtime.
+ /// This allows MCP and other components to adjust logging without
+ /// direct coupling to the concrete implementation.
+ ///
+ public interface ILogLevelController
+ {
+ ///
+ /// Gets a value indicating whether the log level was overridden by CLI arguments.
+ /// When true, MCP and config-based log level changes are ignored.
+ ///
+ bool IsCliOverridden { get; }
+
+ ///
+ /// 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.
+ ///
+ bool IsConfigOverridden { get; }
+
+ ///
+ /// 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)
+ ///
+ /// The MCP log level string (e.g., "debug", "info", "warning", "error").
+ /// True if the level was changed; false if CLI or Config override prevented the change.
+ bool UpdateFromMcp(string mcpLevel);
+ }
+}
diff --git a/src/Service.Tests/UnitTests/DynamicLogLevelProviderTests.cs b/src/Service.Tests/UnitTests/DynamicLogLevelProviderTests.cs
new file mode 100644
index 0000000000..3071b7265d
--- /dev/null
+++ b/src/Service.Tests/UnitTests/DynamicLogLevelProviderTests.cs
@@ -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
+{
+ ///
+ /// Unit tests for the DynamicLogLevelProvider class.
+ /// Tests the MCP logging/setLevel support.
+ ///
+ [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));
+ }
+ }
+}
diff --git a/src/Service/Program.cs b/src/Service/Program.cs
index d601f4f6b4..61829238b6 100644
--- a/src/Service/Program.cs
+++ b/src/Service/Program.cs
@@ -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(LogLevelProvider);
})
.ConfigureLogging(logging =>
{
diff --git a/src/Service/Telemetry/DynamicLogLevelProvider.cs b/src/Service/Telemetry/DynamicLogLevelProvider.cs
index 3c35e295e6..c1b4e4b851 100644
--- a/src/Service/Telemetry/DynamicLogLevelProvider.cs
+++ b/src/Service/Telemetry/DynamicLogLevelProvider.cs
@@ -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
+ ///
+ /// Provides dynamic log level control with support for CLI override, runtime config, and MCP.
+ ///
+ public class DynamicLogLevelProvider : ILogLevelController
{
+ ///
+ /// Maps MCP log level strings to Microsoft.Extensions.Logging.LogLevel.
+ /// MCP levels: debug, info, notice, warning, error, critical, alert, emergency.
+ ///
+ private static readonly Dictionary _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();
+ }
+ }
+
+ ///
+ /// 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.
+ ///
+ /// The MCP log level string (e.g., "debug", "info", "warning", "error").
+ /// True if the level was changed; false if CLI/Config override prevented the change or level was invalid.
+ 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)