using Microsoft.Extensions.Logging; using ModelContextProtocol.Client; using ModelContextProtocol.Protocol.Messages; using ModelContextProtocol.Protocol.Transport; using ModelContextProtocol.Protocol.Types; using ModelContextProtocol.Utils.Json; using Moq; using System.IO.Pipelines; using System.Text.Json; using System.Threading.Channels; namespace ModelContextProtocol.Tests.Client; public class McpClientFactoryTests { [Fact] public async Task CreateAsync_WithInvalidArgs_Throws() { await Assert.ThrowsAsync("clientTransport", () => McpClientFactory.CreateAsync(null!, cancellationToken: TestContext.Current.CancellationToken)); } [Fact] public async Task CreateAsync_NopTransport_ReturnsClient() { // Act await using var client = await McpClientFactory.CreateAsync( new NopTransport(), cancellationToken: TestContext.Current.CancellationToken); Assert.NotNull(client); } [Theory] [InlineData(false)] [InlineData(true)] public async Task Cancellation_ThrowsCancellationException(bool preCanceled) { var cts = new CancellationTokenSource(); if (preCanceled) { cts.Cancel(); } Task t = McpClientFactory.CreateAsync( new StreamClientTransport(new Pipe().Writer.AsStream(), new Pipe().Reader.AsStream()), cancellationToken: cts.Token); Assert.False(t.IsCompleted); if (!preCanceled) { cts.Cancel(); } await Assert.ThrowsAnyAsync(() => t); } [Theory] [InlineData(typeof(NopTransport))] [InlineData(typeof(FailureTransport))] public async Task CreateAsync_WithCapabilitiesOptions(Type transportType) { // Arrange var clientOptions = new McpClientOptions { Capabilities = new ClientCapabilities { Sampling = new SamplingCapability { SamplingHandler = async (c, p, t) => new CreateMessageResult { Content = new Content { Text = "result" }, Model = "test-model", Role = Role.User, StopReason = "endTurn" }, }, Roots = new RootsCapability { ListChanged = true, RootsHandler = async (t, r) => new ListRootsResult { Roots = [] }, } } }; var clientTransport = (IClientTransport)Activator.CreateInstance(transportType)!; IMcpClient? client = null; var actionTask = McpClientFactory.CreateAsync(clientTransport, clientOptions, new Mock().Object, CancellationToken.None); // Act if (clientTransport is FailureTransport) { var exception = await Assert.ThrowsAsync(async() => await actionTask); Assert.Equal(FailureTransport.ExpectedMessage, exception.Message); } else { client = await actionTask; // Assert Assert.NotNull(client); } } private class NopTransport : ITransport, IClientTransport { private readonly Channel _channel = Channel.CreateUnbounded(); public bool IsConnected => true; public ChannelReader MessageReader => _channel.Reader; public Task ConnectAsync(CancellationToken cancellationToken = default) => Task.FromResult(this); public ValueTask DisposeAsync() => default; public string Name => "Test Nop Transport"; public virtual Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default) { switch (message) { case JsonRpcRequest: _channel.Writer.TryWrite(new JsonRpcResponse { Id = ((JsonRpcRequest)message).Id, Result = JsonSerializer.SerializeToNode(new InitializeResult { Capabilities = new ServerCapabilities(), ProtocolVersion = "2024-11-05", ServerInfo = new Implementation { Name = "NopTransport", Version = "1.0.0" }, }, McpJsonUtilities.DefaultOptions), }); break; } return Task.CompletedTask; } } private sealed class FailureTransport : NopTransport { public const string ExpectedMessage = "Something failed"; public override Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default) { throw new InvalidOperationException(ExpectedMessage); } } }