diff --git a/src/ModelContextProtocol/Client/McpClientExtensions.cs b/src/ModelContextProtocol/Client/McpClientExtensions.cs
index 56b77f69..f11f0295 100644
--- a/src/ModelContextProtocol/Client/McpClientExtensions.cs
+++ b/src/ModelContextProtocol/Client/McpClientExtensions.cs
@@ -474,6 +474,21 @@ internal static CreateMessageResult ToCreateMessageResult(this ChatResponse chat
};
}
+ ///
+ /// Configures the minimum logging level for the server.
+ ///
+ /// The client.
+ /// The minimum log level of messages to be generated.
+ /// A token to cancel the operation.
+ public static Task SetLoggingLevel(this IMcpClient client, LoggingLevel level, CancellationToken cancellationToken = default)
+ {
+ Throw.IfNull(client);
+
+ return client.SendRequestAsync(
+ CreateRequest("logging/setLevel", new() { ["level"] = level.ToJsonValue() }),
+ cancellationToken);
+ }
+
private static JsonRpcRequest CreateRequest(string method, Dictionary? parameters) =>
new()
{
@@ -499,6 +514,22 @@ private static JsonRpcRequest CreateRequest(string method, Dictionary "debug",
+ LoggingLevel.Info => "info",
+ LoggingLevel.Notice => "notice",
+ LoggingLevel.Warning => "warning",
+ LoggingLevel.Error => "error",
+ LoggingLevel.Critical => "critical",
+ LoggingLevel.Alert => "alert",
+ LoggingLevel.Emergency => "emergency",
+ _ => throw new ArgumentOutOfRangeException(nameof(level))
+ };
+ }
+
/// Provides an AI function that calls a tool through .
private sealed class McpAIFunction(IMcpClient client, Tool tool) : AIFunction
{
diff --git a/src/ModelContextProtocol/Protocol/Messages/NotificationMethods.cs b/src/ModelContextProtocol/Protocol/Messages/NotificationMethods.cs
index 063f503a..521d3eb3 100644
--- a/src/ModelContextProtocol/Protocol/Messages/NotificationMethods.cs
+++ b/src/ModelContextProtocol/Protocol/Messages/NotificationMethods.cs
@@ -29,4 +29,9 @@ public static class NotificationMethods
/// Sent by the client when roots have been updated.
///
public const string RootsUpdatedNotification = "notifications/roots/list_changed";
+
+ ///
+ /// Sent by the server when a log message is generated.
+ ///
+ public const string LoggingMessageNotification = "notifications/message";
}
\ No newline at end of file
diff --git a/src/ModelContextProtocol/Protocol/Types/Capabilities.cs b/src/ModelContextProtocol/Protocol/Types/Capabilities.cs
index 3392f57c..0e4b88f9 100644
--- a/src/ModelContextProtocol/Protocol/Types/Capabilities.cs
+++ b/src/ModelContextProtocol/Protocol/Types/Capabilities.cs
@@ -65,6 +65,13 @@ public record SamplingCapability
public record LoggingCapability
{
// Currently empty in the spec, but may be extended in the future
+
+
+ ///
+ /// Gets or sets the handler for set logging level requests.
+ ///
+ [JsonIgnore]
+ public Func, CancellationToken, Task>? SetLoggingLevelHandler { get; init; }
}
///
diff --git a/src/ModelContextProtocol/Protocol/Types/LoggingLevel.cs b/src/ModelContextProtocol/Protocol/Types/LoggingLevel.cs
new file mode 100644
index 00000000..4e603e69
--- /dev/null
+++ b/src/ModelContextProtocol/Protocol/Types/LoggingLevel.cs
@@ -0,0 +1,43 @@
+using System.Text.Json.Serialization;
+
+namespace ModelContextProtocol.Protocol.Types;
+
+///
+/// The severity of a log message.
+/// These map to syslog message severities, as specified in RFC-5424:
+/// https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1
+///
+public enum LoggingLevel
+{
+ /// Detailed debug information, typically only valuable to developers.
+ [JsonPropertyName("debug")]
+ Debug,
+
+ /// Normal operational messages that require no action.
+ [JsonPropertyName("info")]
+ Info,
+
+ /// Normal but significant events that might deserve attention.
+ [JsonPropertyName("notice")]
+ Notice,
+
+ /// Warning conditions that don't represent an error but indicate potential issues.
+ [JsonPropertyName("warning")]
+ Warning,
+
+ /// Error conditions that should be addressed but don't require immediate action.
+ [JsonPropertyName("error")]
+ Error,
+
+ /// Critical conditions that require immediate attention.
+ [JsonPropertyName("critical")]
+ Critical,
+
+ /// Action must be taken immediately to address the condition.
+ [JsonPropertyName("alert")]
+ Alert,
+
+ /// System is unusable and requires immediate attention.
+ [JsonPropertyName("emergency")]
+ Emergency
+}
\ No newline at end of file
diff --git a/src/ModelContextProtocol/Protocol/Types/LoggingMessageNotificationParams.cs b/src/ModelContextProtocol/Protocol/Types/LoggingMessageNotificationParams.cs
new file mode 100644
index 00000000..2784bbcb
--- /dev/null
+++ b/src/ModelContextProtocol/Protocol/Types/LoggingMessageNotificationParams.cs
@@ -0,0 +1,31 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace ModelContextProtocol.Protocol.Types;
+
+///
+/// Sent from the server as the payload of "notifications/message" notifications whenever a log message is generated.
+///
+/// If no logging/setLevel request has been sent from the client, the server MAY decide which messages to send automatically.
+/// See the schema for details
+///
+public class LoggingMessageNotificationParams
+{
+ ///
+ /// The severity of this log message.
+ ///
+ [JsonPropertyName("level")]
+ public LoggingLevel Level { get; init; }
+
+ ///
+ /// An optional name of the logger issuing this message.
+ ///
+ [JsonPropertyName("logger")]
+ public string? Logger { get; init; }
+
+ ///
+ /// The data to be logged, such as a string message or an object. Any JSON serializable type is allowed here.
+ ///
+ [JsonPropertyName("data")]
+ public JsonElement? Data { get; init; }
+}
\ No newline at end of file
diff --git a/src/ModelContextProtocol/Protocol/Types/SetLevelRequestParams.cs b/src/ModelContextProtocol/Protocol/Types/SetLevelRequestParams.cs
new file mode 100644
index 00000000..9491cd77
--- /dev/null
+++ b/src/ModelContextProtocol/Protocol/Types/SetLevelRequestParams.cs
@@ -0,0 +1,17 @@
+using System.Text.Json.Serialization;
+
+namespace ModelContextProtocol.Protocol.Types;
+
+///
+/// A request from the client to the server, to enable or adjust logging.
+/// See the schema for details
+///
+public class SetLevelRequestParams
+{
+ ///
+ /// The level of logging that the client wants to receive from the server.
+ /// The server should send all logs at this level and higher (i.e., more severe) to the client as notifications/message.
+ ///
+ [JsonPropertyName("level")]
+ public required LoggingLevel Level { get; init; }
+}
diff --git a/src/ModelContextProtocol/Server/McpServer.cs b/src/ModelContextProtocol/Server/McpServer.cs
index 6ad1defe..c2fdb074 100644
--- a/src/ModelContextProtocol/Server/McpServer.cs
+++ b/src/ModelContextProtocol/Server/McpServer.cs
@@ -51,6 +51,7 @@ public McpServer(ITransport transport, McpServerOptions options, ILoggerFactory?
SetToolsHandler(options);
SetPromptsHandler(options);
SetResourcesHandler(options);
+ SetSetLoggingLevelHandler(options);
}
public ClientCapabilities? ClientCapabilities { get; set; }
@@ -210,4 +211,19 @@ private void SetToolsHandler(McpServerOptions options)
SetRequestHandler("tools/list", (request, ct) => listToolsHandler(new(this, request), ct));
SetRequestHandler("tools/call", (request, ct) => callToolHandler(new(this, request), ct));
}
+
+ private void SetSetLoggingLevelHandler(McpServerOptions options)
+ {
+ if (options.Capabilities?.Logging is not { } loggingCapability)
+ {
+ return;
+ }
+
+ if (loggingCapability.SetLoggingLevelHandler is not { } setLoggingLevelHandler)
+ {
+ throw new McpServerException("Logging capability was enabled, but SetLoggingLevelHandler was not specified.");
+ }
+
+ SetRequestHandler("logging/setLevel", (request, ct) => setLoggingLevelHandler(new(this, request), ct));
+ }
}
diff --git a/src/ModelContextProtocol/Server/McpServerHandlers.cs b/src/ModelContextProtocol/Server/McpServerHandlers.cs
index c41c601e..32deca49 100644
--- a/src/ModelContextProtocol/Server/McpServerHandlers.cs
+++ b/src/ModelContextProtocol/Server/McpServerHandlers.cs
@@ -57,6 +57,11 @@ public sealed class McpServerHandlers
///
public Func, CancellationToken, Task>? UnsubscribeFromResourcesHandler { get; set; }
+ ///
+ /// Get or sets the handler for set logging level requests.
+ ///
+ public Func, CancellationToken, Task>? SetLoggingLevelHandler { get; set; }
+
///
/// Overwrite any handlers in McpServerOptions with non-null handlers from this instance.
///
@@ -125,6 +130,20 @@ toolsCapability with
};
}
+ LoggingCapability? loggingCapability = options.Capabilities?.Logging;
+ if (SetLoggingLevelHandler is not null)
+ {
+ loggingCapability = loggingCapability is null ?
+ new()
+ {
+ SetLoggingLevelHandler = SetLoggingLevelHandler,
+ } :
+ loggingCapability with
+ {
+ SetLoggingLevelHandler = SetLoggingLevelHandler ?? loggingCapability.SetLoggingLevelHandler,
+ };
+ }
+
options.Capabilities = options.Capabilities is null ?
new()
{
diff --git a/tests/ModelContextProtocol.TestServer/Program.cs b/tests/ModelContextProtocol.TestServer/Program.cs
index 8574a73a..e0558447 100644
--- a/tests/ModelContextProtocol.TestServer/Program.cs
+++ b/tests/ModelContextProtocol.TestServer/Program.cs
@@ -39,6 +39,7 @@ private static async Task Main(string[] args)
Tools = ConfigureTools(),
Resources = ConfigureResources(),
Prompts = ConfigurePrompts(),
+ Logging = ConfigureLogging()
},
ProtocolVersion = "2024-11-05",
ServerInstructions = "This is a test server with only stub functionality",
@@ -54,10 +55,35 @@ private static async Task Main(string[] args)
Log.Logger.Information("Server started.");
+ // everything server sends random log level messages every 15 seconds
+ int loggingSeconds = 0;
+ Random random = Random.Shared;
+ var loggingLevels = Enum.GetValues().ToList();
+
// Run until process is stopped by the client (parent process)
while (true)
{
await Task.Delay(5000);
+ if (_minimumLoggingLevel is not null)
+ {
+ loggingSeconds += 5;
+
+ // Send random log messages every 15 seconds
+ if (loggingSeconds >= 15)
+ {
+ var logLevelIndex = random.Next(loggingLevels.Count);
+ var logLevel = loggingLevels[logLevelIndex];
+ await server.SendMessageAsync(new JsonRpcNotification()
+ {
+ Method = NotificationMethods.LoggingMessageNotification,
+ Params = new LoggingMessageNotificationParams
+ {
+ Level = logLevel,
+ Data = JsonSerializer.Deserialize("\"Random log message\"")
+ }
+ });
+ }
+ }
// Snapshot the subscribed resources, rather than locking while sending notifications
List resources;
@@ -266,6 +292,26 @@ private static PromptsCapability ConfigurePrompts()
};
}
+ private static LoggingLevel? _minimumLoggingLevel = null;
+
+ private static LoggingCapability ConfigureLogging()
+ {
+ return new()
+ {
+ SetLoggingLevelHandler = (request, cancellationToken) =>
+ {
+ if (request.Params?.Level is null)
+ {
+ throw new McpServerException("Missing required argument 'level'");
+ }
+
+ _minimumLoggingLevel = request.Params.Level;
+
+ return Task.FromResult(new EmptyResult());
+ }
+ };
+ }
+
private static readonly HashSet _subscribedResources = new();
private static readonly object _subscribedResourcesLock = new();
diff --git a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs
index 121c4eac..f1996ca3 100644
--- a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs
+++ b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs
@@ -7,6 +7,9 @@
using ModelContextProtocol.Configuration;
using ModelContextProtocol.Protocol.Transport;
using Xunit.Sdk;
+using System.Text.Encodings.Web;
+using System.Text.Json.Serialization.Metadata;
+using System.Text.Json.Serialization;
namespace ModelContextProtocol.Tests;
@@ -550,6 +553,41 @@ public async Task SamplingViaChatClient_RequestResponseProperlyPropagated()
Assert.Contains("Eiffel", result.Content[0].Text);
}
+ [Theory]
+ [MemberData(nameof(GetClients))]
+ public async Task SetLoggingLevel_ReceivesLoggingMessages(string clientId)
+ {
+ // arrange
+ JsonSerializerOptions jsonSerializerOptions = new(JsonSerializerDefaults.Web)
+ {
+ TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
+ Converters = { new JsonStringEnumConverter() },
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ NumberHandling = JsonNumberHandling.AllowReadingFromString,
+ Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
+ };
+
+ int logCounter = 0;
+ await using var client = await _fixture.CreateClientAsync(clientId);
+ client.AddNotificationHandler(NotificationMethods.LoggingMessageNotification, (notification) =>
+ {
+ var loggingMessageNotificationParameters = JsonSerializer.Deserialize(notification.Params!.ToString() ?? string.Empty,
+ jsonSerializerOptions);
+ if (loggingMessageNotificationParameters is not null)
+ {
+ ++logCounter;
+ }
+ return Task.CompletedTask;
+ });
+
+ // act
+ await client.SetLoggingLevel(LoggingLevel.Debug, CancellationToken.None);
+ await Task.Delay(16000, TestContext.Current.CancellationToken);
+
+ // assert
+ Assert.True(logCounter > 0);
+ }
+
private static void SkipTestIfNoOpenAIKey()
{
Assert.SkipWhen(s_openAIKey is null, "No OpenAI key provided. Skipping test.");