Skip to content

Commit d023b9d

Browse files
committed
Add McpLogger/McpLoggerProvider for server logs
Adds a new ILoggerProvider to the McpServer's logging factory which can then relay server logs which occur within the System.Extensions.Logging pattern back to the client via a notification message as per the spec.
1 parent a97a74f commit d023b9d

File tree

7 files changed

+154
-0
lines changed

7 files changed

+154
-0
lines changed

src/ModelContextProtocol/Client/McpClientExtensions.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,19 @@ namespace ModelContextProtocol.Client;
1313
/// </summary>
1414
public static class McpClientExtensions
1515
{
16+
/// <summary>
17+
/// A request from the client to the server, to enable or adjust logging.
18+
/// </summary>
19+
/// <param name="client">The client.</param>
20+
/// <param name="loggingLevel">The logging level severity to set.</param>
21+
/// <param name="cancellationToken">A token to cancel the operation.</param>
22+
/// <returns></returns>
23+
public static Task SetLogLevelAsync(this IMcpClient client, LoggingLevel loggingLevel, CancellationToken cancellationToken = default)
24+
{
25+
Throw.IfNull(client);
26+
return client.SendNotificationAsync("logging/setLevel", new SetLoggingLevelRequestParams { Level = loggingLevel }, cancellationToken);
27+
}
28+
1629
/// <summary>
1730
/// Sends a notification to the server with parameters.
1831
/// </summary>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using Microsoft.Extensions.Logging;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Collections.Concurrent;
5+
using ModelContextProtocol.Protocol.Types;
6+
using ModelContextProtocol.Server;
7+
using System.Text.Json;
8+
using System.Diagnostics.CodeAnalysis;
9+
10+
namespace ModelContextProtocol.Logging;
11+
12+
internal class McpLogger(string categoryName, IMcpServer mcpServer) : ILogger
13+
{
14+
public IDisposable? BeginScope<TState>(TState state) where TState : notnull
15+
=> default;
16+
17+
public bool IsEnabled(LogLevel logLevel)
18+
=> logLevel.ToLoggingLevel() <= mcpServer.LoggingLevel;
19+
20+
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
21+
[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "<Pending>")]
22+
public async void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
23+
{
24+
if (!IsEnabled(logLevel))
25+
return;
26+
27+
var message = formatter(state, exception);
28+
if (string.IsNullOrEmpty(message))
29+
return;
30+
31+
// Use JsonSerializer to properly escape the string for JSON and turn it into a JsonElement
32+
var json = JsonSerializer.Serialize(message);
33+
var element = JsonSerializer.Deserialize<JsonElement>(json);
34+
35+
await mcpServer.SendLogNotificationAsync(new LoggingMessageNotificationParams
36+
{
37+
Data = element,
38+
Level = logLevel.ToLoggingLevel(),
39+
Logger = categoryName
40+
});
41+
}
42+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using Microsoft.Extensions.Logging;
3+
using ModelContextProtocol.Server;
4+
using System;
5+
using System.Collections.Concurrent;
6+
7+
namespace ModelContextProtocol.Logging
8+
{
9+
/// <summary>
10+
/// Provides logging over MCP's notifications to send log messages to the client
11+
/// </summary>
12+
/// <param name="mcpServer">MCP Server.</param>
13+
public class McpLoggerProvider(IMcpServer mcpServer) : ILoggerProvider
14+
{
15+
/// <summary>
16+
/// Creates a new instance of an MCP logger
17+
/// </summary>
18+
/// <param name="categoryName">Logger Category Name</param>
19+
/// <returns>New Logger instance</returns>
20+
public ILogger CreateLogger(string categoryName)
21+
=> new McpLogger(categoryName, mcpServer);
22+
23+
/// <summary>
24+
/// Dispose
25+
/// </summary>
26+
public void Dispose()
27+
{
28+
}
29+
}
30+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using Microsoft.Extensions.Logging;
2+
using ModelContextProtocol.Protocol.Types;
3+
4+
namespace ModelContextProtocol.Logging;
5+
6+
internal static class McpLoggingLevelExtensions
7+
{
8+
public static LogLevel ToLogLevel(this LoggingLevel level)
9+
=> level switch
10+
{
11+
LoggingLevel.Emergency or LoggingLevel.Alert or LoggingLevel.Critical => LogLevel.Critical,
12+
LoggingLevel.Error => LogLevel.Error,
13+
LoggingLevel.Warning => LogLevel.Warning,
14+
LoggingLevel.Notice or LoggingLevel.Info => LogLevel.Information,
15+
LoggingLevel.Debug => LogLevel.Debug,
16+
_ => LogLevel.None,
17+
};
18+
19+
public static LoggingLevel ToLoggingLevel(this LogLevel level)
20+
=> level switch
21+
{
22+
LogLevel.Critical => LoggingLevel.Critical,
23+
LogLevel.Error => LoggingLevel.Error,
24+
LogLevel.Warning => LoggingLevel.Warning,
25+
LogLevel.Information => LoggingLevel.Info,
26+
LogLevel.Debug or LogLevel.Trace => LoggingLevel.Debug,
27+
_ => LoggingLevel.Info,
28+
};
29+
}

src/ModelContextProtocol/Server/IMcpServer.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ public interface IMcpServer : IAsyncDisposable
2828
/// </summary>
2929
IServiceProvider? ServiceProvider { get; }
3030

31+
/// <summary>
32+
/// Current Logging level
33+
/// </summary>
34+
LoggingLevel LoggingLevel { get; }
35+
3136
/// <summary>
3237
/// Adds a handler for client notifications of a specific method.
3338
/// </summary>

src/ModelContextProtocol/Server/McpServer.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ public McpServer(ITransport transport, McpServerOptions options, ILoggerFactory?
3535

3636
_serverTransport = transport as IServerTransport;
3737
_options = options;
38+
39+
// Add the MCP Logger provider to send MCP notification messages to the client when logs occur
40+
loggerFactory?.AddProvider(new McpLoggerProvider(this));
41+
3842
_logger = (ILogger?)loggerFactory?.CreateLogger<McpServer>() ?? NullLogger.Instance;
3943
ServerInstructions = options.ServerInstructions;
4044
ServiceProvider = serviceProvider;
@@ -44,6 +48,7 @@ public McpServer(ITransport transport, McpServerOptions options, ILoggerFactory?
4448
IsInitialized = true;
4549
return Task.CompletedTask;
4650
});
51+
AddLoggingLevelNotificationHandler(options);
4752

4853
SetInitializeHandler(options);
4954
SetCompletionHandler(options);
@@ -65,6 +70,8 @@ public McpServer(ITransport transport, McpServerOptions options, ILoggerFactory?
6570
/// <inheritdoc />
6671
public IServiceProvider? ServiceProvider { get; }
6772

73+
public LoggingLevel LoggingLevel { get; private set; }
74+
6875
/// <inheritdoc />
6976
public override string EndpointName =>
7077
$"Server ({_options.ServerInfo.Name} {_options.ServerInfo.Version}), Client ({ClientInfo?.Name} {ClientInfo?.Version})";
@@ -140,6 +147,18 @@ options.GetCompletionHandler is { } handler ?
140147
(request, ct) => Task.FromResult(new CompleteResult() { Completion = new() { Values = [], Total = 0, HasMore = false } }));
141148
}
142149

150+
private void AddLoggingLevelNotificationHandler(McpServerOptions options)
151+
{
152+
AddNotificationHandler("notifications/message", notification =>
153+
{
154+
if (notification.Params is LoggingMessageNotificationParams loggingMessageNotificationParams)
155+
{
156+
LoggingLevel = loggingMessageNotificationParams.Level;
157+
}
158+
return Task.CompletedTask;
159+
});
160+
}
161+
143162
private void SetResourcesHandler(McpServerOptions options)
144163
{
145164
if (options.Capabilities?.Resources is not { } resourcesCapability)

src/ModelContextProtocol/Server/McpServerExtensions.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,22 @@ namespace ModelContextProtocol.Server;
1010
/// <inheritdoc />
1111
public static class McpServerExtensions
1212
{
13+
/// <summary>
14+
/// Sends a logging message notification to the client.
15+
/// </summary>
16+
/// <param name="server">The server instance that will handle the log notification request.</param>
17+
/// <param name="loggingMessageNotification">Contains the details of the log message to be sent.</param>
18+
/// <param name="cancellationToken">Allows the operation to be canceled if needed.</param>
19+
/// <returns>Returns a task representing the asynchronous operation.</returns>
20+
public static Task SendLogNotificationAsync(this IMcpServer server, LoggingMessageNotificationParams loggingMessageNotification, CancellationToken cancellationToken = default)
21+
{
22+
Throw.IfNull(server);
23+
Throw.IfNull(loggingMessageNotification);
24+
return server.SendRequestAsync<EmptyResult>(
25+
new JsonRpcRequest { Method = "notifications/message", Params = loggingMessageNotification },
26+
cancellationToken);
27+
}
28+
1329
/// <summary>
1430
/// Requests to sample an LLM via the client.
1531
/// </summary>

0 commit comments

Comments
 (0)