using System.Threading.Channels; using ModelContextProtocol.Client; using ModelContextProtocol.Configuration; using ModelContextProtocol.Protocol.Messages; using ModelContextProtocol.Protocol.Transport; using ModelContextProtocol.Protocol.Types; namespace ModelContextProtocol.Tests.Client; public class McpClientFactoryTests { private readonly McpClientOptions _defaultOptions = new() { ClientInfo = new() { Name = "TestClient", Version = "1.0.0" } }; [Fact] public async Task CreateAsync_WithInvalidArgs_Throws() { await Assert.ThrowsAsync("serverConfig", () => McpClientFactory.CreateAsync((McpServerConfig)null!, _defaultOptions, cancellationToken: TestContext.Current.CancellationToken)); await Assert.ThrowsAsync("serverConfig", () => McpClientFactory.CreateAsync(new McpServerConfig() { Name = "name", Id = "id", TransportType = "somethingunsupported", }, _defaultOptions, cancellationToken: TestContext.Current.CancellationToken)); await Assert.ThrowsAsync(() => McpClientFactory.CreateAsync(new McpServerConfig() { Name = "name", Id = "id", TransportType = TransportTypes.StdIo, }, _defaultOptions, (_, __) => null!, cancellationToken: TestContext.Current.CancellationToken)); } [Fact] public async Task CreateAsync_NullOptions_EntryAssemblyInferred() { // Arrange var serverConfig = new McpServerConfig { Id = "test-server", Name = "Test Server", TransportType = TransportTypes.StdIo, Location = "/path/to/server", }; // Act await using var client = await McpClientFactory.CreateAsync( serverConfig, null, (_, __) => new NopTransport(), cancellationToken: TestContext.Current.CancellationToken); Assert.NotNull(client); } [Fact] public async Task CreateAsync_WithValidStdioConfig_CreatesNewClient() { // Arrange var serverConfig = new McpServerConfig { Id = "test-server", Name = "Test Server", TransportType = TransportTypes.StdIo, Location = "/path/to/server", TransportOptions = new Dictionary { ["arguments"] = "--test arg", ["workingDirectory"] = "/working/dir" } }; // Act await using var client = await McpClientFactory.CreateAsync( serverConfig, _defaultOptions, (_, __) => new NopTransport(), cancellationToken: TestContext.Current.CancellationToken); // Assert Assert.NotNull(client); // We could add more assertions here about the client's configuration } [Fact] public async Task CreateAsync_WithNoTransportOptions_CreatesNewClient() { // Arrange var serverConfig = new McpServerConfig { Id = "test-server", Name = "Test Server", TransportType = TransportTypes.StdIo, Location = "/path/to/server", }; // Act await using var client = await McpClientFactory.CreateAsync( serverConfig, _defaultOptions, (_, __) => new NopTransport(), cancellationToken: TestContext.Current.CancellationToken); // Assert Assert.NotNull(client); // We could add more assertions here about the client's configuration } [Fact] public async Task CreateAsync_WithValidSseConfig_CreatesNewClient() { // Arrange var serverConfig = new McpServerConfig { Id = "test-server", Name = "Test Server", TransportType = TransportTypes.Sse, Location = "http://localhost:8080" }; // Act await using var client = await McpClientFactory.CreateAsync( serverConfig, _defaultOptions, (_, __) => new NopTransport(), cancellationToken: TestContext.Current.CancellationToken); // Assert Assert.NotNull(client); // We could add more assertions here about the client's configuration } [Fact] public async Task CreateAsync_WithSse_CreatesCorrectTransportOptions() { // Arrange var serverConfig = new McpServerConfig { Id = "test-server", Name = "Test Server", TransportType = TransportTypes.Sse, Location = "http://localhost:8080", TransportOptions = new Dictionary { ["connectionTimeout"] = "10", ["maxReconnectAttempts"] = "2", ["reconnectDelay"] = "5", ["header.test"] = "the_header_value" } }; // Act await using var client = await McpClientFactory.CreateAsync( serverConfig, _defaultOptions, (_, __) => new NopTransport(), cancellationToken: TestContext.Current.CancellationToken); // Assert Assert.NotNull(client); // We could add more assertions here about the client's configuration } [Theory] [InlineData("connectionTimeout", "not_a_number")] [InlineData("maxReconnectAttempts", "invalid")] [InlineData("reconnectDelay", "bad_value")] public async Task McpFactory_WithInvalidTransportOptions_ThrowsFormatException(string key, string value) { // arrange var config = new McpServerConfig { Id = "test-server", Name = "Test Server", TransportType = TransportTypes.Sse, Location = "http://localhost:8080", TransportOptions = new Dictionary { [key] = value } }; // act & assert await Assert.ThrowsAsync(() => McpClientFactory.CreateAsync(config, _defaultOptions, cancellationToken: TestContext.Current.CancellationToken)); } private sealed class NopTransport : IClientTransport { private readonly Channel _channel = Channel.CreateUnbounded(); public bool IsConnected => true; public ChannelReader MessageReader => _channel.Reader; public Task ConnectAsync(CancellationToken cancellationToken = default) => Task.CompletedTask; public ValueTask DisposeAsync() => default; public Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default) { switch (message) { case JsonRpcRequest: _channel.Writer.TryWrite(new JsonRpcResponse { Id = ((JsonRpcRequest)message).Id, Result = new InitializeResult() { Capabilities = new ServerCapabilities(), ProtocolVersion = "2024-11-05", ServerInfo = new Implementation() { Name = "NopTransport", Version = "1.0.0" }, } }); break; } return Task.CompletedTask; } } }