diff --git a/src/ModelContextProtocol/Client/McpClientExtensions.cs b/src/ModelContextProtocol/Client/McpClientExtensions.cs index f11f0295..e13bcdf1 100644 --- a/src/ModelContextProtocol/Client/McpClientExtensions.cs +++ b/src/ModelContextProtocol/Client/McpClientExtensions.cs @@ -13,6 +13,19 @@ namespace ModelContextProtocol.Client; /// public static class McpClientExtensions { + /// + /// A request from the client to the server, to enable or adjust logging. + /// + /// The client. + /// The logging level severity to set. + /// A token to cancel the operation. + /// + public static Task SetLogLevelAsync(this IMcpClient client, LoggingLevel loggingLevel, CancellationToken cancellationToken = default) + { + Throw.IfNull(client); + return client.SendNotificationAsync("logging/setLevel", new SetLevelRequestParams { Level = loggingLevel }, cancellationToken); + } + /// /// Sends a notification to the server with parameters. /// diff --git a/src/ModelContextProtocol/Logging/McpLogger.cs b/src/ModelContextProtocol/Logging/McpLogger.cs new file mode 100644 index 00000000..4c42bdea --- /dev/null +++ b/src/ModelContextProtocol/Logging/McpLogger.cs @@ -0,0 +1,42 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Collections.Concurrent; +using ModelContextProtocol.Protocol.Types; +using ModelContextProtocol.Server; +using System.Text.Json; +using System.Diagnostics.CodeAnalysis; + +namespace ModelContextProtocol.Logging; + +internal class McpLogger(string categoryName, IMcpServer mcpServer) : ILogger +{ + public IDisposable? BeginScope(TState state) where TState : notnull + => default; + + public bool IsEnabled(LogLevel logLevel) + => logLevel.ToLoggingLevel() <= mcpServer.LoggingLevel; + + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] + [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")] + public async void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (!IsEnabled(logLevel)) + return; + + var message = formatter(state, exception); + if (string.IsNullOrEmpty(message)) + return; + + // Use JsonSerializer to properly escape the string for JSON and turn it into a JsonElement + var json = JsonSerializer.Serialize(message); + var element = JsonSerializer.Deserialize(json); + + await mcpServer.SendLogNotificationAsync(new LoggingMessageNotificationParams + { + Data = element, + Level = logLevel.ToLoggingLevel(), + Logger = categoryName + }); + } +} diff --git a/src/ModelContextProtocol/Logging/McpLoggerProvider.cs b/src/ModelContextProtocol/Logging/McpLoggerProvider.cs new file mode 100644 index 00000000..f67ad08d --- /dev/null +++ b/src/ModelContextProtocol/Logging/McpLoggerProvider.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using ModelContextProtocol.Server; +using System; +using System.Collections.Concurrent; + +namespace ModelContextProtocol.Logging +{ + /// + /// Provides logging over MCP's notifications to send log messages to the client + /// + /// MCP Server. + public class McpLoggerProvider(IMcpServer mcpServer) : ILoggerProvider + { + /// + /// Creates a new instance of an MCP logger + /// + /// Logger Category Name + /// New Logger instance + public ILogger CreateLogger(string categoryName) + => new McpLogger(categoryName, mcpServer); + + /// + /// Dispose + /// + public void Dispose() + { + } + } +} \ No newline at end of file diff --git a/src/ModelContextProtocol/Logging/McpLoggingLevelExtensions.cs b/src/ModelContextProtocol/Logging/McpLoggingLevelExtensions.cs new file mode 100644 index 00000000..a9d7cf74 --- /dev/null +++ b/src/ModelContextProtocol/Logging/McpLoggingLevelExtensions.cs @@ -0,0 +1,29 @@ +using Microsoft.Extensions.Logging; +using ModelContextProtocol.Protocol.Types; + +namespace ModelContextProtocol.Logging; + + internal static class McpLoggingLevelExtensions +{ + public static LogLevel ToLogLevel(this LoggingLevel level) + => level switch + { + LoggingLevel.Emergency or LoggingLevel.Alert or LoggingLevel.Critical => LogLevel.Critical, + LoggingLevel.Error => LogLevel.Error, + LoggingLevel.Warning => LogLevel.Warning, + LoggingLevel.Notice or LoggingLevel.Info => LogLevel.Information, + LoggingLevel.Debug => LogLevel.Debug, + _ => LogLevel.None, + }; + + public static LoggingLevel ToLoggingLevel(this LogLevel level) + => level switch + { + LogLevel.Critical => LoggingLevel.Critical, + LogLevel.Error => LoggingLevel.Error, + LogLevel.Warning => LoggingLevel.Warning, + LogLevel.Information => LoggingLevel.Info, + LogLevel.Debug or LogLevel.Trace => LoggingLevel.Debug, + _ => LoggingLevel.Info, + }; +} diff --git a/src/ModelContextProtocol/Server/IMcpServer.cs b/src/ModelContextProtocol/Server/IMcpServer.cs index 7168a41c..a9743a07 100644 --- a/src/ModelContextProtocol/Server/IMcpServer.cs +++ b/src/ModelContextProtocol/Server/IMcpServer.cs @@ -28,6 +28,11 @@ public interface IMcpServer : IAsyncDisposable /// IServiceProvider? ServiceProvider { get; } + /// + /// Current Logging level + /// + LoggingLevel LoggingLevel { get; } + /// /// Adds a handler for client notifications of a specific method. /// diff --git a/src/ModelContextProtocol/Server/McpServer.cs b/src/ModelContextProtocol/Server/McpServer.cs index c2fdb074..7d9851d4 100644 --- a/src/ModelContextProtocol/Server/McpServer.cs +++ b/src/ModelContextProtocol/Server/McpServer.cs @@ -35,6 +35,10 @@ public McpServer(ITransport transport, McpServerOptions options, ILoggerFactory? _serverTransport = transport as IServerTransport; _options = options; + + // Add the MCP Logger provider to send MCP notification messages to the client when logs occur + loggerFactory?.AddProvider(new McpLoggerProvider(this)); + _logger = (ILogger?)loggerFactory?.CreateLogger() ?? NullLogger.Instance; ServerInstructions = options.ServerInstructions; ServiceProvider = serviceProvider; @@ -44,6 +48,7 @@ public McpServer(ITransport transport, McpServerOptions options, ILoggerFactory? IsInitialized = true; return Task.CompletedTask; }); + AddLoggingLevelNotificationHandler(options); SetInitializeHandler(options); SetCompletionHandler(options); @@ -65,6 +70,8 @@ public McpServer(ITransport transport, McpServerOptions options, ILoggerFactory? /// public IServiceProvider? ServiceProvider { get; } + public LoggingLevel LoggingLevel { get; private set; } + /// public override string EndpointName => $"Server ({_options.ServerInfo.Name} {_options.ServerInfo.Version}), Client ({ClientInfo?.Name} {ClientInfo?.Version})"; @@ -140,6 +147,18 @@ options.GetCompletionHandler is { } handler ? (request, ct) => Task.FromResult(new CompleteResult() { Completion = new() { Values = [], Total = 0, HasMore = false } })); } + private void AddLoggingLevelNotificationHandler(McpServerOptions options) + { + AddNotificationHandler("notifications/message", notification => + { + if (notification.Params is LoggingMessageNotificationParams loggingMessageNotificationParams) + { + LoggingLevel = loggingMessageNotificationParams.Level; + } + return Task.CompletedTask; + }); + } + private void SetResourcesHandler(McpServerOptions options) { if (options.Capabilities?.Resources is not { } resourcesCapability) diff --git a/src/ModelContextProtocol/Server/McpServerExtensions.cs b/src/ModelContextProtocol/Server/McpServerExtensions.cs index 3f746004..77a7988d 100644 --- a/src/ModelContextProtocol/Server/McpServerExtensions.cs +++ b/src/ModelContextProtocol/Server/McpServerExtensions.cs @@ -10,6 +10,22 @@ namespace ModelContextProtocol.Server; /// public static class McpServerExtensions { + /// + /// Sends a logging message notification to the client. + /// + /// The server instance that will handle the log notification request. + /// Contains the details of the log message to be sent. + /// Allows the operation to be canceled if needed. + /// Returns a task representing the asynchronous operation. + public static Task SendLogNotificationAsync(this IMcpServer server, LoggingMessageNotificationParams loggingMessageNotification, CancellationToken cancellationToken = default) + { + Throw.IfNull(server); + Throw.IfNull(loggingMessageNotification); + return server.SendRequestAsync( + new JsonRpcRequest { Method = "notifications/message", Params = loggingMessageNotification }, + cancellationToken); + } + /// /// Requests to sample an LLM via the client. ///