Skip to content

Commit deb9866

Browse files
committed
Implemented MCP Set Log Level
1 parent fbe03e5 commit deb9866

6 files changed

Lines changed: 284 additions & 6 deletions

File tree

src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using Azure.DataApiBuilder.Config.ObjectModel;
77
using Azure.DataApiBuilder.Core.AuthenticationHelpers.AuthenticationSimulator;
88
using Azure.DataApiBuilder.Core.Configurations;
9+
using Azure.DataApiBuilder.Core.Telemetry;
910
using Azure.DataApiBuilder.Mcp.Model;
1011
using Azure.DataApiBuilder.Mcp.Utils;
1112
using Microsoft.AspNetCore.Http;
@@ -131,6 +132,10 @@ public async Task RunAsync(CancellationToken cancellationToken)
131132
WriteResult(id, new { ok = true });
132133
break;
133134

135+
case "logging/setLevel":
136+
HandleSetLogLevel(id, root);
137+
break;
138+
134139
case "shutdown":
135140
WriteResult(id, new { ok = true });
136141
return;
@@ -228,6 +233,71 @@ private void HandleListTools(JsonElement? id)
228233
WriteResult(id, new { tools = toolsWire });
229234
}
230235

236+
/// <summary>
237+
/// Handles the "logging/setLevel" JSON-RPC method by updating the runtime log level.
238+
/// </summary>
239+
/// <param name="id">The request identifier extracted from the incoming JSON-RPC request.</param>
240+
/// <param name="root">The root JSON element of the incoming JSON-RPC request.</param>
241+
/// <remarks>
242+
/// Log level precedence (highest to lowest):
243+
/// 1. CLI --LogLevel flag - cannot be overridden
244+
/// 2. Config runtime.telemetry.log-level - cannot be overridden by MCP
245+
/// 3. MCP logging/setLevel - only works if neither CLI nor Config explicitly set a level
246+
///
247+
/// If CLI or Config set the log level, this method accepts the request but silently ignores it.
248+
/// The client won't get an error, but CLI/Config wins.
249+
/// </remarks>
250+
private void HandleSetLogLevel(JsonElement? id, JsonElement root)
251+
{
252+
// Extract the level parameter from the request
253+
string? level = null;
254+
if (root.TryGetProperty("params", out JsonElement paramsEl) &&
255+
paramsEl.TryGetProperty("level", out JsonElement levelEl) &&
256+
levelEl.ValueKind == JsonValueKind.String)
257+
{
258+
level = levelEl.GetString();
259+
}
260+
261+
if (string.IsNullOrWhiteSpace(level))
262+
{
263+
WriteError(id, McpStdioJsonRpcErrorCodes.INVALID_PARAMS, "Missing or invalid 'level' parameter");
264+
return;
265+
}
266+
267+
// Get the ILogLevelController from service provider
268+
ILogLevelController? logLevelController = _serviceProvider.GetService<ILogLevelController>();
269+
if (logLevelController is null)
270+
{
271+
// Log level controller not available - still accept request per MCP spec
272+
Console.Error.WriteLine("[MCP DEBUG] ILogLevelController not available, logging/setLevel ignored.");
273+
WriteResult(id, new { });
274+
return;
275+
}
276+
277+
// Attempt to update the log level
278+
// If CLI or Config overrode, this returns false but we still return success to the client
279+
bool changed = logLevelController.UpdateFromMcp(level);
280+
if (changed)
281+
{
282+
Console.Error.WriteLine($"[MCP DEBUG] Log level changed to: {level}");
283+
}
284+
else if (logLevelController.IsCliOverridden)
285+
{
286+
Console.Error.WriteLine($"[MCP DEBUG] Log level not changed (CLI override active), requested: {level}");
287+
}
288+
else if (logLevelController.IsConfigOverridden)
289+
{
290+
Console.Error.WriteLine($"[MCP DEBUG] Log level not changed (Config override active), requested: {level}");
291+
}
292+
else
293+
{
294+
Console.Error.WriteLine($"[MCP DEBUG] Log level not changed, invalid level: {level}");
295+
}
296+
297+
// Always return success (empty result object) per MCP spec
298+
WriteResult(id, new { });
299+
}
300+
231301
/// <summary>
232302
/// Handles the "tools/call" JSON-RPC method by executing the specified tool with the provided arguments.
233303
/// </summary>

src/Cli/ConfigGenerator.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2582,7 +2582,8 @@ public static bool TryStartEngineWithOptions(StartOptions options, FileSystemRun
25822582
List<string> args = new()
25832583
{ "--ConfigFileName", runtimeConfigFile };
25842584

2585-
/// Add arguments for LogLevel. Checks if LogLevel is overridden with option `--LogLevel`.
2585+
/// Add arguments for LogLevel. Only pass --LogLevel when user explicitly specified it,
2586+
/// so that MCP logging/setLevel can still adjust the level when no CLI override is present.
25862587
/// If not provided, Default minimum LogLevel is Debug for Development mode and Error for Production mode.
25872588
LogLevel minimumLogLevel;
25882589
if (options.LogLevel is not null)
@@ -2597,17 +2598,22 @@ public static bool TryStartEngineWithOptions(StartOptions options, FileSystemRun
25972598

25982599
minimumLogLevel = (LogLevel)options.LogLevel;
25992600
_logger.LogInformation("Setting minimum LogLevel: {minimumLogLevel}.", minimumLogLevel);
2601+
2602+
// Only add --LogLevel when user explicitly specified it via CLI.
2603+
// This allows MCP logging/setLevel to work when no CLI override is present.
2604+
args.Add("--LogLevel");
2605+
args.Add(minimumLogLevel.ToString());
26002606
}
26012607
else
26022608
{
26032609
minimumLogLevel = deserializedRuntimeConfig.GetConfiguredLogLevel();
26042610
HostMode hostModeType = deserializedRuntimeConfig.IsDevelopmentMode() ? HostMode.Development : HostMode.Production;
26052611

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

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

26122618
// This will add args to disable automatic redirects to https if specified by user
26132619
if (options.IsHttpsRedirectionDisabled)
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace Azure.DataApiBuilder.Core.Telemetry
5+
{
6+
/// <summary>
7+
/// Interface for controlling log levels dynamically at runtime.
8+
/// This allows MCP and other components to adjust logging without
9+
/// direct coupling to the concrete implementation.
10+
/// </summary>
11+
public interface ILogLevelController
12+
{
13+
/// <summary>
14+
/// Gets a value indicating whether the log level was overridden by CLI arguments.
15+
/// When true, MCP and config-based log level changes are ignored.
16+
/// </summary>
17+
bool IsCliOverridden { get; }
18+
19+
/// <summary>
20+
/// Gets a value indicating whether the log level was explicitly set in the config file.
21+
/// When true along with IsCliOverridden being false, MCP log level changes are ignored.
22+
/// </summary>
23+
bool IsConfigOverridden { get; }
24+
25+
/// <summary>
26+
/// Updates the log level from an MCP logging/setLevel request.
27+
/// The MCP level string is mapped to the appropriate LogLevel.
28+
/// Log level precedence (highest to lowest):
29+
/// 1. CLI --LogLevel flag (IsCliOverridden = true)
30+
/// 2. Config runtime.telemetry.log-level (IsConfigOverridden = true)
31+
/// 3. MCP logging/setLevel (only works if neither CLI nor Config set a level)
32+
/// </summary>
33+
/// <param name="mcpLevel">The MCP log level string (e.g., "debug", "info", "warning", "error").</param>
34+
/// <returns>True if the level was changed; false if CLI or Config override prevented the change.</returns>
35+
bool UpdateFromMcp(string mcpLevel);
36+
}
37+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
#nullable enable
5+
6+
using Azure.DataApiBuilder.Service.Telemetry;
7+
using Microsoft.Extensions.Logging;
8+
using Microsoft.VisualStudio.TestTools.UnitTesting;
9+
10+
namespace Azure.DataApiBuilder.Service.Tests.UnitTests
11+
{
12+
/// <summary>
13+
/// Unit tests for the DynamicLogLevelProvider class.
14+
/// Tests the MCP logging/setLevel support.
15+
/// </summary>
16+
[TestClass]
17+
public class DynamicLogLevelProviderTests
18+
{
19+
[TestMethod]
20+
public void UpdateFromMcp_ValidLevel_ChangesLogLevel()
21+
{
22+
// Arrange
23+
DynamicLogLevelProvider provider = new();
24+
provider.SetInitialLogLevel(LogLevel.Error, isCliOverridden: false);
25+
26+
// Act
27+
bool result = provider.UpdateFromMcp("debug");
28+
29+
// Assert
30+
Assert.IsTrue(result);
31+
Assert.AreEqual(LogLevel.Debug, provider.CurrentLogLevel);
32+
}
33+
34+
[TestMethod]
35+
public void UpdateFromMcp_CliOverridden_DoesNotChangeLogLevel()
36+
{
37+
// Arrange
38+
DynamicLogLevelProvider provider = new();
39+
provider.SetInitialLogLevel(LogLevel.Error, isCliOverridden: true);
40+
41+
// Act
42+
bool result = provider.UpdateFromMcp("debug");
43+
44+
// Assert
45+
Assert.IsFalse(result);
46+
Assert.AreEqual(LogLevel.Error, provider.CurrentLogLevel);
47+
}
48+
49+
[TestMethod]
50+
public void UpdateFromMcp_ConfigOverridden_DoesNotChangeLogLevel()
51+
{
52+
// Arrange
53+
DynamicLogLevelProvider provider = new();
54+
provider.SetInitialLogLevel(LogLevel.Warning, isCliOverridden: false, isConfigOverridden: true);
55+
56+
// Act
57+
bool result = provider.UpdateFromMcp("debug");
58+
59+
// Assert
60+
Assert.IsFalse(result);
61+
Assert.AreEqual(LogLevel.Warning, provider.CurrentLogLevel);
62+
}
63+
64+
[TestMethod]
65+
public void UpdateFromMcp_InvalidLevel_ReturnsFalse()
66+
{
67+
// Arrange
68+
DynamicLogLevelProvider provider = new();
69+
provider.SetInitialLogLevel(LogLevel.Error, isCliOverridden: false);
70+
71+
// Act
72+
bool result = provider.UpdateFromMcp("invalid");
73+
74+
// Assert
75+
Assert.IsFalse(result);
76+
Assert.AreEqual(LogLevel.Error, provider.CurrentLogLevel);
77+
}
78+
79+
[TestMethod]
80+
public void ShouldLog_ReturnsCorrectResult()
81+
{
82+
// Arrange
83+
DynamicLogLevelProvider provider = new();
84+
provider.SetInitialLogLevel(LogLevel.Warning, isCliOverridden: false);
85+
86+
// Assert - logs at or above Warning should pass
87+
Assert.IsTrue(provider.ShouldLog(LogLevel.Warning));
88+
Assert.IsTrue(provider.ShouldLog(LogLevel.Error));
89+
Assert.IsFalse(provider.ShouldLog(LogLevel.Debug));
90+
}
91+
}
92+
}

src/Service/Program.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Text.RegularExpressions;
1010
using System.Threading.Tasks;
1111
using Azure.DataApiBuilder.Config;
12+
using Azure.DataApiBuilder.Core.Telemetry;
1213
using Azure.DataApiBuilder.Service.Exceptions;
1314
using Azure.DataApiBuilder.Service.Telemetry;
1415
using Azure.DataApiBuilder.Service.Utilities;
@@ -110,6 +111,7 @@ public static IHostBuilder CreateHostBuilder(string[] args, bool runMcpStdio, st
110111
.ConfigureServices((context, services) =>
111112
{
112113
services.AddSingleton(LogLevelProvider);
114+
services.AddSingleton<ILogLevelController>(LogLevelProvider);
113115
})
114116
.ConfigureLogging(logging =>
115117
{

src/Service/Telemetry/DynamicLogLevelProvider.cs

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,43 @@
1+
using System;
2+
using System.Collections.Generic;
13
using Azure.DataApiBuilder.Config.ObjectModel;
4+
using Azure.DataApiBuilder.Core.Telemetry;
25
using Microsoft.Extensions.Logging;
36

47
namespace Azure.DataApiBuilder.Service.Telemetry
58
{
6-
public class DynamicLogLevelProvider
9+
/// <summary>
10+
/// Provides dynamic log level control with support for CLI override, runtime config, and MCP.
11+
/// </summary>
12+
public class DynamicLogLevelProvider : ILogLevelController
713
{
14+
/// <summary>
15+
/// Maps MCP log level strings to Microsoft.Extensions.Logging.LogLevel.
16+
/// MCP levels: debug, info, notice, warning, error, critical, alert, emergency.
17+
/// </summary>
18+
private static readonly Dictionary<string, LogLevel> _mcpLevelMapping = new(StringComparer.OrdinalIgnoreCase)
19+
{
20+
["debug"] = LogLevel.Debug,
21+
["info"] = LogLevel.Information,
22+
["notice"] = LogLevel.Information, // MCP "notice" maps to Information (no direct equivalent)
23+
["warning"] = LogLevel.Warning,
24+
["error"] = LogLevel.Error,
25+
["critical"] = LogLevel.Critical,
26+
["alert"] = LogLevel.Critical, // MCP "alert" maps to Critical
27+
["emergency"] = LogLevel.Critical // MCP "emergency" maps to Critical
28+
};
29+
830
public LogLevel CurrentLogLevel { get; private set; }
31+
932
public bool IsCliOverridden { get; private set; }
1033

11-
public void SetInitialLogLevel(LogLevel logLevel = LogLevel.Error, bool isCliOverridden = false)
34+
public bool IsConfigOverridden { get; private set; }
35+
36+
public void SetInitialLogLevel(LogLevel logLevel = LogLevel.Error, bool isCliOverridden = false, bool isConfigOverridden = false)
1237
{
1338
CurrentLogLevel = logLevel;
1439
IsCliOverridden = isCliOverridden;
40+
IsConfigOverridden = isConfigOverridden;
1541
}
1642

1743
public void UpdateFromRuntimeConfig(RuntimeConfig runtimeConfig)
@@ -20,7 +46,52 @@ public void UpdateFromRuntimeConfig(RuntimeConfig runtimeConfig)
2046
if (!IsCliOverridden)
2147
{
2248
CurrentLogLevel = runtimeConfig.GetConfiguredLogLevel();
49+
50+
// Track if config explicitly set a log level (not just using defaults)
51+
IsConfigOverridden = !runtimeConfig.IsLogLevelNull();
52+
}
53+
}
54+
55+
/// <summary>
56+
/// Updates the log level from an MCP logging/setLevel request.
57+
/// Precedence (highest to lowest):
58+
/// 1. CLI --LogLevel flag (IsCliOverridden = true)
59+
/// 2. Config runtime.telemetry.log-level (IsConfigOverridden = true)
60+
/// 3. MCP logging/setLevel
61+
///
62+
/// If CLI or Config overrode, this method accepts the request silently but does not change the level.
63+
/// </summary>
64+
/// <param name="mcpLevel">The MCP log level string (e.g., "debug", "info", "warning", "error").</param>
65+
/// <returns>True if the level was changed; false if CLI/Config override prevented the change or level was invalid.</returns>
66+
public bool UpdateFromMcp(string mcpLevel)
67+
{
68+
// If CLI overrode the log level, accept the request but don't change anything.
69+
// This prevents MCP clients from getting errors, but CLI wins.
70+
if (IsCliOverridden)
71+
{
72+
return false;
2373
}
74+
75+
// If Config explicitly set the log level, accept the request but don't change anything.
76+
// Config has second precedence after CLI.
77+
if (IsConfigOverridden)
78+
{
79+
return false;
80+
}
81+
82+
if (string.IsNullOrWhiteSpace(mcpLevel))
83+
{
84+
return false;
85+
}
86+
87+
if (_mcpLevelMapping.TryGetValue(mcpLevel, out LogLevel logLevel))
88+
{
89+
CurrentLogLevel = logLevel;
90+
return true;
91+
}
92+
93+
// Unknown level - don't change, but don't fail either
94+
return false;
2495
}
2596

2697
public bool ShouldLog(LogLevel logLevel)

0 commit comments

Comments
 (0)