Logging Capability (#53)

* Logging Capability

* Readability

* Redundant using

---------

Co-authored-by: Stephen Toub <stoub@microsoft.com>
This commit is contained in:
Peder Holdgaard Pedersen 2025-03-21 22:50:41 +01:00 • committed by GitHub
commit 10c712a12f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 253 additions and 0 deletions

View file

@ -474,6 +474,21 @@ public static class McpClientExtensions
};
}
/// <summary>
/// Configures the minimum logging level for the server.
/// </summary>
/// <param name="client">The client.</param>
/// <param name="level">The minimum log level of messages to be generated.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
public static Task SetLoggingLevel(this IMcpClient client, LoggingLevel level, CancellationToken cancellationToken = default)
{
Throw.IfNull(client);
return client.SendRequestAsync<EmptyResult>(
CreateRequest("logging/setLevel", new() { ["level"] = level.ToJsonValue() }),
cancellationToken);
}
private static JsonRpcRequest CreateRequest(string method, Dictionary<string, object?>? parameters) =>
new()
{
@ -499,6 +514,22 @@ public static class McpClientExtensions
return parameters;
}
private static string ToJsonValue(this LoggingLevel level)
{
return level switch
{
LoggingLevel.Debug => "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))
};
}
/// <summary>Provides an AI function that calls a tool through <see cref="IMcpClient"/>.</summary>
private sealed class McpAIFunction(IMcpClient client, Tool tool) : AIFunction
{

View file

@ -29,4 +29,9 @@ public static class NotificationMethods
/// Sent by the client when roots have been updated.
/// </summary>
public const string RootsUpdatedNotification = "notifications/roots/list_changed";
/// <summary>
/// Sent by the server when a log message is generated.
/// </summary>
public const string LoggingMessageNotification = "notifications/message";
}

View file

@ -65,6 +65,13 @@ public record SamplingCapability
public record LoggingCapability
{
// Currently empty in the spec, but may be extended in the future
/// <summary>
/// Gets or sets the handler for set logging level requests.
/// </summary>
[JsonIgnore]
public Func<RequestContext<SetLevelRequestParams>, CancellationToken, Task<EmptyResult>>? SetLoggingLevelHandler { get; init; }
}
/// <summary>

View file

@ -0,0 +1,43 @@
using System.Text.Json.Serialization;
namespace ModelContextProtocol.Protocol.Types;
/// <summary>
/// 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
/// </summary>
public enum LoggingLevel
{
/// <summary>Detailed debug information, typically only valuable to developers.</summary>
[JsonPropertyName("debug")]
Debug,
/// <summary>Normal operational messages that require no action.</summary>
[JsonPropertyName("info")]
Info,
/// <summary>Normal but significant events that might deserve attention.</summary>
[JsonPropertyName("notice")]
Notice,
/// <summary>Warning conditions that don't represent an error but indicate potential issues.</summary>
[JsonPropertyName("warning")]
Warning,
/// <summary>Error conditions that should be addressed but don't require immediate action.</summary>
[JsonPropertyName("error")]
Error,
/// <summary>Critical conditions that require immediate attention.</summary>
[JsonPropertyName("critical")]
Critical,
/// <summary>Action must be taken immediately to address the condition.</summary>
[JsonPropertyName("alert")]
Alert,
/// <summary>System is unusable and requires immediate attention.</summary>
[JsonPropertyName("emergency")]
Emergency
}

View file

@ -0,0 +1,31 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace ModelContextProtocol.Protocol.Types;
/// <summary>
/// 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 href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
/// </summary>
public class LoggingMessageNotificationParams
{
/// <summary>
/// The severity of this log message.
/// </summary>
[JsonPropertyName("level")]
public LoggingLevel Level { get; init; }
/// <summary>
/// An optional name of the logger issuing this message.
/// </summary>
[JsonPropertyName("logger")]
public string? Logger { get; init; }
/// <summary>
/// The data to be logged, such as a string message or an object. Any JSON serializable type is allowed here.
/// </summary>
[JsonPropertyName("data")]
public JsonElement? Data { get; init; }
}

View file

@ -0,0 +1,17 @@
using System.Text.Json.Serialization;
namespace ModelContextProtocol.Protocol.Types;
/// <summary>
/// A request from the client to the server, to enable or adjust logging.
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
/// </summary>
public class SetLevelRequestParams
{
/// <summary>
/// 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.
/// </summary>
[JsonPropertyName("level")]
public required LoggingLevel Level { get; init; }
}

View file

@ -51,6 +51,7 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
SetToolsHandler(options);
SetPromptsHandler(options);
SetResourcesHandler(options);
SetSetLoggingLevelHandler(options);
}
public ClientCapabilities? ClientCapabilities { get; set; }
@ -210,4 +211,19 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
SetRequestHandler<ListToolsRequestParams, ListToolsResult>("tools/list", (request, ct) => listToolsHandler(new(this, request), ct));
SetRequestHandler<CallToolRequestParams, CallToolResponse>("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<SetLevelRequestParams, EmptyResult>("logging/setLevel", (request, ct) => setLoggingLevelHandler(new(this, request), ct));
}
}

View file

@ -57,6 +57,11 @@ public sealed class McpServerHandlers
/// </summary>
public Func<RequestContext<UnsubscribeRequestParams>, CancellationToken, Task<EmptyResult>>? UnsubscribeFromResourcesHandler { get; set; }
/// <summary>
/// Get or sets the handler for set logging level requests.
/// </summary>
public Func<RequestContext<SetLevelRequestParams>, CancellationToken, Task<EmptyResult>>? SetLoggingLevelHandler { get; set; }
/// <summary>
/// Overwrite any handlers in McpServerOptions with non-null handlers from this instance.
/// </summary>
@ -125,6 +130,20 @@ public sealed class McpServerHandlers
};
}
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()
{