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
2 changed files with 253 additions and 0 deletions

View file

@ -39,6 +39,7 @@ internal static class Program
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 @@ internal static class Program
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<LoggingLevel>().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<JsonElement>("\"Random log message\"")
}
});
}
}
// Snapshot the subscribed resources, rather than locking while sending notifications
List<string> resources;
@ -266,6 +292,26 @@ internal static class Program
};
}
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<string> _subscribedResources = new();
private static readonly object _subscribedResourcesLock = new();

View file

@ -7,6 +7,9 @@ using System.Text.Json;
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 class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
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<LoggingMessageNotificationParams>(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.");