Add XunitLoggerProvider (#81)

* Add XunitLoggerProvider

* Avoid logging to stale ITestOutputHelper in fixture disposal

* Use LoggedTest base class everywhere an ILoggerFactory is used
This commit is contained in:
Stephen Halter 2025-03-25 05:53:55 -07:00 • committed by GitHub
commit 60ea96a97c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 178 additions and 86 deletions

View file

@ -10,9 +10,10 @@ namespace ModelContextProtocol.TestSseServer;
public class Program
{
private static ILoggerFactory CreateLoggerFactory()
private static ILoggerFactory CreateLoggerFactory() => LoggerFactory.Create(ConfigureSerilog);
public static void ConfigureSerilog(ILoggingBuilder loggingBuilder)
{
// Use serilog
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Verbose() // Capture all log levels
.WriteTo.File(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs", "TestServer_.log"),
@ -21,15 +22,12 @@ public class Program
.CreateLogger();
var logsPath = Path.Combine(AppContext.BaseDirectory, "testserver.log");
return LoggerFactory.Create(builder =>
{
builder.AddSerilog();
});
loggingBuilder.AddSerilog();
}
public static Task Main(string[] args) => MainAsync(args);
public static async Task MainAsync(string[] args, CancellationToken cancellationToken = default)
public static async Task MainAsync(string[] args, ILoggerFactory? loggerFactory = null, CancellationToken cancellationToken = default)
{
Console.WriteLine("Starting server...");
@ -385,7 +383,7 @@ public class Program
},
};
using var loggerFactory = CreateLoggerFactory();
loggerFactory ??= CreateLoggerFactory();
server = McpServerFactory.Create(new HttpListenerSseServerTransport("TestServer", 3001, loggerFactory), options, loggerFactory);
Console.WriteLine("Server initialized.");

View file

@ -5,9 +5,10 @@ using Microsoft.Extensions.Logging;
namespace ModelContextProtocol.Tests;
public class ClientIntegrationTestFixture : IDisposable
public class ClientIntegrationTestFixture
{
public ILoggerFactory LoggerFactory { get; }
private ILoggerFactory? _loggerFactory;
public McpClientOptions DefaultOptions { get; }
public McpServerConfig EverythingServerConfig { get; }
public McpServerConfig TestServerConfig { get; }
@ -16,10 +17,6 @@ public class ClientIntegrationTestFixture : IDisposable
public ClientIntegrationTestFixture()
{
LoggerFactory = Microsoft.Extensions.Logging.LoggerFactory.Create(builder =>
builder.AddConsole()
.SetMinimumLevel(LogLevel.Debug));
DefaultOptions = new()
{
ClientInfo = new() { Name = "IntegrationTestClient", Version = "1.0.0" },
@ -56,17 +53,16 @@ public class ClientIntegrationTestFixture : IDisposable
}
}
public void Initialize(ILoggerFactory loggerFactory)
{
_loggerFactory = loggerFactory;
}
public Task<IMcpClient> CreateClientAsync(string clientId, McpClientOptions? clientOptions = null) =>
McpClientFactory.CreateAsync(clientId switch
{
"everything" => EverythingServerConfig,
"test_server" => TestServerConfig,
_ => throw new ArgumentException($"Unknown client ID: {clientId}")
}, clientOptions ?? DefaultOptions, loggerFactory: LoggerFactory);
public void Dispose()
{
LoggerFactory?.Dispose();
GC.SuppressFinalize(this);
}
}, clientOptions ?? DefaultOptions, loggerFactory: _loggerFactory);
}

View file

@ -6,6 +6,7 @@ using ModelContextProtocol.Protocol.Messages;
using System.Text.Json;
using ModelContextProtocol.Configuration;
using ModelContextProtocol.Protocol.Transport;
using ModelContextProtocol.Tests.Utils;
using Xunit.Sdk;
using System.Text.Encodings.Web;
using System.Text.Json.Serialization.Metadata;
@ -13,15 +14,17 @@ using System.Text.Json.Serialization;
namespace ModelContextProtocol.Tests;
public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture>
public class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIntegrationTestFixture>
{
private static readonly string? s_openAIKey = Environment.GetEnvironmentVariable("AI:OpenAI:ApiKey")!;
private readonly ClientIntegrationTestFixture _fixture;
public ClientIntegrationTests(ClientIntegrationTestFixture fixture)
public ClientIntegrationTests(ClientIntegrationTestFixture fixture, ITestOutputHelper testOutputHelper)
: base(testOutputHelper)
{
_fixture = fixture;
_fixture.Initialize(LoggerFactory);
}
public static IEnumerable<object[]> GetClients() =>
@ -474,7 +477,7 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
await using var client = await McpClientFactory.CreateAsync(
serverConfig,
clientOptions,
loggerFactory: _fixture.LoggerFactory,
loggerFactory: LoggerFactory,
cancellationToken: TestContext.Current.CancellationToken);
// act

View file

@ -1,18 +1,18 @@
using ModelContextProtocol.Protocol.Transport;
using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Server;
using Microsoft.Extensions.Logging.Abstractions;
using ModelContextProtocol.Tests.Utils;
using Moq;
namespace ModelContextProtocol.Tests.Server;
public class McpServerFactoryTests
public class McpServerFactoryTests : LoggedTest
{
private readonly Mock<IServerTransport> _serverTransport;
private readonly McpServerOptions _options;
private readonly IServiceProvider _serviceProvider;
public McpServerFactoryTests()
public McpServerFactoryTests(ITestOutputHelper testOutputHelper)
: base(testOutputHelper)
{
_serverTransport = new Mock<IServerTransport>();
_options = new McpServerOptions
@ -21,14 +21,13 @@ public class McpServerFactoryTests
ProtocolVersion = "1.0",
InitializationTimeout = TimeSpan.FromSeconds(30)
};
_serviceProvider = new Mock<IServiceProvider>().Object;
}
[Fact]
public async Task Create_Should_Initialize_With_Valid_Parameters()
{
// Arrange & Act
await using IMcpServer server = McpServerFactory.Create(_serverTransport.Object, _options, NullLoggerFactory.Instance);
await using IMcpServer server = McpServerFactory.Create(_serverTransport.Object, _options, LoggerFactory);
// Assert
Assert.NotNull(server);
@ -38,13 +37,13 @@ public class McpServerFactoryTests
public void Constructor_Throws_For_Null_ServerTransport()
{
// Arrange, Act & Assert
Assert.Throws<ArgumentNullException>("serverTransport", () => McpServerFactory.Create(null!, _options, NullLoggerFactory.Instance));
Assert.Throws<ArgumentNullException>("serverTransport", () => McpServerFactory.Create(null!, _options, LoggerFactory));
}
[Fact]
public void Constructor_Throws_For_Null_Options()
{
// Arrange, Act & Assert
Assert.Throws<ArgumentNullException>("serverOptions", () => McpServerFactory.Create(_serverTransport.Object, null!, NullLoggerFactory.Instance));
Assert.Throws<ArgumentNullException>("serverOptions", () => McpServerFactory.Create(_serverTransport.Object, null!, LoggerFactory));
}
}

View file

@ -11,20 +11,18 @@ using System.Reflection;
namespace ModelContextProtocol.Tests.Server;
public class McpServerTests
public class McpServerTests : LoggedTest
{
private readonly Mock<IServerTransport> _serverTransport;
private readonly Mock<ILoggerFactory> _loggerFactory;
private readonly Mock<ILogger> _logger;
private readonly McpServerOptions _options;
private readonly IServiceProvider _serviceProvider;
public McpServerTests()
public McpServerTests(ITestOutputHelper testOutputHelper)
: base(testOutputHelper)
{
_serverTransport = new Mock<IServerTransport>();
_loggerFactory = new Mock<ILoggerFactory>();
_logger = new Mock<ILogger>();
_loggerFactory.Setup(f => f.CreateLogger(It.IsAny<string>())).Returns(_logger.Object);
_options = CreateOptions();
_serviceProvider = new Mock<IServiceProvider>().Object;
}
@ -44,7 +42,7 @@ public class McpServerTests
public async Task Constructor_Should_Initialize_With_Valid_Parameters()
{
// Arrange & Act
await using var server = McpServerFactory.Create(_serverTransport.Object, _options, _loggerFactory.Object, _serviceProvider);
await using var server = McpServerFactory.Create(_serverTransport.Object, _options, LoggerFactory, _serviceProvider);
// Assert
Assert.NotNull(server);
@ -54,14 +52,14 @@ public class McpServerTests
public void Constructor_Throws_For_Null_Transport()
{
// Arrange, Act & Assert
Assert.Throws<ArgumentNullException>(() => McpServerFactory.Create(null!, _options, _loggerFactory.Object, _serviceProvider));
Assert.Throws<ArgumentNullException>(() => McpServerFactory.Create(null!, _options, LoggerFactory, _serviceProvider));
}
[Fact]
public void Constructor_Throws_For_Null_Options()
{
// Arrange, Act & Assert
Assert.Throws<ArgumentNullException>(() => McpServerFactory.Create(_serverTransport.Object, null!, _loggerFactory.Object, _serviceProvider));
Assert.Throws<ArgumentNullException>(() => McpServerFactory.Create(_serverTransport.Object, null!, LoggerFactory, _serviceProvider));
}
[Fact]
@ -78,7 +76,7 @@ public class McpServerTests
public async Task Constructor_Does_Not_Throw_For_Null_ServiceProvider()
{
// Arrange & Act
await using var server = McpServerFactory.Create(_serverTransport.Object, _options, _loggerFactory.Object, null);
await using var server = McpServerFactory.Create(_serverTransport.Object, _options, LoggerFactory, null);
// Assert
Assert.NotNull(server);
@ -88,7 +86,7 @@ public class McpServerTests
public async Task StartAsync_Should_Throw_InvalidOperationException_If_Already_Initializing()
{
// Arrange
await using var server = McpServerFactory.Create(_serverTransport.Object, _options, _loggerFactory.Object, _serviceProvider);
await using var server = McpServerFactory.Create(_serverTransport.Object, _options, LoggerFactory, _serviceProvider);
var task = server.StartAsync(TestContext.Current.CancellationToken);
// Act & Assert
@ -101,7 +99,7 @@ public class McpServerTests
public async Task StartAsync_Should_Do_Nothing_If_Already_Initialized()
{
// Arrange
await using var server = McpServerFactory.Create(_serverTransport.Object, _options, _loggerFactory.Object, _serviceProvider);
await using var server = McpServerFactory.Create(_serverTransport.Object, _options, LoggerFactory, _serviceProvider);
SetInitialized(server, true);
await server.StartAsync(TestContext.Current.CancellationToken);
@ -114,7 +112,7 @@ public class McpServerTests
public async Task StartAsync_ShouldStartListening()
{
// Arrange
await using var server = McpServerFactory.Create(_serverTransport.Object, _options, _loggerFactory.Object, _serviceProvider);
await using var server = McpServerFactory.Create(_serverTransport.Object, _options, LoggerFactory, _serviceProvider);
// Act
await server.StartAsync(TestContext.Current.CancellationToken);
@ -127,7 +125,7 @@ public class McpServerTests
public async Task StartAsync_Sets_Initialized_After_Transport_Responses_Initialized_Notification()
{
await using var transport = new TestServerTransport();
await using var server = McpServerFactory.Create(transport, _options, _loggerFactory.Object, _serviceProvider);
await using var server = McpServerFactory.Create(transport, _options, LoggerFactory, _serviceProvider);
await server.StartAsync(TestContext.Current.CancellationToken);
@ -147,7 +145,7 @@ public class McpServerTests
public async Task RequestSamplingAsync_Should_Throw_McpServerException_If_Client_Does_Not_Support_Sampling()
{
// Arrange
await using var server = McpServerFactory.Create(_serverTransport.Object, _options, _loggerFactory.Object, _serviceProvider);
await using var server = McpServerFactory.Create(_serverTransport.Object, _options, LoggerFactory, _serviceProvider);
SetClientCapabilities(server, new ClientCapabilities());
var action = () => server.RequestSamplingAsync(new CreateMessageRequestParams { Messages = [] }, CancellationToken.None);
@ -161,7 +159,7 @@ public class McpServerTests
{
// Arrange
await using var transport = new TestServerTransport();
await using var server = McpServerFactory.Create(transport, _options, _loggerFactory.Object, _serviceProvider);
await using var server = McpServerFactory.Create(transport, _options, LoggerFactory, _serviceProvider);
SetClientCapabilities(server, new ClientCapabilities { Sampling = new SamplingCapability() });
await server.StartAsync(TestContext.Current.CancellationToken);
@ -179,7 +177,7 @@ public class McpServerTests
public async Task RequestRootsAsync_Should_Throw_McpServerException_If_Client_Does_Not_Support_Roots()
{
// Arrange
await using var server = McpServerFactory.Create(_serverTransport.Object, _options, _loggerFactory.Object, _serviceProvider);
await using var server = McpServerFactory.Create(_serverTransport.Object, _options, LoggerFactory, _serviceProvider);
SetClientCapabilities(server, new ClientCapabilities());
// Act & Assert
@ -191,7 +189,7 @@ public class McpServerTests
{
// Arrange
await using var transport = new TestServerTransport();
await using var server = McpServerFactory.Create(transport, _options, _loggerFactory.Object, _serviceProvider);
await using var server = McpServerFactory.Create(transport, _options, LoggerFactory, _serviceProvider);
SetClientCapabilities(server, new ClientCapabilities { Roots = new RootsCapability() });
await server.StartAsync(TestContext.Current.CancellationToken);
@ -208,7 +206,7 @@ public class McpServerTests
[Fact]
public async Task Throws_Exception_If_Not_Connected()
{
await using var server = McpServerFactory.Create(_serverTransport.Object, _options, _loggerFactory.Object, _serviceProvider);
await using var server = McpServerFactory.Create(_serverTransport.Object, _options, LoggerFactory, _serviceProvider);
SetClientCapabilities(server, new ClientCapabilities { Roots = new RootsCapability() });
_serverTransport.SetupGet(t => t.IsConnected).Returns(false);
@ -555,7 +553,7 @@ public class McpServerTests
var options = CreateOptions(serverCapabilities);
configureOptions?.Invoke(options);
await using var server = McpServerFactory.Create(transport, options, _loggerFactory.Object, _serviceProvider);
await using var server = McpServerFactory.Create(transport, options, LoggerFactory, _serviceProvider);
await server.StartAsync();
@ -587,7 +585,7 @@ public class McpServerTests
await using var transport = new TestServerTransport();
var options = CreateOptions(serverCapabilities);
Assert.Throws<McpServerException>(() => McpServerFactory.Create(transport, options, _loggerFactory.Object, _serviceProvider));
Assert.Throws<McpServerException>(() => McpServerFactory.Create(transport, options, LoggerFactory, _serviceProvider));
}
[Fact]

View file

@ -2,23 +2,29 @@
using ModelContextProtocol.Client;
using ModelContextProtocol.Configuration;
using ModelContextProtocol.Protocol.Transport;
using ModelContextProtocol.Test.Utils;
using ModelContextProtocol.TestSseServer;
namespace ModelContextProtocol.Tests;
public class SseServerIntegrationTestFixture : IAsyncDisposable
{
private readonly CancellationTokenSource _stopCts = new();
private readonly Task _serverTask;
private readonly CancellationTokenSource _stopCts = new();
private readonly DelegatingTestOutputHelper _delegatingTestOutputHelper = new();
private readonly ILoggerFactory _redirectingLoggerFactory;
public ILoggerFactory LoggerFactory { get; }
public McpClientOptions DefaultOptions { get; }
public McpServerConfig DefaultConfig { get; }
public SseServerIntegrationTestFixture()
{
LoggerFactory = Microsoft.Extensions.Logging.LoggerFactory.Create(builder =>
builder.AddConsole()
.SetMinimumLevel(LogLevel.Debug));
_redirectingLoggerFactory = LoggerFactory.Create(builder =>
{
Program.ConfigureSerilog(builder);
builder.AddProvider(new XunitLoggerProvider(_delegatingTestOutputHelper));
});
DefaultOptions = new()
{
@ -34,12 +40,17 @@ public class SseServerIntegrationTestFixture : IAsyncDisposable
Location = "http://localhost:3001/sse"
};
_serverTask = TestSseServer.Program.MainAsync([], _stopCts.Token);
_serverTask = Program.MainAsync([], _redirectingLoggerFactory, _stopCts.Token);
}
public void Initialize(ITestOutputHelper output)
{
_delegatingTestOutputHelper.CurrentTestOutputHelper = output;
}
public async ValueTask DisposeAsync()
{
LoggerFactory.Dispose();
_delegatingTestOutputHelper.CurrentTestOutputHelper = null;
_stopCts.Cancel();
try
{
@ -48,6 +59,19 @@ public class SseServerIntegrationTestFixture : IAsyncDisposable
catch (OperationCanceledException)
{
}
_redirectingLoggerFactory.Dispose();
_stopCts.Dispose();
}
private class DelegatingTestOutputHelper() : ITestOutputHelper
{
public ITestOutputHelper? CurrentTestOutputHelper { get; set; }
public string Output => CurrentTestOutputHelper?.Output ?? string.Empty;
public void Write(string message) => CurrentTestOutputHelper?.Write(message);
public void Write(string format, params object[] args) => CurrentTestOutputHelper?.Write(format, args);
public void WriteLine(string message) => CurrentTestOutputHelper?.WriteLine(message);
public void WriteLine(string format, params object[] args) => CurrentTestOutputHelper?.WriteLine(format, args);
}
}

View file

@ -1,15 +1,18 @@
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Tests.Utils;
namespace ModelContextProtocol.Tests;
public class SseServerIntegrationTests : IClassFixture<SseServerIntegrationTestFixture>
public class SseServerIntegrationTests : LoggedTest, IClassFixture<SseServerIntegrationTestFixture>
{
private readonly SseServerIntegrationTestFixture _fixture;
public SseServerIntegrationTests(SseServerIntegrationTestFixture fixture)
public SseServerIntegrationTests(SseServerIntegrationTestFixture fixture, ITestOutputHelper testOutputHelper)
: base(testOutputHelper)
{
_fixture = fixture;
_fixture.Initialize(testOutputHelper);
}
private Task<IMcpClient> GetClientAsync(McpClientOptions? options = null)
@ -17,7 +20,7 @@ public class SseServerIntegrationTests : IClassFixture<SseServerIntegrationTestF
return McpClientFactory.CreateAsync(
_fixture.DefaultConfig,
options ?? _fixture.DefaultOptions,
loggerFactory: _fixture.LoggerFactory);
loggerFactory: LoggerFactory);
}
[Fact]

View file

@ -1,5 +1,4 @@
using Microsoft.Extensions.Logging.Abstractions;
using ModelContextProtocol.Configuration;
using ModelContextProtocol.Configuration;
using ModelContextProtocol.Protocol.Messages;
using ModelContextProtocol.Protocol.Transport;
using ModelContextProtocol.Tests.Utils;
@ -8,12 +7,13 @@ using System.Reflection;
namespace ModelContextProtocol.Tests.Transport;
public class SseClientTransportTests
public class SseClientTransportTests : LoggedTest
{
private readonly McpServerConfig _serverConfig;
private readonly SseClientTransportOptions _transportOptions;
public SseClientTransportTests()
public SseClientTransportTests(ITestOutputHelper testOutputHelper)
: base(testOutputHelper)
{
_serverConfig = new McpServerConfig
{
@ -39,7 +39,7 @@ public class SseClientTransportTests
public async Task Constructor_Should_Initialize_With_Valid_Parameters()
{
// Act
await using var transport = new SseClientTransport(_transportOptions, _serverConfig, NullLoggerFactory.Instance);
await using var transport = new SseClientTransport(_transportOptions, _serverConfig, LoggerFactory);
// Assert
Assert.NotNull(transport);
@ -58,21 +58,21 @@ public class SseClientTransportTests
[Fact]
public void Constructor_Throws_For_Null_Options()
{
var exception = Assert.Throws<ArgumentNullException>(() => new SseClientTransport(null!, _serverConfig, NullLoggerFactory.Instance));
var exception = Assert.Throws<ArgumentNullException>(() => new SseClientTransport(null!, _serverConfig, LoggerFactory));
Assert.Equal("transportOptions", exception.ParamName);
}
[Fact]
public void Constructor_Throws_For_Null_Config()
{
var exception = Assert.Throws<ArgumentNullException>(() => new SseClientTransport(_transportOptions, null!, NullLoggerFactory.Instance));
var exception = Assert.Throws<ArgumentNullException>(() => new SseClientTransport(_transportOptions, null!, LoggerFactory));
Assert.Equal("serverConfig", exception.ParamName);
}
[Fact]
public void Constructor_Throws_For_Null_HttpClientg()
{
var exception = Assert.Throws<ArgumentNullException>(() => new SseClientTransport(_transportOptions, _serverConfig, null!, NullLoggerFactory.Instance));
var exception = Assert.Throws<ArgumentNullException>(() => new SseClientTransport(_transportOptions, _serverConfig, null!, LoggerFactory));
Assert.Equal("httpClient", exception.ParamName);
}
@ -81,7 +81,7 @@ public class SseClientTransportTests
{
using var mockHttpHandler = new MockHttpHandler();
using var httpClient = new HttpClient(mockHttpHandler);
await using var transport = new SseClientTransport(_transportOptions, _serverConfig, httpClient, NullLoggerFactory.Instance);
await using var transport = new SseClientTransport(_transportOptions, _serverConfig, httpClient, LoggerFactory);
bool firstCall = true;
@ -109,7 +109,7 @@ public class SseClientTransportTests
{
using var mockHttpHandler = new MockHttpHandler();
using var httpClient = new HttpClient(mockHttpHandler);
await using var transport = new SseClientTransport(_transportOptions, _serverConfig, httpClient, NullLoggerFactory.Instance);
await using var transport = new SseClientTransport(_transportOptions, _serverConfig, httpClient, LoggerFactory);
var tcsConnected = new TaskCompletionSource();
var tcsDone = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var callIndex = 0;
@ -157,7 +157,7 @@ public class SseClientTransportTests
{
using var mockHttpHandler = new MockHttpHandler();
using var httpClient = new HttpClient(mockHttpHandler);
await using var transport = new SseClientTransport(_transportOptions, _serverConfig, httpClient, NullLoggerFactory.Instance);
await using var transport = new SseClientTransport(_transportOptions, _serverConfig, httpClient, LoggerFactory);
var retries = 0;
mockHttpHandler.RequestHandler = (request) =>
@ -177,7 +177,7 @@ public class SseClientTransportTests
[Fact]
public async Task SendMessageAsync_Throws_Exception_If_MessageEndpoint_Not_Set()
{
await using var transport = new SseClientTransport(_transportOptions, _serverConfig, NullLoggerFactory.Instance);
await using var transport = new SseClientTransport(_transportOptions, _serverConfig, LoggerFactory);
// Assert
await Assert.ThrowsAsync<InvalidOperationException>(() => transport.SendMessageAsync(new JsonRpcRequest() { Method = "test" }, CancellationToken.None));
@ -188,7 +188,7 @@ public class SseClientTransportTests
{
using var mockHttpHandler = new MockHttpHandler();
using var httpClient = new HttpClient(mockHttpHandler);
await using var transport = new SseClientTransport(_transportOptions, _serverConfig, httpClient, NullLoggerFactory.Instance);
await using var transport = new SseClientTransport(_transportOptions, _serverConfig, httpClient, LoggerFactory);
var firstCall = true;
mockHttpHandler.RequestHandler = (request) =>
@ -227,7 +227,7 @@ public class SseClientTransportTests
{
using var mockHttpHandler = new MockHttpHandler();
using var httpClient = new HttpClient(mockHttpHandler);
await using var transport = new SseClientTransport(_transportOptions, _serverConfig, httpClient, NullLoggerFactory.Instance);
await using var transport = new SseClientTransport(_transportOptions, _serverConfig, httpClient, LoggerFactory);
var firstCall = true;
mockHttpHandler.RequestHandler = (request) =>
@ -266,7 +266,7 @@ public class SseClientTransportTests
{
using var mockHttpHandler = new MockHttpHandler();
using var httpClient = new HttpClient(mockHttpHandler);
await using var transport = new SseClientTransport(_transportOptions, _serverConfig, httpClient, NullLoggerFactory.Instance);
await using var transport = new SseClientTransport(_transportOptions, _serverConfig, httpClient, LoggerFactory);
var callIndex = 0;
mockHttpHandler.RequestHandler = (request) =>
@ -303,7 +303,7 @@ public class SseClientTransportTests
[Fact]
public async Task CloseAsync_Should_Dispose_Resources()
{
await using var transport = new SseClientTransport(_transportOptions, _serverConfig, NullLoggerFactory.Instance);
await using var transport = new SseClientTransport(_transportOptions, _serverConfig, LoggerFactory);
await transport.CloseAsync();
@ -313,7 +313,7 @@ public class SseClientTransportTests
[Fact]
public async Task DisposeAsync_Should_Dispose_Resources()
{
await using var transport = new SseClientTransport(_transportOptions, _serverConfig, NullLoggerFactory.Instance);
await using var transport = new SseClientTransport(_transportOptions, _serverConfig, LoggerFactory);
await transport.DisposeAsync();

View file

@ -1,8 +1,8 @@
using Microsoft.Extensions.Logging.Abstractions;
using ModelContextProtocol.Protocol.Messages;
using ModelContextProtocol.Protocol.Messages;
using ModelContextProtocol.Protocol.Transport;
using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Server;
using ModelContextProtocol.Tests.Utils;
using ModelContextProtocol.Utils.Json;
using System.IO.Pipelines;
using System.Text;
@ -10,11 +10,12 @@ using System.Text.Json;
namespace ModelContextProtocol.Tests.Transport;
public class StdioServerTransportTests
public class StdioServerTransportTests : LoggedTest
{
private readonly McpServerOptions _serverOptions;
public StdioServerTransportTests()
public StdioServerTransportTests(ITestOutputHelper testOutputHelper)
: base(testOutputHelper)
{
_serverOptions = new McpServerOptions
{
@ -68,7 +69,7 @@ public class StdioServerTransportTests
_serverOptions.ServerInfo.Name,
new Pipe().Reader.AsStream(),
output,
NullLoggerFactory.Instance);
LoggerFactory);
await transport.StartListeningAsync(TestContext.Current.CancellationToken);
@ -122,7 +123,7 @@ public class StdioServerTransportTests
_serverOptions.ServerInfo.Name,
input,
Stream.Null,
NullLoggerFactory.Instance);
LoggerFactory);
await transport.StartListeningAsync(TestContext.Current.CancellationToken);
@ -165,7 +166,7 @@ public class StdioServerTransportTests
_serverOptions.ServerInfo.Name,
new Pipe().Reader.AsStream(),
output,
NullLoggerFactory.Instance);
LoggerFactory);
await transport.StartListeningAsync(TestContext.Current.CancellationToken);

View file

@ -0,0 +1,18 @@
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Test.Utils;
namespace ModelContextProtocol.Tests.Utils;
public class LoggedTest(ITestOutputHelper testOutputHelper)
{
public ITestOutputHelper TestOutputHelper { get; } = testOutputHelper;
public ILoggerFactory LoggerFactory { get; } = CreateLoggerFactory(testOutputHelper);
private static ILoggerFactory CreateLoggerFactory(ITestOutputHelper testOutputHelper)
{
return Microsoft.Extensions.Logging.LoggerFactory.Create(builder =>
{
builder.AddProvider(new XunitLoggerProvider(testOutputHelper));
});
}
}

View file

@ -0,0 +1,52 @@
using System.Globalization;
using System.Text;
using Microsoft.Extensions.Logging;
namespace ModelContextProtocol.Test.Utils;
public class XunitLoggerProvider(ITestOutputHelper output) : ILoggerProvider
{
public ILogger CreateLogger(string categoryName)
{
return new XunitLogger(output, categoryName);
}
public void Dispose()
{
}
private class XunitLogger(ITestOutputHelper output, string category) : ILogger
{
public void Log<TState>(
LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
var sb = new StringBuilder();
var timestamp = DateTimeOffset.UtcNow.ToString("s", CultureInfo.InvariantCulture);
var prefix = $"| [{timestamp}] {category} {logLevel}: ";
var lines = formatter(state, exception);
sb.Append(prefix);
sb.Append(lines);
if (exception is not null)
{
sb.AppendLine();
sb.Append(exception.ToString());
}
output.WriteLine(sb.ToString());
}
public bool IsEnabled(LogLevel logLevel) => true;
public IDisposable BeginScope<TState>(TState state) where TState : notnull
=> new NoopDisposable();
private sealed class NoopDisposable : IDisposable
{
public void Dispose()
{
}
}
}
}