Enable servers to log to the clients via ILogger (#229)
* Enable servers to log to the client via ILogger * Address feedback
This commit is contained in:
parent
3a2e9a228d
commit
3b83ad4103
2 changed files with 242 additions and 18 deletions
|
|
@ -1,5 +1,6 @@
|
|||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ModelContextProtocol.Client;
|
||||
using ModelContextProtocol.Protocol.Messages;
|
||||
using ModelContextProtocol.Protocol.Transport;
|
||||
|
|
@ -10,6 +11,7 @@ using Moq;
|
|||
using System.IO.Pipelines;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using System.Threading.Channels;
|
||||
|
||||
namespace ModelContextProtocol.Tests.Client;
|
||||
|
||||
|
|
@ -19,6 +21,7 @@ public class McpClientExtensionsTests : LoggedTest
|
|||
private readonly Pipe _serverToClientPipe = new();
|
||||
private readonly ServiceProvider _serviceProvider;
|
||||
private readonly CancellationTokenSource _cts;
|
||||
private readonly IMcpServer _server;
|
||||
private readonly Task _serverTask;
|
||||
|
||||
public McpClientExtensionsTests(ITestOutputHelper outputHelper)
|
||||
|
|
@ -36,9 +39,9 @@ public class McpClientExtensionsTests : LoggedTest
|
|||
sc.AddSingleton(McpServerTool.Create([McpServerTool(Destructive = false, OpenWorld = true)](string i) => $"{i} Result", new() { Name = "ValuesSetViaOptions", Destructive = true, OpenWorld = false, ReadOnly = true }));
|
||||
_serviceProvider = sc.BuildServiceProvider();
|
||||
|
||||
var server = _serviceProvider.GetRequiredService<IMcpServer>();
|
||||
_server = _serviceProvider.GetRequiredService<IMcpServer>();
|
||||
_cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
|
||||
_serverTask = server.RunAsync(cancellationToken: _cts.Token);
|
||||
_serverTask = _server.RunAsync(cancellationToken: _cts.Token);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
@ -374,4 +377,104 @@ public class McpClientExtensionsTests : LoggedTest
|
|||
Assert.Equal("ToolWithNewDescription", redescribedTool.Description);
|
||||
Assert.Equal(originalDescription, tool?.Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsClientLoggerProvider_MessagesSentToClient()
|
||||
{
|
||||
IMcpClient client = await CreateMcpClientForServer();
|
||||
|
||||
ILoggerProvider loggerProvider = _server.AsClientLoggerProvider();
|
||||
Assert.Throws<ArgumentNullException>("categoryName", () => loggerProvider.CreateLogger(null!));
|
||||
|
||||
ILogger logger = loggerProvider.CreateLogger("TestLogger");
|
||||
Assert.NotNull(logger);
|
||||
|
||||
Assert.Null(logger.BeginScope(""));
|
||||
|
||||
Assert.Null(_server.LoggingLevel);
|
||||
Assert.False(logger.IsEnabled(LogLevel.Trace));
|
||||
Assert.False(logger.IsEnabled(LogLevel.Debug));
|
||||
Assert.False(logger.IsEnabled(LogLevel.Information));
|
||||
Assert.False(logger.IsEnabled(LogLevel.Warning));
|
||||
Assert.False(logger.IsEnabled(LogLevel.Error));
|
||||
Assert.False(logger.IsEnabled(LogLevel.Critical));
|
||||
|
||||
await client.SetLoggingLevel(LoggingLevel.Info, TestContext.Current.CancellationToken);
|
||||
|
||||
DateTime start = DateTime.UtcNow;
|
||||
while (_server.LoggingLevel is null)
|
||||
{
|
||||
await Task.Delay(1, TestContext.Current.CancellationToken);
|
||||
Assert.True(DateTime.UtcNow - start < TimeSpan.FromSeconds(10), "Timed out waiting for logging level to be set");
|
||||
}
|
||||
|
||||
Assert.Equal(LoggingLevel.Info, _server.LoggingLevel);
|
||||
Assert.False(logger.IsEnabled(LogLevel.Trace));
|
||||
Assert.False(logger.IsEnabled(LogLevel.Debug));
|
||||
Assert.True(logger.IsEnabled(LogLevel.Information));
|
||||
Assert.True(logger.IsEnabled(LogLevel.Warning));
|
||||
Assert.True(logger.IsEnabled(LogLevel.Error));
|
||||
Assert.True(logger.IsEnabled(LogLevel.Critical));
|
||||
|
||||
List<string> data = [];
|
||||
var channel = Channel.CreateUnbounded<LoggingMessageNotificationParams?>();
|
||||
|
||||
await using (client.RegisterNotificationHandler(NotificationMethods.LoggingMessageNotification,
|
||||
(notification, cancellationToken) =>
|
||||
{
|
||||
Assert.True(channel.Writer.TryWrite(JsonSerializer.Deserialize<LoggingMessageNotificationParams>(notification.Params)));
|
||||
return Task.CompletedTask;
|
||||
}))
|
||||
{
|
||||
logger.LogTrace("Trace {Message}", "message");
|
||||
logger.LogDebug("Debug {Message}", "message");
|
||||
logger.LogInformation("Information {Message}", "message");
|
||||
logger.LogWarning("Warning {Message}", "message");
|
||||
logger.LogError("Error {Message}", "message");
|
||||
logger.LogCritical("Critical {Message}", "message");
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
var m = await channel.Reader.ReadAsync(TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(m);
|
||||
Assert.NotNull(m.Data);
|
||||
|
||||
Assert.Equal("TestLogger", m.Logger);
|
||||
|
||||
string ? s = JsonSerializer.Deserialize<string>(m.Data.Value);
|
||||
Assert.NotNull(s);
|
||||
|
||||
if (s.Contains("Information"))
|
||||
{
|
||||
Assert.Equal(LoggingLevel.Info, m.Level);
|
||||
}
|
||||
else if (s.Contains("Warning"))
|
||||
{
|
||||
Assert.Equal(LoggingLevel.Warning, m.Level);
|
||||
}
|
||||
else if (s.Contains("Error"))
|
||||
{
|
||||
Assert.Equal(LoggingLevel.Error, m.Level);
|
||||
}
|
||||
else if (s.Contains("Critical"))
|
||||
{
|
||||
Assert.Equal(LoggingLevel.Critical, m.Level);
|
||||
}
|
||||
|
||||
data.Add(s);
|
||||
}
|
||||
|
||||
channel.Writer.Complete();
|
||||
}
|
||||
|
||||
Assert.False(await channel.Reader.WaitToReadAsync(TestContext.Current.CancellationToken));
|
||||
Assert.Equal(
|
||||
[
|
||||
"Critical message",
|
||||
"Error message",
|
||||
"Information message",
|
||||
"Warning message",
|
||||
],
|
||||
data.OrderBy(s => s));
|
||||
}
|
||||
}
|
||||
|
|
@ -618,6 +618,7 @@ public class McpServerTests : LoggedTest
|
|||
public Implementation? ClientInfo => throw new NotImplementedException();
|
||||
public McpServerOptions ServerOptions => throw new NotImplementedException();
|
||||
public IServiceProvider? Services => throw new NotImplementedException();
|
||||
public LoggingLevel? LoggingLevel => throw new NotImplementedException();
|
||||
public Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default) =>
|
||||
throw new NotImplementedException();
|
||||
public Task RunAsync(CancellationToken cancellationToken = default) =>
|
||||
|
|
|
|||
Loading…
Reference in a new issue