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 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() Log.Logger = new LoggerConfiguration()
.MinimumLevel.Verbose() // Capture all log levels .MinimumLevel.Verbose() // Capture all log levels
.WriteTo.File(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs", "TestServer_.log"), .WriteTo.File(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs", "TestServer_.log"),
@ -21,15 +22,12 @@ public class Program
.CreateLogger(); .CreateLogger();
var logsPath = Path.Combine(AppContext.BaseDirectory, "testserver.log"); var logsPath = Path.Combine(AppContext.BaseDirectory, "testserver.log");
return LoggerFactory.Create(builder => loggingBuilder.AddSerilog();
{
builder.AddSerilog();
});
} }
public static Task Main(string[] args) => MainAsync(args); 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..."); 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); server = McpServerFactory.Create(new HttpListenerSseServerTransport("TestServer", 3001, loggerFactory), options, loggerFactory);
Console.WriteLine("Server initialized."); Console.WriteLine("Server initialized.");

View file

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

View file

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

View file

@ -1,18 +1,18 @@
using ModelContextProtocol.Protocol.Transport; using ModelContextProtocol.Protocol.Transport;
using ModelContextProtocol.Protocol.Types; using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Server; using ModelContextProtocol.Server;
using Microsoft.Extensions.Logging.Abstractions; using ModelContextProtocol.Tests.Utils;
using Moq; using Moq;
namespace ModelContextProtocol.Tests.Server; namespace ModelContextProtocol.Tests.Server;
public class McpServerFactoryTests public class McpServerFactoryTests : LoggedTest
{ {
private readonly Mock<IServerTransport> _serverTransport; private readonly Mock<IServerTransport> _serverTransport;
private readonly McpServerOptions _options; private readonly McpServerOptions _options;
private readonly IServiceProvider _serviceProvider;
public McpServerFactoryTests() public McpServerFactoryTests(ITestOutputHelper testOutputHelper)
: base(testOutputHelper)
{ {
_serverTransport = new Mock<IServerTransport>(); _serverTransport = new Mock<IServerTransport>();
_options = new McpServerOptions _options = new McpServerOptions
@ -21,14 +21,13 @@ public class McpServerFactoryTests
ProtocolVersion = "1.0", ProtocolVersion = "1.0",
InitializationTimeout = TimeSpan.FromSeconds(30) InitializationTimeout = TimeSpan.FromSeconds(30)
}; };
_serviceProvider = new Mock<IServiceProvider>().Object;
} }
[Fact] [Fact]
public async Task Create_Should_Initialize_With_Valid_Parameters() public async Task Create_Should_Initialize_With_Valid_Parameters()
{ {
// Arrange & Act // 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
Assert.NotNull(server); Assert.NotNull(server);
@ -38,13 +37,13 @@ public class McpServerFactoryTests
public void Constructor_Throws_For_Null_ServerTransport() public void Constructor_Throws_For_Null_ServerTransport()
{ {
// Arrange, Act & Assert // Arrange, Act & Assert
Assert.Throws<ArgumentNullException>("serverTransport", () => McpServerFactory.Create(null!, _options, NullLoggerFactory.Instance)); Assert.Throws<ArgumentNullException>("serverTransport", () => McpServerFactory.Create(null!, _options, LoggerFactory));
} }
[Fact] [Fact]
public void Constructor_Throws_For_Null_Options() public void Constructor_Throws_For_Null_Options()
{ {
// Arrange, Act & Assert // 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; namespace ModelContextProtocol.Tests.Server;
public class McpServerTests public class McpServerTests : LoggedTest
{ {
private readonly Mock<IServerTransport> _serverTransport; private readonly Mock<IServerTransport> _serverTransport;
private readonly Mock<ILoggerFactory> _loggerFactory;
private readonly Mock<ILogger> _logger; private readonly Mock<ILogger> _logger;
private readonly McpServerOptions _options; private readonly McpServerOptions _options;
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
public McpServerTests() public McpServerTests(ITestOutputHelper testOutputHelper)
: base(testOutputHelper)
{ {
_serverTransport = new Mock<IServerTransport>(); _serverTransport = new Mock<IServerTransport>();
_loggerFactory = new Mock<ILoggerFactory>();
_logger = new Mock<ILogger>(); _logger = new Mock<ILogger>();
_loggerFactory.Setup(f => f.CreateLogger(It.IsAny<string>())).Returns(_logger.Object);
_options = CreateOptions(); _options = CreateOptions();
_serviceProvider = new Mock<IServiceProvider>().Object; _serviceProvider = new Mock<IServiceProvider>().Object;
} }
@ -44,7 +42,7 @@ public class McpServerTests
public async Task Constructor_Should_Initialize_With_Valid_Parameters() public async Task Constructor_Should_Initialize_With_Valid_Parameters()
{ {
// Arrange & Act // 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
Assert.NotNull(server); Assert.NotNull(server);
@ -54,14 +52,14 @@ public class McpServerTests
public void Constructor_Throws_For_Null_Transport() public void Constructor_Throws_For_Null_Transport()
{ {
// Arrange, Act & Assert // Arrange, Act & Assert
Assert.Throws<ArgumentNullException>(() => McpServerFactory.Create(null!, _options, _loggerFactory.Object, _serviceProvider)); Assert.Throws<ArgumentNullException>(() => McpServerFactory.Create(null!, _options, LoggerFactory, _serviceProvider));
} }
[Fact] [Fact]
public void Constructor_Throws_For_Null_Options() public void Constructor_Throws_For_Null_Options()
{ {
// Arrange, Act & Assert // 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] [Fact]
@ -78,7 +76,7 @@ public class McpServerTests
public async Task Constructor_Does_Not_Throw_For_Null_ServiceProvider() public async Task Constructor_Does_Not_Throw_For_Null_ServiceProvider()
{ {
// Arrange & Act // 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
Assert.NotNull(server); Assert.NotNull(server);
@ -88,7 +86,7 @@ public class McpServerTests
public async Task StartAsync_Should_Throw_InvalidOperationException_If_Already_Initializing() public async Task StartAsync_Should_Throw_InvalidOperationException_If_Already_Initializing()
{ {
// Arrange // 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); var task = server.StartAsync(TestContext.Current.CancellationToken);
// Act & Assert // Act & Assert
@ -101,7 +99,7 @@ public class McpServerTests
public async Task StartAsync_Should_Do_Nothing_If_Already_Initialized() public async Task StartAsync_Should_Do_Nothing_If_Already_Initialized()
{ {
// Arrange // 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); SetInitialized(server, true);
await server.StartAsync(TestContext.Current.CancellationToken); await server.StartAsync(TestContext.Current.CancellationToken);
@ -114,7 +112,7 @@ public class McpServerTests
public async Task StartAsync_ShouldStartListening() public async Task StartAsync_ShouldStartListening()
{ {
// Arrange // 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 // Act
await server.StartAsync(TestContext.Current.CancellationToken); await server.StartAsync(TestContext.Current.CancellationToken);
@ -127,7 +125,7 @@ public class McpServerTests
public async Task StartAsync_Sets_Initialized_After_Transport_Responses_Initialized_Notification() public async Task StartAsync_Sets_Initialized_After_Transport_Responses_Initialized_Notification()
{ {
await using var transport = new TestServerTransport(); 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); 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() public async Task RequestSamplingAsync_Should_Throw_McpServerException_If_Client_Does_Not_Support_Sampling()
{ {
// Arrange // 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()); SetClientCapabilities(server, new ClientCapabilities());
var action = () => server.RequestSamplingAsync(new CreateMessageRequestParams { Messages = [] }, CancellationToken.None); var action = () => server.RequestSamplingAsync(new CreateMessageRequestParams { Messages = [] }, CancellationToken.None);
@ -161,7 +159,7 @@ public class McpServerTests
{ {
// Arrange // Arrange
await using var transport = new TestServerTransport(); 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() }); SetClientCapabilities(server, new ClientCapabilities { Sampling = new SamplingCapability() });
await server.StartAsync(TestContext.Current.CancellationToken); 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() public async Task RequestRootsAsync_Should_Throw_McpServerException_If_Client_Does_Not_Support_Roots()
{ {
// Arrange // 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()); SetClientCapabilities(server, new ClientCapabilities());
// Act & Assert // Act & Assert
@ -191,7 +189,7 @@ public class McpServerTests
{ {
// Arrange // Arrange
await using var transport = new TestServerTransport(); 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() }); SetClientCapabilities(server, new ClientCapabilities { Roots = new RootsCapability() });
await server.StartAsync(TestContext.Current.CancellationToken); await server.StartAsync(TestContext.Current.CancellationToken);
@ -208,7 +206,7 @@ public class McpServerTests
[Fact] [Fact]
public async Task Throws_Exception_If_Not_Connected() 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() }); SetClientCapabilities(server, new ClientCapabilities { Roots = new RootsCapability() });
_serverTransport.SetupGet(t => t.IsConnected).Returns(false); _serverTransport.SetupGet(t => t.IsConnected).Returns(false);
@ -555,7 +553,7 @@ public class McpServerTests
var options = CreateOptions(serverCapabilities); var options = CreateOptions(serverCapabilities);
configureOptions?.Invoke(options); 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(); await server.StartAsync();
@ -587,7 +585,7 @@ public class McpServerTests
await using var transport = new TestServerTransport(); await using var transport = new TestServerTransport();
var options = CreateOptions(serverCapabilities); var options = CreateOptions(serverCapabilities);
Assert.Throws<McpServerException>(() => McpServerFactory.Create(transport, options, _loggerFactory.Object, _serviceProvider)); Assert.Throws<McpServerException>(() => McpServerFactory.Create(transport, options, LoggerFactory, _serviceProvider));
} }
[Fact] [Fact]

View file

@ -2,23 +2,29 @@
using ModelContextProtocol.Client; using ModelContextProtocol.Client;
using ModelContextProtocol.Configuration; using ModelContextProtocol.Configuration;
using ModelContextProtocol.Protocol.Transport; using ModelContextProtocol.Protocol.Transport;
using ModelContextProtocol.Test.Utils;
using ModelContextProtocol.TestSseServer;
namespace ModelContextProtocol.Tests; namespace ModelContextProtocol.Tests;
public class SseServerIntegrationTestFixture : IAsyncDisposable public class SseServerIntegrationTestFixture : IAsyncDisposable
{ {
private readonly CancellationTokenSource _stopCts = new();
private readonly Task _serverTask; 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 McpClientOptions DefaultOptions { get; }
public McpServerConfig DefaultConfig { get; } public McpServerConfig DefaultConfig { get; }
public SseServerIntegrationTestFixture() public SseServerIntegrationTestFixture()
{ {
LoggerFactory = Microsoft.Extensions.Logging.LoggerFactory.Create(builder => _redirectingLoggerFactory = LoggerFactory.Create(builder =>
builder.AddConsole() {
.SetMinimumLevel(LogLevel.Debug)); Program.ConfigureSerilog(builder);
builder.AddProvider(new XunitLoggerProvider(_delegatingTestOutputHelper));
});
DefaultOptions = new() DefaultOptions = new()
{ {
@ -34,12 +40,17 @@ public class SseServerIntegrationTestFixture : IAsyncDisposable
Location = "http://localhost:3001/sse" 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() public async ValueTask DisposeAsync()
{ {
LoggerFactory.Dispose(); _delegatingTestOutputHelper.CurrentTestOutputHelper = null;
_stopCts.Cancel(); _stopCts.Cancel();
try try
{ {
@ -48,6 +59,19 @@ public class SseServerIntegrationTestFixture : IAsyncDisposable
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
} }
_redirectingLoggerFactory.Dispose();
_stopCts.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.Client;
using ModelContextProtocol.Protocol.Types; using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Tests.Utils;
namespace ModelContextProtocol.Tests; namespace ModelContextProtocol.Tests;
public class SseServerIntegrationTests : IClassFixture<SseServerIntegrationTestFixture> public class SseServerIntegrationTests : LoggedTest, IClassFixture<SseServerIntegrationTestFixture>
{ {
private readonly SseServerIntegrationTestFixture _fixture; private readonly SseServerIntegrationTestFixture _fixture;
public SseServerIntegrationTests(SseServerIntegrationTestFixture fixture) public SseServerIntegrationTests(SseServerIntegrationTestFixture fixture, ITestOutputHelper testOutputHelper)
: base(testOutputHelper)
{ {
_fixture = fixture; _fixture = fixture;
_fixture.Initialize(testOutputHelper);
} }
private Task<IMcpClient> GetClientAsync(McpClientOptions? options = null) private Task<IMcpClient> GetClientAsync(McpClientOptions? options = null)
@ -17,7 +20,7 @@ public class SseServerIntegrationTests : IClassFixture<SseServerIntegrationTestF
return McpClientFactory.CreateAsync( return McpClientFactory.CreateAsync(
_fixture.DefaultConfig, _fixture.DefaultConfig,
options ?? _fixture.DefaultOptions, options ?? _fixture.DefaultOptions,
loggerFactory: _fixture.LoggerFactory); loggerFactory: LoggerFactory);
} }
[Fact] [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.Messages;
using ModelContextProtocol.Protocol.Transport; using ModelContextProtocol.Protocol.Transport;
using ModelContextProtocol.Tests.Utils; using ModelContextProtocol.Tests.Utils;
@ -8,12 +7,13 @@ using System.Reflection;
namespace ModelContextProtocol.Tests.Transport; namespace ModelContextProtocol.Tests.Transport;
public class SseClientTransportTests public class SseClientTransportTests : LoggedTest
{ {
private readonly McpServerConfig _serverConfig; private readonly McpServerConfig _serverConfig;
private readonly SseClientTransportOptions _transportOptions; private readonly SseClientTransportOptions _transportOptions;
public SseClientTransportTests() public SseClientTransportTests(ITestOutputHelper testOutputHelper)
: base(testOutputHelper)
{ {
_serverConfig = new McpServerConfig _serverConfig = new McpServerConfig
{ {
@ -39,7 +39,7 @@ public class SseClientTransportTests
public async Task Constructor_Should_Initialize_With_Valid_Parameters() public async Task Constructor_Should_Initialize_With_Valid_Parameters()
{ {
// Act // Act
await using var transport = new SseClientTransport(_transportOptions, _serverConfig, NullLoggerFactory.Instance); await using var transport = new SseClientTransport(_transportOptions, _serverConfig, LoggerFactory);
// Assert // Assert
Assert.NotNull(transport); Assert.NotNull(transport);
@ -58,21 +58,21 @@ public class SseClientTransportTests
[Fact] [Fact]
public void Constructor_Throws_For_Null_Options() 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); Assert.Equal("transportOptions", exception.ParamName);
} }
[Fact] [Fact]
public void Constructor_Throws_For_Null_Config() 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); Assert.Equal("serverConfig", exception.ParamName);
} }
[Fact] [Fact]
public void Constructor_Throws_For_Null_HttpClientg() 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); Assert.Equal("httpClient", exception.ParamName);
} }
@ -81,7 +81,7 @@ public class SseClientTransportTests
{ {
using var mockHttpHandler = new MockHttpHandler(); using var mockHttpHandler = new MockHttpHandler();
using var httpClient = new HttpClient(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; bool firstCall = true;
@ -109,7 +109,7 @@ public class SseClientTransportTests
{ {
using var mockHttpHandler = new MockHttpHandler(); using var mockHttpHandler = new MockHttpHandler();
using var httpClient = new HttpClient(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 tcsConnected = new TaskCompletionSource();
var tcsDone = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var tcsDone = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var callIndex = 0; var callIndex = 0;
@ -157,7 +157,7 @@ public class SseClientTransportTests
{ {
using var mockHttpHandler = new MockHttpHandler(); using var mockHttpHandler = new MockHttpHandler();
using var httpClient = new HttpClient(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; var retries = 0;
mockHttpHandler.RequestHandler = (request) => mockHttpHandler.RequestHandler = (request) =>
@ -177,7 +177,7 @@ public class SseClientTransportTests
[Fact] [Fact]
public async Task SendMessageAsync_Throws_Exception_If_MessageEndpoint_Not_Set() 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 // Assert
await Assert.ThrowsAsync<InvalidOperationException>(() => transport.SendMessageAsync(new JsonRpcRequest() { Method = "test" }, CancellationToken.None)); 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 mockHttpHandler = new MockHttpHandler();
using var httpClient = new HttpClient(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; var firstCall = true;
mockHttpHandler.RequestHandler = (request) => mockHttpHandler.RequestHandler = (request) =>
@ -227,7 +227,7 @@ public class SseClientTransportTests
{ {
using var mockHttpHandler = new MockHttpHandler(); using var mockHttpHandler = new MockHttpHandler();
using var httpClient = new HttpClient(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; var firstCall = true;
mockHttpHandler.RequestHandler = (request) => mockHttpHandler.RequestHandler = (request) =>
@ -266,7 +266,7 @@ public class SseClientTransportTests
{ {
using var mockHttpHandler = new MockHttpHandler(); using var mockHttpHandler = new MockHttpHandler();
using var httpClient = new HttpClient(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; var callIndex = 0;
mockHttpHandler.RequestHandler = (request) => mockHttpHandler.RequestHandler = (request) =>
@ -303,7 +303,7 @@ public class SseClientTransportTests
[Fact] [Fact]
public async Task CloseAsync_Should_Dispose_Resources() 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(); await transport.CloseAsync();
@ -313,7 +313,7 @@ public class SseClientTransportTests
[Fact] [Fact]
public async Task DisposeAsync_Should_Dispose_Resources() 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(); 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.Transport;
using ModelContextProtocol.Protocol.Types; using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Server; using ModelContextProtocol.Server;
using ModelContextProtocol.Tests.Utils;
using ModelContextProtocol.Utils.Json; using ModelContextProtocol.Utils.Json;
using System.IO.Pipelines; using System.IO.Pipelines;
using System.Text; using System.Text;
@ -10,11 +10,12 @@ using System.Text.Json;
namespace ModelContextProtocol.Tests.Transport; namespace ModelContextProtocol.Tests.Transport;
public class StdioServerTransportTests public class StdioServerTransportTests : LoggedTest
{ {
private readonly McpServerOptions _serverOptions; private readonly McpServerOptions _serverOptions;
public StdioServerTransportTests() public StdioServerTransportTests(ITestOutputHelper testOutputHelper)
: base(testOutputHelper)
{ {
_serverOptions = new McpServerOptions _serverOptions = new McpServerOptions
{ {
@ -68,7 +69,7 @@ public class StdioServerTransportTests
_serverOptions.ServerInfo.Name, _serverOptions.ServerInfo.Name,
new Pipe().Reader.AsStream(), new Pipe().Reader.AsStream(),
output, output,
NullLoggerFactory.Instance); LoggerFactory);
await transport.StartListeningAsync(TestContext.Current.CancellationToken); await transport.StartListeningAsync(TestContext.Current.CancellationToken);
@ -122,7 +123,7 @@ public class StdioServerTransportTests
_serverOptions.ServerInfo.Name, _serverOptions.ServerInfo.Name,
input, input,
Stream.Null, Stream.Null,
NullLoggerFactory.Instance); LoggerFactory);
await transport.StartListeningAsync(TestContext.Current.CancellationToken); await transport.StartListeningAsync(TestContext.Current.CancellationToken);
@ -165,7 +166,7 @@ public class StdioServerTransportTests
_serverOptions.ServerInfo.Name, _serverOptions.ServerInfo.Name,
new Pipe().Reader.AsStream(), new Pipe().Reader.AsStream(),
output, output,
NullLoggerFactory.Instance); LoggerFactory);
await transport.StartListeningAsync(TestContext.Current.CancellationToken); 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()
{
}
}
}
}