Skip to content

Commit 1ba3f54

Browse files
committed
Logging Capability
1 parent 3e4dd01 commit 1ba3f54

File tree

10 files changed

+250
-0
lines changed

10 files changed

+250
-0
lines changed

src/ModelContextProtocol/Client/McpClientExtensions.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,21 @@ internal static CreateMessageResult ToCreateMessageResult(this ChatResponse chat
436436
};
437437
}
438438

439+
/// <summary>
440+
/// Configures the minimum logging level for the server.
441+
/// </summary>
442+
/// <param name="client">The client.</param>
443+
/// <param name="level">The minimum log level of messages to be generated.</param>
444+
/// <param name="cancellationToken">A token to cancel the operation.</param>
445+
public static Task SetLoggingLevel(this IMcpClient client, LoggingLevel level, CancellationToken cancellationToken = default)
446+
{
447+
Throw.IfNull(client);
448+
449+
return client.SendRequestAsync<EmptyResult>(
450+
CreateRequest("logging/setLevel", new() { ["level"] = level.ToJsonValue() }),
451+
cancellationToken);
452+
}
453+
439454
private static JsonRpcRequest CreateRequest(string method, Dictionary<string, object?>? parameters) =>
440455
new()
441456
{
@@ -461,6 +476,22 @@ private static JsonRpcRequest CreateRequest(string method, Dictionary<string, ob
461476
return parameters;
462477
}
463478

479+
private static string ToJsonValue(this LoggingLevel level)
480+
{
481+
return level switch
482+
{
483+
LoggingLevel.Debug => "debug",
484+
LoggingLevel.Info => "info",
485+
LoggingLevel.Notice => "notice",
486+
LoggingLevel.Warning => "warning",
487+
LoggingLevel.Error => "error",
488+
LoggingLevel.Critical => "critical",
489+
LoggingLevel.Alert => "alert",
490+
LoggingLevel.Emergency => "emergency",
491+
_ => throw new ArgumentOutOfRangeException(nameof(level))
492+
};
493+
}
494+
464495
/// <summary>Provides an AI function that calls a tool through <see cref="IMcpClient"/>.</summary>
465496
private sealed class McpAIFunction(IMcpClient client, Tool tool) : AIFunction
466497
{

src/ModelContextProtocol/Protocol/Messages/NotificationMethods.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,9 @@ public static class NotificationMethods
2929
/// Sent by the client when roots have been updated.
3030
/// </summary>
3131
public const string RootsUpdatedNotification = "notifications/roots/list_changed";
32+
33+
/// <summary>
34+
/// Sent by the server when a log message is generated.
35+
/// </summary>
36+
public const string LoggingMessageNotification = "notifications/message";
3237
}

src/ModelContextProtocol/Protocol/Types/Capabilities.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,13 @@ public record SamplingCapability
6565
public record LoggingCapability
6666
{
6767
// Currently empty in the spec, but may be extended in the future
68+
69+
70+
/// <summary>
71+
/// Gets or sets the handler for set logging level requests.
72+
/// </summary>
73+
[JsonIgnore]
74+
public Func<RequestContext<SetLevelRequestParams>, CancellationToken, Task<EmptyResult>>? SetLoggingLevelHandler { get; init; }
6875
}
6976

7077
/// <summary>
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace ModelContextProtocol.Protocol.Types;
4+
5+
/// <summary>
6+
/// The severity of a log message.
7+
/// These map to syslog message severities, as specified in RFC-5424:
8+
/// https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1
9+
/// </summary>
10+
public enum LoggingLevel
11+
{
12+
/// <summary>Detailed debug information, typically only valuable to developers.</summary>
13+
[JsonPropertyName("debug")]
14+
Debug,
15+
16+
/// <summary>Normal operational messages that require no action.</summary>
17+
[JsonPropertyName("info")]
18+
Info,
19+
20+
/// <summary>Normal but significant events that might deserve attention.</summary>
21+
[JsonPropertyName("notice")]
22+
Notice,
23+
24+
/// <summary>Warning conditions that don't represent an error but indicate potential issues.</summary>
25+
[JsonPropertyName("warning")]
26+
Warning,
27+
28+
/// <summary>Error conditions that should be addressed but don't require immediate action.</summary>
29+
[JsonPropertyName("error")]
30+
Error,
31+
32+
/// <summary>Critical conditions that require immediate attention.</summary>
33+
[JsonPropertyName("critical")]
34+
Critical,
35+
36+
/// <summary>Action must be taken immediately to address the condition.</summary>
37+
[JsonPropertyName("alert")]
38+
Alert,
39+
40+
/// <summary>System is unusable and requires immediate attention.</summary>
41+
[JsonPropertyName("emergency")]
42+
Emergency
43+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using System.Text.Json;
2+
3+
namespace ModelContextProtocol.Protocol.Types;
4+
5+
/// <summary>
6+
/// Sent from the server as the payload of "notifications/message" notifications whenever a log message is generated.
7+
///
8+
/// If no logging/setLevel request has been sent from the client, the server MAY decide which messages to send automatically.
9+
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
10+
/// </summary>
11+
public class LoggingMessageNotificationParams
12+
{
13+
/// <summary>
14+
/// The severity of this log message.
15+
/// </summary>
16+
[System.Text.Json.Serialization.JsonPropertyName("level")]
17+
public LoggingLevel Level { get; init; }
18+
19+
/// <summary>
20+
/// An optional name of the logger issuing this message.
21+
/// </summary>
22+
[System.Text.Json.Serialization.JsonPropertyName("logger")]
23+
public string? Logger { get; init; }
24+
25+
/// <summary>
26+
/// The data to be logged, such as a string message or an object. Any JSON serializable type is allowed here.
27+
/// </summary>
28+
[System.Text.Json.Serialization.JsonPropertyName("data")]
29+
public JsonElement? Data { get; init; }
30+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace ModelContextProtocol.Protocol.Types;
2+
3+
/// <summary>
4+
/// A request from the client to the server, to enable or adjust logging.
5+
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
6+
/// </summary>
7+
public class SetLevelRequestParams
8+
{
9+
/// <summary>
10+
/// The level of logging that the client wants to receive from the server.
11+
/// The server should send all logs at this level and higher (i.e., more severe) to the client as notifications/message.
12+
/// </summary>
13+
[System.Text.Json.Serialization.JsonPropertyName("level")]
14+
public required LoggingLevel Level { get; init; }
15+
}

src/ModelContextProtocol/Server/McpServer.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ public McpServer(ITransport transport, McpServerOptions options, ILoggerFactory?
5151
SetToolsHandler(options);
5252
SetPromptsHandler(options);
5353
SetResourcesHandler(options);
54+
SetSetLoggingLevelHandler(options);
5455
}
5556

5657
public ClientCapabilities? ClientCapabilities { get; set; }
@@ -204,4 +205,18 @@ private void SetToolsHandler(McpServerOptions options)
204205
SetRequestHandler<ListToolsRequestParams, ListToolsResult>("tools/list", (request, ct) => listToolsHandler(new(this, request), ct));
205206
SetRequestHandler<CallToolRequestParams, CallToolResponse>("tools/call", (request, ct) => callToolHandler(new(this, request), ct));
206207
}
208+
209+
private void SetSetLoggingLevelHandler(McpServerOptions options)
210+
{
211+
if (options.Capabilities?.Logging is not { } loggingCapability)
212+
{
213+
return;
214+
}
215+
if (loggingCapability.SetLoggingLevelHandler is not { } setLoggingLevelHandler)
216+
{
217+
throw new McpServerException("Logging capability was enabled, but SetLoggingLevelHandler was not specified.");
218+
}
219+
220+
SetRequestHandler<SetLevelRequestParams, EmptyResult>("logging/setLevel", (request, ct) => setLoggingLevelHandler(new(this, request), ct));
221+
}
207222
}

src/ModelContextProtocol/Server/McpServerHandlers.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ public sealed class McpServerHandlers
5252
/// </summary>
5353
public Func<RequestContext<UnsubscribeRequestParams>, CancellationToken, Task<EmptyResult>>? UnsubscribeFromResourcesHandler { get; set; }
5454

55+
/// <summary>
56+
/// Get or sets the handler for set logging level requests.
57+
/// </summary>
58+
public Func<RequestContext<SetLevelRequestParams>, CancellationToken, Task<EmptyResult>>? SetLoggingLevelHandler { get; set; }
59+
5560
/// <summary>
5661
/// Overwrite any handlers in McpServerOptions with non-null handlers from this instance.
5762
/// </summary>
@@ -118,6 +123,20 @@ toolsCapability with
118123
};
119124
}
120125

126+
LoggingCapability? loggingCapability = options.Capabilities?.Logging;
127+
if (SetLoggingLevelHandler is not null)
128+
{
129+
loggingCapability = loggingCapability is null ?
130+
new()
131+
{
132+
SetLoggingLevelHandler = SetLoggingLevelHandler,
133+
} :
134+
loggingCapability with
135+
{
136+
SetLoggingLevelHandler = SetLoggingLevelHandler ?? loggingCapability.SetLoggingLevelHandler,
137+
};
138+
}
139+
121140
options.Capabilities = options.Capabilities is null ?
122141
new()
123142
{

tests/ModelContextProtocol.TestServer/Program.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using ModelContextProtocol.Protocol.Types;
55
using ModelContextProtocol.Server;
66
using Serilog;
7+
using System.Runtime.CompilerServices;
78
using System.Text;
89
using System.Text.Json;
910

@@ -39,6 +40,7 @@ private static async Task Main(string[] args)
3940
Tools = ConfigureTools(),
4041
Resources = ConfigureResources(),
4142
Prompts = ConfigurePrompts(),
43+
Logging = ConfigureLogging()
4244
},
4345
ProtocolVersion = "2024-11-05",
4446
ServerInstructions = "This is a test server with only stub functionality",
@@ -54,10 +56,35 @@ private static async Task Main(string[] args)
5456

5557
Log.Logger.Information("Server started.");
5658

59+
// everything server sends random log level messages every 15 seconds
60+
int loggingSeconds = 0;
61+
Random random = Random.Shared;
62+
var loggingLevels = Enum.GetValues<LoggingLevel>().ToList();
63+
5764
// Run until process is stopped by the client (parent process)
5865
while (true)
5966
{
6067
await Task.Delay(5000);
68+
if (_minimumLoggingLevel is not null)
69+
{
70+
loggingSeconds += 5;
71+
72+
// Send random log messages every 15 seconds
73+
if (loggingSeconds >= 15)
74+
{
75+
var logLevelIndex = random.Next(loggingLevels.Count);
76+
var logLevel = loggingLevels[logLevelIndex];
77+
await server.SendMessageAsync(new JsonRpcNotification()
78+
{
79+
Method = NotificationMethods.LoggingMessageNotification,
80+
Params = new LoggingMessageNotificationParams
81+
{
82+
Level = logLevel,
83+
Data = JsonSerializer.Deserialize<JsonElement>("\"Random log message\"")
84+
}
85+
});
86+
}
87+
}
6188

6289
// Snapshot the subscribed resources, rather than locking while sending notifications
6390
List<string> resources;
@@ -266,6 +293,26 @@ private static PromptsCapability ConfigurePrompts()
266293
};
267294
}
268295

296+
private static LoggingLevel? _minimumLoggingLevel = null;
297+
298+
private static LoggingCapability ConfigureLogging()
299+
{
300+
return new()
301+
{
302+
SetLoggingLevelHandler = (request, cancellationToken) =>
303+
{
304+
if (request.Params?.Level is null)
305+
{
306+
throw new McpServerException("Missing required argument 'level'");
307+
}
308+
309+
_minimumLoggingLevel = request.Params.Level;
310+
311+
return Task.FromResult(new EmptyResult());
312+
}
313+
};
314+
}
315+
269316
private static readonly HashSet<string> _subscribedResources = new();
270317
private static readonly object _subscribedResourcesLock = new();
271318

tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
using ModelContextProtocol.Configuration;
88
using ModelContextProtocol.Protocol.Transport;
99
using Xunit.Sdk;
10+
using System.Text.Encodings.Web;
11+
using System.Text.Json.Serialization.Metadata;
12+
using System.Text.Json.Serialization;
1013

1114
namespace ModelContextProtocol.Tests;
1215

@@ -535,6 +538,41 @@ public async Task SamplingViaChatClient_RequestResponseProperlyPropagated()
535538
Assert.Contains("Eiffel", result.Content[0].Text);
536539
}
537540

541+
[Theory]
542+
[MemberData(nameof(GetClients))]
543+
public async Task SetLoggingLevel_ReceivesLoggingMessages(string clientId)
544+
{
545+
// arrange
546+
JsonSerializerOptions jsonSerializerOptions = new(JsonSerializerDefaults.Web)
547+
{
548+
TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
549+
Converters = { new JsonStringEnumConverter() },
550+
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
551+
NumberHandling = JsonNumberHandling.AllowReadingFromString,
552+
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
553+
};
554+
555+
int logCounter = 0;
556+
await using var client = await _fixture.CreateClientAsync(clientId);
557+
client.AddNotificationHandler(NotificationMethods.LoggingMessageNotification, (notification) =>
558+
{
559+
var loggingMessageNotificationParameters = JsonSerializer.Deserialize<LoggingMessageNotificationParams>(notification.Params!.ToString() ?? string.Empty,
560+
jsonSerializerOptions);
561+
if (loggingMessageNotificationParameters is not null)
562+
{
563+
++logCounter;
564+
}
565+
return Task.CompletedTask;
566+
});
567+
568+
// act
569+
await client.SetLoggingLevel(LoggingLevel.Debug, CancellationToken.None);
570+
await Task.Delay(16000, TestContext.Current.CancellationToken);
571+
572+
// assert
573+
Assert.True(logCounter > 0);
574+
}
575+
538576
private static void SkipTestIfNoOpenAIKey()
539577
{
540578
Assert.SkipWhen(s_openAIKey is null, "No OpenAI key provided. Skipping test.");

0 commit comments

Comments
 (0)