Add more tests (#51)

* Add more tests

* Fix some code smells

* Remove more code smells

* Combine "ServerIntegrationTests" and "ClientIntegrationTests" because both are client integration tests just using different servers

* Clean up resources after test

* Improve kill logic for spawned child processes

* Improved testability for Stdio

* Fix SseIntegration tests
This commit is contained in:
Toni Wenzel 2025-03-12 18:55:52 +01:00 • committed by GitHub
commit b5076263d1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 883 additions and 751 deletions

View file

@ -86,9 +86,10 @@ public sealed class McpSessionScope : IAsyncDisposable
McpClientOptions? options,
ILoggerFactory? loggerFactory = null)
{
var factory = new McpClientFactory([config],
using var factory = new McpClientFactory([config],
options ?? new() { ClientInfo = new() { Name = "AnonymousClient", Version = "1.0.0.0" } },
loggerFactory ?? NullLoggerFactory.Instance);
factory.DisposeClientsOnDispose = false;
var client = await factory.GetClientAsync(config.Id).ConfigureAwait(false);
_clients.Add(client);
return client;

View file

@ -1,4 +1,5 @@
using System.Runtime.InteropServices;
using System.Globalization;
using System.Runtime.InteropServices;
using McpDotNet.Configuration;
using McpDotNet.Logging;
using McpDotNet.Protocol.Transport;
@ -11,7 +12,7 @@ namespace McpDotNet.Client;
/// All server configurations must be passed in the constructor.
/// Capabilities (as defined in client options) are shared across all clients, as the client host can always decide not to use certain capabilities.
/// </summary>
public class McpClientFactory
public class McpClientFactory : IDisposable
{
private const string ARGUMENTS_OPTIONS_KEY = "arguments";
private const string COMMAND_OPTIONS_KEY = "command";
@ -22,6 +23,12 @@ public class McpClientFactory
private readonly Func<IClientTransport, McpServerConfig, McpClientOptions, IMcpClient> _clientFactoryMethod;
private readonly ILoggerFactory _loggerFactory;
private readonly ILogger<McpClientFactory> _logger;
private bool _isDisposed;
/// <summary>
/// Gets or sets a value indicating whether clients should be disposed when the factory is disposed.
/// </summary>
public bool DisposeClientsOnDispose { get; set; } = true;
/// <summary>
/// Initializes a new instance of the <see cref="McpClientFactory"/> class.
@ -109,7 +116,8 @@ public class McpClientFactory
WorkingDirectory = config.TransportOptions?.GetValueOrDefault("workingDirectory"),
EnvironmentVariables = config.TransportOptions?
.Where(kv => kv.Key.StartsWith("env:", StringComparison.Ordinal))
.ToDictionary(kv => kv.Key.Substring(4), kv => kv.Value)
.ToDictionary(kv => kv.Key.Substring(4), kv => kv.Value),
ShutdownTimeout = TimeSpan.TryParse(config.TransportOptions?.GetValueOrDefault("shutdownTimeout"), CultureInfo.InvariantCulture, out var timespan) ? timespan : StdioClientTransportOptions.DefaultShutdownTimeout
}, config, _loggerFactory);
}
@ -181,4 +189,37 @@ public class McpClientFactory
? $"/c {command} {args}"
: $"/c {command}";
}
/// <summary>
/// Disposes all clients created by the factory.
/// </summary>
/// <param name="disposing"></param>
protected virtual void Dispose(bool disposing)
{
if (!_isDisposed)
{
if (disposing && DisposeClientsOnDispose)
DisposeClients();
_isDisposed = true;
}
}
private void DisposeClients()
{
foreach (var client in _clients.Values)
{
client?.DisposeAsync().AsTask().Wait();
}
_clients.Clear();
}
/// <inheritdoc />
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}

View file

@ -116,7 +116,7 @@ public static partial class McpServerBuilderExtensions
if (toolTypes.Count == 0)
throw new ArgumentException("No types with marked methods found in the assembly.", nameof(assembly));
return WithTools(builder, toolTypes.ToArray());
return WithTools(builder, [.. toolTypes]);
}
private static Tool CreateTool(MethodInfo method, McpToolAttribute mcpAttribute)
@ -187,7 +187,7 @@ public static partial class McpServerBuilderExtensions
BindingFlags.DoNotWrapExceptions |
#endif
BindingFlags.Default;
result = method.Invoke(objectInstance, InvokeFlags, binder: null, parameters.ToArray(), culture: null);
result = method.Invoke(objectInstance, InvokeFlags, binder: null, [.. parameters], culture: null);
}
catch (TargetInvocationException e) when (e.InnerException is not null)
{
@ -206,7 +206,7 @@ public static partial class McpServerBuilderExtensions
return new CallToolResponse { Content = [new Content() { Text = resultString, Type = "text" }] };
if (result is string[] resultStringArray)
return new CallToolResponse { Content = resultStringArray.Select(s => new Content() { Text = s, Type = "text" }).ToList() };
return new CallToolResponse { Content = [.. resultStringArray.Select(s => new Content() { Text = s, Type = "text" })] };
if (result is null)
return new CallToolResponse { Content = [new Content() { Text = "null" }] };

View file

@ -1,6 +1,7 @@
using System.Reflection;
using McpDotNet.Configuration;
using McpDotNet.Hosting;
using McpDotNet.Protocol.Transport;
using McpDotNet.Protocol.Types;
using McpDotNet.Server;
using Microsoft.Extensions.DependencyInjection;
@ -38,6 +39,7 @@ public static class McpServerServiceCollectionExtension
services.AddSingleton<IMcpServerFactory, McpServerFactory>();
services.AddHostedService<McpServerHostedService>();
services.AddOptions();
services.AddSingleton(InputOutputStreams.Default);
services.AddSingleton<IMcpServer>(sp => sp.GetRequiredService<IMcpServerFactory>().CreateServer());

View file

@ -0,0 +1,26 @@
namespace McpDotNet.Protocol.Transport;
/// <summary>
/// Container for the input and output streams.
/// </summary>
public class InputOutputStreams
{
/// <summary>
/// Gets the default input and output streams.
/// </summary>
public static readonly InputOutputStreams Default = new()
{
Output = Console.Out,
Input = Console.In
};
/// <summary>
/// Gets or sets the output stream.
/// </summary>
public required TextWriter Output { get; init; }
/// <summary>
/// Gets or sets the input stream.
/// </summary>
public required TextReader Input { get; init; }
}

View file

@ -119,7 +119,7 @@ public sealed class SseClientTransport : TransportBase, IClientTransport
if (_messageEndpoint == null)
throw new InvalidOperationException("Transport not connected");
var content = new StringContent(
using var content = new StringContent(
JsonSerializer.Serialize(message, _jsonOptions),
Encoding.UTF8,
"application/json"
@ -217,7 +217,7 @@ public sealed class SseClientTransport : TransportBase, IClientTransport
{
try
{
var request = new HttpRequestMessage(HttpMethod.Get, _sseEndpoint);
using var request = new HttpRequestMessage(HttpMethod.Get, _sseEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream"));
using var response = await _httpClient.SendAsync(
@ -328,7 +328,7 @@ public sealed class SseClientTransport : TransportBase, IClientTransport
if (hostUrl.EndsWith("/sse", StringComparison.Ordinal))
hostUrl = hostUrl[..^4];
var endpointUri = hostUrl + data;
var endpointUri = $"{hostUrl.TrimEnd('/')}/{data.TrimStart('/')}";
_messageEndpoint = new Uri(endpointUri);
}

View file

@ -5,6 +5,7 @@ using System.Text.Json;
using McpDotNet.Configuration;
using McpDotNet.Logging;
using McpDotNet.Protocol.Messages;
using McpDotNet.Utils;
using McpDotNet.Utils.Json;
using Microsoft.Extensions.Logging;
@ -245,12 +246,10 @@ public sealed class StdioClientTransport : TransportBase, IClientTransport
// Wait for the process to exit
_logger.TransportWaitingForShutdown(EndpointName);
if (!_process.WaitForExit((int)_options.ShutdownTimeout.TotalMilliseconds))
{
// If it doesn't exit gracefully, terminate it
_logger.TransportKillingProcess(EndpointName);
_process.Kill(true);
}
// Kill the while process tree because the process may spawn child processes
// and Node.js does not kill its children when it exits properly
_process.KillTree(_options.ShutdownTimeout);
}
catch (Exception ex)
{
@ -262,9 +261,11 @@ public sealed class StdioClientTransport : TransportBase, IClientTransport
}
if (_shutdownCts != null)
{
await _shutdownCts.CancelAsync().ConfigureAwait(false);
_shutdownCts?.Dispose();
_shutdownCts = null;
_shutdownCts.Dispose();
_shutdownCts = null;
}
if (_readTask != null)
{

View file

@ -6,6 +6,11 @@ namespace McpDotNet.Protocol.Transport;
/// </summary>
public record StdioClientTransportOptions
{
/// <summary>
/// The default timeout to wait for the server to shut down gracefully.
/// </summary>
public static readonly TimeSpan DefaultShutdownTimeout = TimeSpan.FromSeconds(5);
/// <summary>
/// The command to execute to start the server process.
/// </summary>
@ -29,5 +34,5 @@ public record StdioClientTransportOptions
/// <summary>
/// The timeout to wait for the server to shut down gracefully.
/// </summary>
public TimeSpan ShutdownTimeout { get; init; } = TimeSpan.FromSeconds(5);
public TimeSpan ShutdownTimeout { get; init; } = DefaultShutdownTimeout;
}

View file

@ -18,6 +18,8 @@ public sealed class StdioServerTransport : TransportBase, IServerTransport
private readonly JsonSerializerOptions _jsonOptions;
private Task? _readTask;
private CancellationTokenSource? _shutdownCts;
private readonly TextReader _input;
private readonly TextWriter _output;
private string EndpointName => $"Server (stdio) ({_serverName})";
@ -26,8 +28,14 @@ public sealed class StdioServerTransport : TransportBase, IServerTransport
/// </summary>
/// <param name="serverOptions">The server options.</param>
/// <param name="loggerFactory">A logger factory for creating loggers.</param>
public StdioServerTransport(McpServerOptions serverOptions, ILoggerFactory? loggerFactory)
: this(serverOptions is not null ? serverOptions.ServerInfo.Name : throw new ArgumentNullException(nameof(serverOptions)), loggerFactory)
/// <param name="inputOutputStreams">Container for the input and output streams</param>
public StdioServerTransport(McpServerOptions serverOptions, ILoggerFactory? loggerFactory, InputOutputStreams inputOutputStreams)
: this(
serverOptions is not null ? serverOptions.ServerInfo.Name : throw new ArgumentNullException(nameof(serverOptions)),
loggerFactory,
inputOutputStreams is not null ? inputOutputStreams.Output : throw new ArgumentNullException(nameof(inputOutputStreams)),
inputOutputStreams.Input
)
{
}
@ -37,11 +45,26 @@ public sealed class StdioServerTransport : TransportBase, IServerTransport
/// <param name="serverName">The name of the server.</param>
/// <param name="loggerFactory">A logger factory for creating loggers.</param>
public StdioServerTransport(string serverName, ILoggerFactory? loggerFactory)
: this(serverName, loggerFactory, Console.Out, Console.In)
{
}
/// <summary>
/// Initializes a new instance of the StdioServerTransport class.
/// </summary>
/// <param name="serverName">The name of the server.</param>
/// <param name="loggerFactory">A logger factory for creating loggers.</param>
/// <param name="output">The output stream writer</param>
/// <param name="input">The input stream reader</param>
public StdioServerTransport(string serverName, ILoggerFactory? loggerFactory, TextWriter output, TextReader input)
: base(loggerFactory)
{
_serverName = serverName;
_logger = loggerFactory is not null ? loggerFactory.CreateLogger<StdioClientTransport>() : NullLogger.Instance;
_jsonOptions = JsonSerializerOptionsExtensions.DefaultOptions;
_input = input ?? throw new ArgumentNullException(nameof(input));
_output = output ?? throw new ArgumentNullException(nameof(output));
}
/// <inheritdoc/>
@ -56,7 +79,6 @@ public sealed class StdioServerTransport : TransportBase, IServerTransport
return Task.CompletedTask;
}
/// <inheritdoc/>
public override async Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default)
{
@ -77,8 +99,8 @@ public sealed class StdioServerTransport : TransportBase, IServerTransport
var json = JsonSerializer.Serialize(message, _jsonOptions);
_logger.TransportSendingMessage(EndpointName, id, json);
await Console.Out.WriteLineAsync(json.AsMemory(), cancellationToken).ConfigureAwait(false);
await Console.Out.FlushAsync(cancellationToken).ConfigureAwait(false);
await _output.WriteLineAsync(json.AsMemory(), cancellationToken).ConfigureAwait(false);
await _output.FlushAsync(cancellationToken).ConfigureAwait(false);
_logger.TransportSentMessage(EndpointName, id);
}
@ -106,7 +128,7 @@ public sealed class StdioServerTransport : TransportBase, IServerTransport
{
_logger.TransportWaitingForMessage(EndpointName);
var reader = Console.In;
var reader = _input;
var line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false);
if (line == null)
{

View file

@ -25,6 +25,7 @@ internal abstract class McpJsonRpcEndpoint : IAsyncDisposable
private int _nextRequestId;
private readonly JsonSerializerOptions _jsonOptions;
private readonly ILogger<McpClient> _logger;
private bool _isDisposed;
/// <summary>
/// Initializes a new instance of the <see cref="McpJsonRpcEndpoint"/> class.
@ -398,6 +399,11 @@ internal abstract class McpJsonRpcEndpoint : IAsyncDisposable
/// <returns></returns>
protected async Task CleanupAsync()
{
if (_isDisposed)
return;
_isDisposed = true;
_logger.CleaningUpEndpoint(EndpointName);
if (CancellationTokenSource != null)

View file

@ -0,0 +1,108 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace McpDotNet.Utils;
/// <summary>
/// Helper class for working with processes.
/// </summary>
internal static class ProcessHelper
{
private static readonly bool _isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
private static readonly TimeSpan _defaultTimeout = TimeSpan.FromSeconds(30);
public static void KillTree(this Process process) => process.KillTree(_defaultTimeout);
public static void KillTree(this Process process, TimeSpan timeout)
{
var pid = process.Id;
if (_isWindows)
{
RunProcessAndWaitForExit(
"taskkill",
$"/T /F /PID {pid}",
timeout,
out var _);
}
else
{
var children = new HashSet<int>();
GetAllChildIdsUnix(pid, children, timeout);
foreach (var childId in children)
{
KillProcessUnix(childId, timeout);
}
KillProcessUnix(pid, timeout);
}
// wait until the process finishes exiting/getting killed.
// We don't want to wait forever here because the task is already supposed to be dieing, we just want to give it long enough
// to try and flush what it can and stop. If it cannot do that in a reasonable time frame then we will just ignore it.
process.WaitForExit((int)timeout.TotalMilliseconds);
}
private static void GetAllChildIdsUnix(int parentId, ISet<int> children, TimeSpan timeout)
{
var exitcode = RunProcessAndWaitForExit(
"pgrep",
$"-P {parentId}",
timeout,
out var stdout);
if (exitcode == 0 && !string.IsNullOrEmpty(stdout))
{
using var reader = new StringReader(stdout);
while (true)
{
var text = reader.ReadLine();
if (text == null)
return;
if (int.TryParse(text, out var id))
{
children.Add(id);
// Recursively get the children
GetAllChildIdsUnix(id, children, timeout);
}
}
}
}
private static void KillProcessUnix(int processId, TimeSpan timeout)
{
RunProcessAndWaitForExit(
"kill",
$"-TERM {processId}",
timeout,
out var _);
}
private static int RunProcessAndWaitForExit(string fileName, string arguments, TimeSpan timeout, out string? stdout)
{
var startInfo = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
};
stdout = null;
var process = Process.Start(startInfo);
if (process == null)
return -1;
if (process.WaitForExit((int)timeout.TotalMilliseconds))
{
stdout = process.StandardOutput.ReadToEnd();
}
else
{
process.Kill();
}
return process.ExitCode;
}
}

View file

@ -51,6 +51,7 @@ internal static class Program
ConfigureTools(server);
ConfigureResources(server);
ConfigurePrompts(server);
ConfigureCompletion(server);
Log.Logger.Information("Server initialized.");
@ -325,6 +326,43 @@ internal static class Program
};
}
private static void ConfigureCompletion(IMcpServer server)
{
List<string> sampleResourceIds = ["1", "2", "3", "4", "5"];
Dictionary<string, List<string>> exampleCompletions = new()
{
{"style", ["casual", "formal", "technical", "friendly"]},
{"temperature", ["0", "0.5", "0.7", "1.0"]},
};
server.GetCompletionHandler = (request, cancellationToken) =>
{
if (request.Params?.Ref?.Type == "ref/resource")
{
var resourceId = request.Params?.Ref?.Uri?.Split('/').LastOrDefault();
if (string.IsNullOrEmpty(resourceId))
return Task.FromResult(new CompleteResult() { Completion = new() { Values = [] } });
// Filter resource IDs that start with the input value
var values = sampleResourceIds.Where(id => id.StartsWith(request.Params.Argument.Value)).ToArray();
return Task.FromResult(new CompleteResult() { Completion = new() { Values = values, HasMore = false, Total = values.Length } });
}
if (request.Params?.Ref?.Type == "ref/prompt")
{
// Handle completion for prompt arguments
if (!exampleCompletions.TryGetValue(request.Params.Argument.Name, out var completions))
return Task.FromResult(new CompleteResult() { Completion = new() { Values = [] } });
var values = completions.Where(value => value.StartsWith(request.Params.Argument.Value)).ToArray();
return Task.FromResult(new CompleteResult() { Completion = new() { Values = values, HasMore = false, Total = values.Length } });
}
throw new McpServerException($"Unknown reference type: {request.Params?.Ref.Type}");
};
}
static CreateMessageRequestParams CreateRequestSamplingParams(string context, string uri, int maxTokens = 100)
{
return new CreateMessageRequestParams()

View file

@ -0,0 +1,368 @@
using System.Threading.Channels;
using McpDotNet.Client;
using McpDotNet.Configuration;
using McpDotNet.Protocol.Messages;
using McpDotNet.Protocol.Transport;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
namespace McpDotNet.Tests.Client;
public class McpClientFactoryTests
{
private readonly McpClientOptions _defaultOptions = new()
{
ClientInfo = new() { Name = "TestClient", Version = "1.0.0" }
};
[Fact]
public async Task GetClientAsync_WithValidStdioConfig_CreatesNewClient()
{
// Arrange
var config = new McpServerConfig
{
Id = "test-server",
Name = "Test Server",
TransportType = TransportTypes.StdIo,
Location = "/path/to/server",
TransportOptions = new Dictionary<string, string>
{
["arguments"] = "--test arg",
["workingDirectory"] = "/working/dir"
}
};
// Create a mock transport
var mockTransport = new Mock<IClientTransport>();
mockTransport.Setup(t => t.ConnectAsync(It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
mockTransport.Setup(t => t.IsConnected).Returns(true);
mockTransport.Setup(t => t.MessageReader).Returns(Mock.Of<ChannelReader<IJsonRpcMessage>>());
// Create a mock client
var mockClient = new Mock<IMcpClient>();
mockClient.Setup(c => c.ConnectAsync(It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
mockClient.Setup(c => c.IsInitialized).Returns(true);
// Inject the mock transport into the factory
using var factory = new McpClientFactory(
[config],
_defaultOptions,
NullLoggerFactory.Instance,
transportFactoryMethod: _ => mockTransport.Object,
clientFactoryMethod: (_, _, _) => mockClient.Object
);
// Act
var client = await factory.GetClientAsync("test-server");
// Assert
Assert.NotNull(client);
// We could add more assertions here about the client's configuration
}
[Fact]
public async Task GetClientAsync_CalledTwice_ReturnsSameInstance()
{
// Arrange
var config = new McpServerConfig
{
Id = "test-server",
Name = "Test Server",
TransportType = TransportTypes.StdIo,
Location = "/path/to/server"
};
// Create a mock transport
var mockTransport = new Mock<IClientTransport>();
mockTransport.Setup(t => t.ConnectAsync(It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
mockTransport.Setup(t => t.IsConnected).Returns(true);
mockTransport.Setup(t => t.MessageReader).Returns(Mock.Of<ChannelReader<IJsonRpcMessage>>());
// Create a mock client
var mockClient = new Mock<IMcpClient>();
mockClient.Setup(c => c.ConnectAsync(It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
mockClient.Setup(c => c.IsInitialized).Returns(true);
using var factory = new McpClientFactory([config],
_defaultOptions,
NullLoggerFactory.Instance,
transportFactoryMethod: _ => mockTransport.Object,
clientFactoryMethod: (_, _, _) => mockClient.Object);
// Act
var client1 = await factory.GetClientAsync("test-server");
var client2 = await factory.GetClientAsync("test-server");
// Assert
Assert.Same(client1, client2);
}
[Fact]
public async Task GetClientAsync_WithInvalidServerId_ThrowsArgumentException()
{
// Arrange
using var factory = new McpClientFactory([],
_defaultOptions,
NullLoggerFactory.Instance);
// Act & Assert
await Assert.ThrowsAsync<ArgumentException>(
() => factory.GetClientAsync("non-existent-server")
);
}
[Fact]
public async Task GetClientAsync_WithUnsupportedTransport_ThrowsArgumentException()
{
// Arrange
var config = new McpServerConfig
{
Id = "test-server",
Name = "Test Server",
TransportType = "unsupported",
Location = "/path/to/server"
};
using var factory = new McpClientFactory([config], _defaultOptions,
NullLoggerFactory.Instance);
// Act & Assert
await Assert.ThrowsAsync<ArgumentException>(
() => factory.GetClientAsync("test-server")
);
}
[Fact]
public async Task GetClientAsync_WithNoTransportOptions_CreatesClientWithDefaults()
{
// Arrange
var config = new McpServerConfig
{
Id = "test-server",
Name = "Test Server",
TransportType = TransportTypes.StdIo,
Location = "/path/to/server"
};
// Create a mock transport
var mockTransport = new Mock<IClientTransport>();
mockTransport.Setup(t => t.ConnectAsync(It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
mockTransport.Setup(t => t.IsConnected).Returns(true);
mockTransport.Setup(t => t.MessageReader).Returns(Mock.Of<ChannelReader<IJsonRpcMessage>>());
// Create a mock client
var mockClient = new Mock<IMcpClient>();
mockClient.Setup(c => c.ConnectAsync(It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
mockClient.Setup(c => c.IsInitialized).Returns(true);
using var factory = new McpClientFactory([config],
_defaultOptions,
NullLoggerFactory.Instance,
transportFactoryMethod: _ => mockTransport.Object,
clientFactoryMethod: (_, _, _) => mockClient.Object);
// Act
var client = await factory.GetClientAsync("test-server");
// Assert
Assert.NotNull(client);
}
[Fact]
public void Constructor_WithDuplicateServerIds_ThrowsArgumentException()
{
// Arrange
McpServerConfig[] configs =
[
new McpServerConfig { Id = "duplicate", Name = "duplicate", TransportType = TransportTypes.StdIo, Location = "/path1" },
new McpServerConfig { Id = "duplicate", Name = "duplicate", TransportType = TransportTypes.StdIo, Location = "/path2" }
];
// Act & Assert
Assert.Throws<ArgumentException>(() => new McpClientFactory(configs, _defaultOptions,
NullLoggerFactory.Instance));
}
[Fact]
public async Task GetClientAsync_WithSseTransport_CanCreateClient()
{
// Arrange
var config = new McpServerConfig
{
Id = "test-server",
Name = "Test Server",
TransportType = TransportTypes.Sse,
Location = "http://localhost:8080"
};
// Create a mock transport
var mockTransport = new Mock<IClientTransport>();
mockTransport.Setup(t => t.ConnectAsync(It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
mockTransport.Setup(t => t.IsConnected).Returns(true);
mockTransport.Setup(t => t.MessageReader).Returns(Mock.Of<ChannelReader<IJsonRpcMessage>>());
// Create a mock client
var mockClient = new Mock<IMcpClient>();
mockClient.Setup(c => c.ConnectAsync(It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
mockClient.Setup(c => c.IsInitialized).Returns(true);
// Inject the mock transport into the factory
using var factory = new McpClientFactory(
[config],
_defaultOptions,
NullLoggerFactory.Instance,
transportFactoryMethod: _ => mockTransport.Object,
clientFactoryMethod: (_, _, _) => mockClient.Object
);
// Act
var client = await factory.GetClientAsync("test-server");
// Assert
Assert.NotNull(client);
}
[Fact]
public void McpFactory_WithSse_CreatesCorrectTransportOptions()
{
// Arrange
var config = new McpServerConfig
{
Id = "test-server",
Name = "Test Server",
TransportType = TransportTypes.Sse,
Location = "http://localhost:8080",
TransportOptions = new Dictionary<string, string>
{
["connectionTimeout"] = "10",
["maxReconnectAttempts"] = "2",
["reconnectDelay"] = "5",
["header.test"] = "the_header_value"
}
};
// Act
using var factory = new McpClientFactory(
[config],
_defaultOptions,
NullLoggerFactory.Instance
);
var transport = factory.TransportFactoryMethod(config) as SseClientTransport;
// Assert
Assert.NotNull(transport);
Assert.Equal(TimeSpan.FromSeconds(10), transport.Options.ConnectionTimeout);
Assert.Equal(2, transport.Options.MaxReconnectAttempts);
Assert.Equal(TimeSpan.FromSeconds(5), transport.Options.ReconnectDelay);
Assert.NotNull(transport.Options.AdditionalHeaders);
Assert.Equal("the_header_value", transport.Options.AdditionalHeaders["test"]);
}
[Fact]
public void McpFactory_WithSseAndNoOptions_CreatesDefaultTransportOptions()
{
// Arrange
var config = new McpServerConfig
{
Id = "test-server",
Name = "Test Server",
TransportType = TransportTypes.Sse,
Location = "http://localhost:8080"
};
var defaultOptions = new SseClientTransportOptions();
// Act
using var factory = new McpClientFactory(
[config],
_defaultOptions,
NullLoggerFactory.Instance
);
var transport = factory.TransportFactoryMethod(config) as SseClientTransport;
// Assert
Assert.NotNull(transport);
Assert.True(transport.Options.ConnectionTimeout == defaultOptions.ConnectionTimeout);
Assert.True(transport.Options.MaxReconnectAttempts == defaultOptions.MaxReconnectAttempts);
Assert.True(transport.Options.ReconnectDelay == defaultOptions.ReconnectDelay);
Assert.True(transport.Options.AdditionalHeaders == null && defaultOptions.AdditionalHeaders == null);
}
[Fact]
public void McpFactory_WithSseAndMissingOptions_CreatesCorrectTransportOptions()
{
// Arrange
var config = new McpServerConfig
{
Id = "test-server",
Name = "Test Server",
TransportType = TransportTypes.Sse,
Location = "http://localhost:8080",
TransportOptions = new Dictionary<string, string>
{
["connectionTimeout"] = "10",
["header.test"] = "the_header_value"
}
};
var defaultOptions = new SseClientTransportOptions();
// Act
using var factory = new McpClientFactory(
[config],
_defaultOptions,
NullLoggerFactory.Instance
);
var transport = factory.TransportFactoryMethod(config) as SseClientTransport;
// Assert
Assert.NotNull(transport);
Assert.Equal(TimeSpan.FromSeconds(10), transport.Options.ConnectionTimeout);
Assert.Equal(defaultOptions.MaxReconnectAttempts, transport.Options.MaxReconnectAttempts);
Assert.Equal(defaultOptions.ReconnectDelay, transport.Options.ReconnectDelay);
Assert.NotNull(transport.Options.AdditionalHeaders);
Assert.Equal("the_header_value", transport.Options.AdditionalHeaders["test"]);
}
[Theory]
[InlineData("connectionTimeout", "not_a_number")]
[InlineData("maxReconnectAttempts", "invalid")]
[InlineData("reconnectDelay", "bad_value")]
public void 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<string, string>
{
[key] = value
}
};
// Act
using var factory = new McpClientFactory(
[config],
_defaultOptions,
NullLoggerFactory.Instance
);
// act & assert
Assert.Throws<FormatException>(() =>
factory.TransportFactoryMethod(config));
}
}

View file

@ -10,7 +10,6 @@ public class ClientIntegrationTestFixture : IDisposable
public ILoggerFactory LoggerFactory { get; }
public McpClientFactory Factory { get; }
public McpClientOptions DefaultOptions { get; }
public McpServerConfig DefaultConfig { get; }
public ClientIntegrationTestFixture()
{
@ -20,10 +19,11 @@ public class ClientIntegrationTestFixture : IDisposable
DefaultOptions = new()
{
ClientInfo = new() { Name = "IntegrationTestClient", Version = "1.0.0" }
ClientInfo = new() { Name = "IntegrationTestClient", Version = "1.0.0" },
Capabilities = new() { Sampling = new(), Roots = new() }
};
DefaultConfig = new McpServerConfig
var everythingServerConfig = new McpServerConfig
{
Id = "everything",
Name = "Everything",
@ -32,13 +32,28 @@ public class ClientIntegrationTestFixture : IDisposable
{
["command"] = "npx",
// Change to ["arguments"] = "mcp-server-everything" if you want to run the server locally after creating a symlink
["arguments"] = "-y @modelcontextprotocol/server-everything",
["arguments"] = "-y --verbose @modelcontextprotocol/server-everything"
}
};
var testServerConfig = new McpServerConfig
{
Id = "test_server",
Name = "TestServer",
TransportType = TransportTypes.StdIo,
TransportOptions = new Dictionary<string, string>
{
["command"] = OperatingSystem.IsWindows() ? "TestServer.exe" : "dotnet",
// Change to ["arguments"] = "mcp-server-everything" if you want to run the server locally after creating a symlink
}
};
if (!OperatingSystem.IsWindows())
testServerConfig.TransportOptions["arguments"] = "TestServer.dll";
// Inject the mock transport into the factory
Factory = new McpClientFactory(
[DefaultConfig],
[everythingServerConfig, testServerConfig],
DefaultOptions,
LoggerFactory
);
@ -46,6 +61,7 @@ public class ClientIntegrationTestFixture : IDisposable
public void Dispose()
{
Factory?.Dispose();
LoggerFactory?.Dispose();
GC.SuppressFinalize(this);
}

View file

@ -15,42 +15,50 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
_fixture = fixture;
}
[Fact]
public async Task ConnectAndPing_Stdio_EverythingServer()
public static IEnumerable<object[]> GetClients()
{
yield return ["everything"];
yield return ["test_server"];
}
[Theory]
[MemberData(nameof(GetClients))]
public async Task ConnectAndPing_Stdio(string clientId)
{
// Arrange
// Act
var client = await _fixture.Factory.GetClientAsync("everything");
var client = await _fixture.Factory.GetClientAsync(clientId);
await client.PingAsync(CancellationToken.None);
// Assert
Assert.NotNull(client);
}
[Fact]
public async Task Connect_EverythingServer_ShouldProvideServerFields()
[Theory]
[MemberData(nameof(GetClients))]
public async Task Connect_ShouldProvideServerFields(string clientId)
{
// Arrange
// Act
var client = await _fixture.Factory.GetClientAsync("everything");
var client = await _fixture.Factory.GetClientAsync(clientId);
// Assert
Assert.NotNull(client.ServerCapabilities);
Assert.NotNull(client.ServerInfo);
// Note: Comment the below assertion back when the everything server is updated to provide instructions
// Assert.NotNull(client.ServerInstructions);
if (clientId != "everything") // Note: Comment the below assertion back when the everything server is updated to provide instructions
Assert.NotNull(client.ServerInstructions);
}
[Fact]
public async Task ListTools_Stdio_EverythingServer()
[Theory]
[MemberData(nameof(GetClients))]
public async Task ListTools_Stdio(string clientId)
{
// arrange
// act
var client = await _fixture.Factory.GetClientAsync("everything");
var client = await _fixture.Factory.GetClientAsync(clientId);
var tools = await client.ListToolsAsync();
// assert
@ -59,13 +67,14 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
// We could add more specific assertions about expected tools
}
[Fact]
public async Task CallTool_Stdio_EchoServer()
[Theory]
[MemberData(nameof(GetClients))]
public async Task CallTool_Stdio_EchoServer(string clientId)
{
// arrange
// act
var client = await _fixture.Factory.GetClientAsync("everything");
var client = await _fixture.Factory.GetClientAsync(clientId);
var result = await client.CallToolAsync(
"echo",
new Dictionary<string, object>
@ -82,13 +91,14 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
Assert.Equal("Echo: Hello MCP!", textContent.Text);
}
[Fact]
public async Task ListPrompts_Stdio_EverythingServer()
[Theory]
[MemberData(nameof(GetClients))]
public async Task ListPrompts_Stdio(string clientId)
{
// arrange
// act
var client = await _fixture.Factory.GetClientAsync("everything");
var client = await _fixture.Factory.GetClientAsync(clientId);
var prompts = await client.ListPromptsAsync();
// assert
@ -99,13 +109,14 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
Assert.Contains(prompts.Prompts, p => p.Name == "complex_prompt");
}
[Fact]
public async Task GetPrompt_Stdio_SimplePrompt()
[Theory]
[MemberData(nameof(GetClients))]
public async Task GetPrompt_Stdio_SimplePrompt(string clientId)
{
// arrange
// act
var client = await _fixture.Factory.GetClientAsync("everything");
var client = await _fixture.Factory.GetClientAsync(clientId);
var result = await client.GetPromptAsync("simple_prompt", null, CancellationToken.None);
// assert
@ -113,13 +124,14 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
Assert.NotEmpty(result.Messages);
}
[Fact]
public async Task GetPrompt_Stdio_ComplexPrompt()
[Theory]
[MemberData(nameof(GetClients))]
public async Task GetPrompt_Stdio_ComplexPrompt(string clientId)
{
// arrange
// act
var client = await _fixture.Factory.GetClientAsync("everything");
var client = await _fixture.Factory.GetClientAsync(clientId);
var arguments = new Dictionary<string, object>
{
{ "temperature", "0.7" },
@ -132,24 +144,26 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
Assert.NotEmpty(result.Messages);
}
[Fact]
public async Task GetPrompt_NonExistent_ThrowsException()
[Theory]
[MemberData(nameof(GetClients))]
public async Task GetPrompt_NonExistent_ThrowsException(string clientId)
{
// arrange
// act
var client = await _fixture.Factory.GetClientAsync("everything");
var client = await _fixture.Factory.GetClientAsync(clientId);
await Assert.ThrowsAsync<McpClientException>(() =>
client.GetPromptAsync("non_existent_prompt", null, CancellationToken.None));
}
[Fact]
public async Task ListResources_Stdio_EverythingServer()
[Theory]
[MemberData(nameof(GetClients))]
public async Task ListResources_Stdio(string clientId)
{
// arrange
// act
var client = await _fixture.Factory.GetClientAsync("everything");
var client = await _fixture.Factory.GetClientAsync(clientId);
List<Resource> allResources = [];
string? cursor = null;
@ -161,17 +175,18 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
}
while (cursor != null);
// The everything server provides 100 test resources
// The server provides 100 test resources
Assert.Equal(100, allResources.Count);
}
[Fact]
public async Task ReadResource_Stdio_TextResource()
[Theory]
[MemberData(nameof(GetClients))]
public async Task ReadResource_Stdio_TextResource(string clientId)
{
// arrange
// act
var client = await _fixture.Factory.GetClientAsync("everything");
var client = await _fixture.Factory.GetClientAsync(clientId);
// Odd numbered resources are text in the everything server (despite the docs saying otherwise)
// 1 is index 0, which is "even" in the 0-based index
var result = await client.ReadResourceAsync("test://static/resource/1", CancellationToken.None);
@ -181,13 +196,14 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
Assert.NotNull(result.Contents[0].Text);
}
[Fact]
public async Task ReadResource_Stdio_BinaryResource()
[Theory]
[MemberData(nameof(GetClients))]
public async Task ReadResource_Stdio_BinaryResource(string clientId)
{
// arrange
// act
var client = await _fixture.Factory.GetClientAsync("everything");
var client = await _fixture.Factory.GetClientAsync(clientId);
// Even numbered resources are binary in the everything server (despite the docs saying otherwise)
// 2 is index 1, which is "odd" in the 0-based index
var result = await client.ReadResourceAsync("test://static/resource/2", CancellationToken.None);
@ -197,23 +213,14 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
Assert.NotNull(result.Contents[0].Blob);
}
/// <summary>
/// Note that as of 19th January 2025, the everything server published to npx does not support prompt completion.
/// However, the prompt completion is implemented in the github.com/modelcontextprotocol/servers repository.
/// You can clone this repo, and build locally then change the config to use a local symlink instead of npx until this is fixed, if you wish to run this test.
/// TransportOptions = new Dictionary<string, string>
/// {
/// ["command"] = "npx",
/// ["arguments"] = "mcp-server-everything" // changed from "-y @modelcontextprotocol/server-everything"
/// }
/// </summary>
[Fact]
public async Task GetCompletion_Stdio_ResourceReference()
[Theory]
[MemberData(nameof(GetClients))]
public async Task GetCompletion_Stdio_ResourceReference(string clientId)
{
// arrange
// act
var client = await _fixture.Factory.GetClientAsync("everything");
var client = await _fixture.Factory.GetClientAsync(clientId);
var result = await client.GetCompletionAsync(new Reference
{
Type = "ref/resource",
@ -228,23 +235,14 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
Assert.Equal("1", result.Completion.Values[0]);
}
/// <summary>
/// Note that as of 19th January 2025, the everything server published to npx does not support prompt completion.
/// However, the prompt completion is implemented in the github.com/modelcontextprotocol/servers repository.
/// You can clone this repo, and build locally then change the config to use a local symlink instead of npx until this is fixed, if you wish to run this test.
/// TransportOptions = new Dictionary<string, string>
/// {
/// ["command"] = "npx",
/// ["arguments"] = "mcp-server-everything" // changed from "-y @modelcontextprotocol/server-everything"
/// }
/// </summary>
[Fact]
public async Task GetCompletion_Stdio_PromptReference()
[Theory]
[MemberData(nameof(GetClients))]
public async Task GetCompletion_Stdio_PromptReference(string clientId)
{
// arrange
// act
var client = await _fixture.Factory.GetClientAsync("everything");
var client = await _fixture.Factory.GetClientAsync(clientId);
var result = await client.GetCompletionAsync(new Reference
{
Type = "ref/prompt",
@ -259,45 +257,18 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
Assert.Equal("formal", result.Completion.Values[0]);
}
[Fact]
public async Task Sampling_Stdio_EverythingServer()
[Theory]
[MemberData(nameof(GetClients))]
public async Task Sampling_Stdio(string clientId)
{
// arrange
var config = new McpServerConfig
{
Id = "everything",
Name = "Everything",
TransportType = TransportTypes.StdIo,
TransportOptions = new Dictionary<string, string>
{
["command"] = "npx",
["arguments"] = "-y @modelcontextprotocol/server-everything",
}
};
var options = new McpClientOptions
{
ClientInfo = new() { Name = "IntegrationTestClient", Version = "1.0.0" },
Capabilities = new ClientCapabilities
{
Sampling = new()
}
};
var factory = new McpClientFactory(
[config],
options,
_fixture.LoggerFactory
);
var client = await factory.GetClientAsync("everything");
var client = await _fixture.Factory.GetClientAsync(clientId);
// Set up the sampling handler
int samplingHandlerCalls = 0;
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
client.SamplingHandler = async (_, _) =>
client.SamplingHandler = (_, _) =>
{
samplingHandlerCalls++;
return new CreateMessageResult
return Task.FromResult(new CreateMessageResult
{
Model = "test-model",
Role = "assistant",
@ -306,9 +277,8 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
Type = "text",
Text = "Test response"
}
};
});
};
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
// Call the server's sampleLLM tool which should trigger our sampling handler
var result = await client.CallToolAsync(
@ -327,88 +297,42 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
Assert.False(string.IsNullOrEmpty(textContent.Text));
}
[Fact]
public async Task Roots_Stdio_EverythingServer()
//[Theory]
//[MemberData(nameof(GetClients))]
//public async Task Roots_Stdio_EverythingServer(string clientId)
//{
// var rootsHandlerCalls = 0;
// var testRoots = new List<Root>
// {
// new() { Uri = "file:///test/root1", Name = "Test Root 1" },
// new() { Uri = "file:///test/root2", Name = "Test Root 2" }
// };
// var client = await _fixture.Factory.GetClientAsync(clientId);
// // Set up the roots handler
// client.RootsHandler = (request, ct) =>
// {
// rootsHandlerCalls++;
// return Task.FromResult(new ListRootsResult
// {
// Roots = testRoots
// });
// };
// // Connect
// await client.ConnectAsync(CancellationToken.None);
// // assert
// // nothing to assert, no servers implement roots, so we if no exception is thrown, it's a success
// Assert.True(true);
//}
[Theory]
[MemberData(nameof(GetClients))]
public async Task Notifications_Stdio(string clientId)
{
// arrange
var config = new McpServerConfig
{
Id = "everything",
Name = "everything",
TransportType = TransportTypes.StdIo,
TransportOptions = new Dictionary<string, string>
{
["command"] = "npx",
["arguments"] = "-y @modelcontextprotocol/server-everything"
}
};
var options = new McpClientOptions
{
ClientInfo = new() { Name = "IntegrationTestClient", Version = "1.0.0" },
Capabilities = new ClientCapabilities
{
Roots = new()
}
};
var rootsHandlerCalls = 0;
var testRoots = new List<Root>
{
new() { Uri = "file:///test/root1", Name = "Test Root 1" },
new() { Uri = "file:///test/root2", Name = "Test Root 2" }
};
var factory = new McpClientFactory(
[config],
options,
_fixture.LoggerFactory
);
var client = await factory.GetClientAsync("everything");
// Set up the roots handler
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
client.RootsHandler = async (request, ct) =>
{
rootsHandlerCalls++;
return new ListRootsResult
{
Roots = testRoots
};
};
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
// Connect
await client.ConnectAsync(CancellationToken.None);
// assert
// nothing to assert, no servers implement roots, so we if no exception is thrown, it's a success
Assert.True(true);
}
[Fact]
public async Task Notifications_Stdio_EverythingServer()
{
// arrange
var config = new McpServerConfig
{
Id = "everything",
Name = "everything",
TransportType = TransportTypes.StdIo,
TransportOptions = new Dictionary<string, string>
{
["command"] = "npx",
["arguments"] = "-y @modelcontextprotocol/server-everything"
}
};
var options = new McpClientOptions
{
ClientInfo = new() { Name = "IntegrationTestClient", Version = "1.0.0" }
};
var factory = new McpClientFactory([config], options, _fixture.LoggerFactory);
var client = await factory.GetClientAsync("everything");
var client = await _fixture.Factory.GetClientAsync(clientId);
await client.ConnectAsync();
@ -442,7 +366,7 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
ClientInfo = new() { Name = "IntegrationTestClient", Version = "1.0.0" }
};
var factory = new McpClientFactory([config], options, _fixture.LoggerFactory);
using var factory = new McpClientFactory([config], options, _fixture.LoggerFactory);
var client = await factory.GetClientAsync("memory");
await client.ConnectAsync();
@ -457,6 +381,8 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
// assert
Assert.NotNull(result);
Assert.False(result.IsError);
var textContent = Assert.Single(result.Content, c => c.Type == "text");
Assert.Single(result.Content, c => c.Type == "text");
await client.DisposeAsync();
}
}

View file

@ -0,0 +1,138 @@
using McpDotNet.Configuration;
using McpDotNet.Protocol.Types;
using McpDotNet.Server;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Moq;
namespace McpDotNet.Tests.Configuration;
public class McpServerBuilderExtensionsHandlerTests
{
private readonly Mock<IMcpServerBuilder> _builder;
private readonly ServiceCollection _services;
public McpServerBuilderExtensionsHandlerTests()
{
_services = new ServiceCollection();
_builder = new Mock<IMcpServerBuilder>();
_builder.SetupGet(b => b.Services).Returns(_services);
}
[Fact]
public void WithListToolsHandler_Sets_Handler()
{
Func<RequestContext<ListToolsRequestParams>, CancellationToken, Task<ListToolsResult>> handler = (context, token) => Task.FromResult(new ListToolsResult());
_builder.Object.WithListToolsHandler(handler);
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
Assert.Equal(handler, options.ListToolsHandler);
}
[Fact]
public void WithCallToolHandler_Sets_Handler()
{
Func<RequestContext<CallToolRequestParams>, CancellationToken, Task<CallToolResponse>> handler = (context, token) => Task.FromResult(new CallToolResponse());
_builder.Object.WithCallToolHandler(handler);
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
Assert.Equal(handler, options.CallToolHandler);
}
[Fact]
public void WithListPromptsHandler_Sets_Handler()
{
Func<RequestContext<ListPromptsRequestParams>, CancellationToken, Task<ListPromptsResult>> handler = (context, token) => Task.FromResult(new ListPromptsResult());
_builder.Object.WithListPromptsHandler(handler);
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
Assert.Equal(handler, options.ListPromptsHandler);
}
[Fact]
public void WithGetPromptHandler_Sets_Handler()
{
Func<RequestContext<GetPromptRequestParams>, CancellationToken, Task<GetPromptResult>> handler = (context, token) => Task.FromResult(new GetPromptResult());
_builder.Object.WithGetPromptHandler(handler);
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
Assert.Equal(handler, options.GetPromptHandler);
}
[Fact]
public void WithListResourcesHandler_Sets_Handler()
{
Func<RequestContext<ListResourcesRequestParams>, CancellationToken, Task<ListResourcesResult>> handler = (context, token) => Task.FromResult(new ListResourcesResult());
_builder.Object.WithListResourcesHandler(handler);
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
Assert.Equal(handler, options.ListResourcesHandler);
}
[Fact]
public void WithReadResourceHandler_Sets_Handler()
{
Func<RequestContext<ReadResourceRequestParams>, CancellationToken, Task<ReadResourceResult>> handler = (context, token) => Task.FromResult(new ReadResourceResult());
_builder.Object.WithReadResourceHandler(handler);
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
Assert.Equal(handler, options.ReadResourceHandler);
}
[Fact]
public void WithGetCompletionHandler_Sets_Handler()
{
Func<RequestContext<CompleteRequestParams>, CancellationToken, Task<CompleteResult>> handler = (context, token) => Task.FromResult(new CompleteResult());
_builder.Object.WithGetCompletionHandler(handler);
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
Assert.Equal(handler, options.GetCompletionHandler);
}
[Fact]
public void WithSubscribeToResourcesHandler_Sets_Handler()
{
Func<RequestContext<string>, CancellationToken, Task> handler = (context, token) => Task.CompletedTask;
_builder.Object.WithSubscribeToResourcesHandler(handler);
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
Assert.Equal(handler, options.SubscribeToResourcesHandler);
}
[Fact]
public void WithUnsubscribeFromResourcesHandler_Sets_Handler()
{
Func<RequestContext<string>, CancellationToken, Task> handler = (context, token) => Task.CompletedTask;
_builder.Object.WithUnsubscribeFromResourcesHandler(handler);
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
Assert.Equal(handler, options.UnsubscribeFromResourcesHandler);
}
}

View file

@ -0,0 +1,444 @@
using System.ComponentModel;
using System.Text.Json;
using McpDotNet.Configuration;
using McpDotNet.Server;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Moq;
namespace McpDotNet.Tests.Configuration;
public class McpServerBuilderExtensionsToolsTests
{
private readonly Mock<IMcpServerBuilder> _builder;
private readonly ServiceCollection _services;
public McpServerBuilderExtensionsToolsTests()
{
_services = new ServiceCollection();
_builder = new Mock<IMcpServerBuilder>();
_builder.SetupGet(b => b.Services).Returns(_services);
}
[Fact]
public void Adds_Tools_To_Server()
{
_builder.Object.WithTools(typeof(EchoTool));
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
Assert.NotNull(options.ListToolsHandler);
Assert.NotNull(options.CallToolHandler);
}
[Fact]
public async Task Can_List_Registered_Tool()
{
_builder.Object.WithTools(typeof(EchoTool));
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
var result = await options.ListToolsHandler!(new(Mock.Of<IMcpServer>(), new()), CancellationToken.None);
Assert.NotNull(result);
Assert.NotEmpty(result.Tools);
var tool = result.Tools[0];
Assert.Equal("Echo", tool.Name);
Assert.Equal("Echoes the input back to the client.", tool.Description);
Assert.NotNull(tool.InputSchema);
Assert.Equal("object", tool.InputSchema.Type);
Assert.NotNull(tool.InputSchema.Properties);
Assert.NotEmpty(tool.InputSchema.Properties);
Assert.Contains("message", tool.InputSchema.Properties);
Assert.Equal("string", tool.InputSchema.Properties["message"].Type);
Assert.Equal("the echoes message", tool.InputSchema.Properties["message"].Description);
Assert.NotNull(tool.InputSchema.Required);
Assert.NotEmpty(tool.InputSchema.Required);
Assert.Contains("message", tool.InputSchema.Required);
tool = result.Tools[1];
Assert.Equal("double_echo", tool.Name);
Assert.Equal("Echoes the input back to the client.", tool.Description);
}
[Fact]
public async Task Can_Call_Registered_Tool()
{
_builder.Object.WithTools(typeof(EchoTool));
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
var result = await options.CallToolHandler!(new(Mock.Of<IMcpServer>(), new() { Name = "Echo", Arguments = new() { { "message", "Peter" } } }), CancellationToken.None);
Assert.NotNull(result);
Assert.NotNull(result.Content);
Assert.NotEmpty(result.Content);
Assert.Equal("hello Peter", result.Content[0].Text);
Assert.Equal("text", result.Content[0].Type);
}
[Fact]
public async Task Can_Call_Registered_Tool_With_Array_Result()
{
_builder.Object.WithTools(typeof(EchoTool));
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
var result = await options.CallToolHandler!(new(Mock.Of<IMcpServer>(), new() { Name = "EchoArray", Arguments = new() { { "message", "Peter" } } }), CancellationToken.None);
Assert.NotNull(result);
Assert.NotNull(result.Content);
Assert.NotEmpty(result.Content);
Assert.Equal("hello Peter", result.Content[0].Text);
Assert.Equal("hello2 Peter", result.Content[1].Text);
}
[Fact]
public async Task Can_Call_Registered_Tool_With_Null_Result()
{
_builder.Object.WithTools(typeof(EchoTool));
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
var result = await options.CallToolHandler!(new(Mock.Of<IMcpServer>(), new() { Name = "ReturnNull" }), CancellationToken.None);
Assert.NotNull(result);
Assert.NotNull(result.Content);
Assert.NotEmpty(result.Content);
Assert.Equal("null", result.Content[0].Text);
Assert.Empty(result.Content[0].Type);
}
[Fact]
public async Task Can_Call_Registered_Tool_With_Json_Result()
{
_builder.Object.WithTools(typeof(EchoTool));
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
var result = await options.CallToolHandler!(new(Mock.Of<IMcpServer>(), new() { Name = "ReturnJson" }), CancellationToken.None);
Assert.NotNull(result);
Assert.NotNull(result.Content);
Assert.NotEmpty(result.Content);
Assert.Equal("{\"SomeProp\": false}", result.Content[0].Text);
Assert.Equal("text", result.Content[0].Type);
}
[Fact]
public async Task Can_Call_Registered_Tool_With_Int_Result()
{
_builder.Object.WithTools(typeof(EchoTool));
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
var result = await options.CallToolHandler!(new(Mock.Of<IMcpServer>(), new() { Name = "ReturnInteger" }), CancellationToken.None);
Assert.NotNull(result);
Assert.NotNull(result.Content);
Assert.NotEmpty(result.Content);
Assert.Equal("5", result.Content[0].Text);
Assert.Equal("text", result.Content[0].Type);
}
[Fact]
public async Task Can_Call_Registered_Tool_And_Pass_Cancellation_Token()
{
_builder.Object.WithTools(typeof(EchoTool));
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
using var cts = new CancellationTokenSource();
var token = cts.Token;
var result = await options.CallToolHandler!(new(Mock.Of<IMcpServer>(), new() { Name = "ReturnCancellationToken" }), token);
Assert.NotNull(result);
Assert.NotNull(result.Content);
Assert.NotEmpty(result.Content);
Assert.Equal(token.GetHashCode().ToString(), result.Content[0].Text);
}
[Fact]
public async Task Can_Call_Registered_Tool_And_Returns_Cancelled_Response()
{
_builder.Object.WithTools(typeof(EchoTool));
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
using var cts = new CancellationTokenSource();
var token = cts.Token;
await cts.CancelAsync();
var action = async () => await options.CallToolHandler!(new(Mock.Of<IMcpServer>(), new() { Name = "ReturnCancellationToken" }), token);
await Assert.ThrowsAsync<OperationCanceledException>(action);
}
[Fact]
public async Task Can_Call_Registered_Tool_And_Pass_ComplexType()
{
_builder.Object.WithTools(typeof(EchoTool));
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
var result = await options.CallToolHandler!(new(Mock.Of<IMcpServer>(), new() { Name = "EchoComplex", Arguments = new() { { "complex", JsonDocument.Parse("{\"Name\": \"Peter\", \"Age\": 25}").RootElement } } }), CancellationToken.None);
Assert.NotNull(result);
Assert.NotNull(result.Content);
Assert.NotEmpty(result.Content);
Assert.Equal("Peter", result.Content[0].Text);
Assert.Equal("text", result.Content[0].Type);
}
[Fact]
public async Task Throws_Exception_When_Tool_Fails()
{
_builder.Object.WithTools(typeof(EchoTool));
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
var action = async () => await options.CallToolHandler!(new(Mock.Of<IMcpServer>(), new() { Name = "ReturnError" }), CancellationToken.None);
var exception = await Assert.ThrowsAsync<InvalidOperationException>(action);
Assert.Equal("Test error", exception.Message);
}
[Fact]
public async Task Can_Call_Registered_Tool_With_Dependency_Injection()
{
bool dependencyInjectionCalled = false;
_services.AddTransient(sp =>
{
dependencyInjectionCalled = true;
return Mock.Of<IDependendService>();
});
_builder.Object.WithTool<EchoToolWithDi>();
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
var mcpServer = new Mock<IMcpServer>();
mcpServer.SetupGet(s => s.ServiceProvider).Returns(serviceProvider);
var result = await options.CallToolHandler!(new(mcpServer.Object, new() { Name = "Echo", Arguments = new() { { "message", "Peter" } } }), CancellationToken.None);
Assert.NotNull(result);
Assert.NotNull(result.Content);
Assert.NotEmpty(result.Content);
Assert.Equal("hello Peter", result.Content[0].Text);
Assert.True(dependencyInjectionCalled);
}
[Fact]
public async Task Throws_Exception_On_Unknown_Tool()
{
_builder.Object.WithTools(typeof(EchoTool));
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
var exception = await Assert.ThrowsAsync<McpServerException>(async () => await options.CallToolHandler!(new(Mock.Of<IMcpServer>(), new() { Name = "NotRegisteredTool" }), CancellationToken.None));
Assert.Equal("Unknown tool: NotRegisteredTool", exception.Message);
}
[Fact]
public async Task Throws_Exception_Missing_Parameter()
{
_builder.Object.WithTools(typeof(EchoTool));
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
var exception = await Assert.ThrowsAsync<McpServerException>(async () => await options.CallToolHandler!(new(Mock.Of<IMcpServer>(), new() { Name = "Echo" }), CancellationToken.None));
Assert.Equal("Missing required argument 'message'.", exception.Message);
}
[Fact]
public void Throws_Exception_For_Null_Types()
{
var action = () => _builder.Object.WithTools(toolTypes: null!);
var exception = Assert.Throws<ArgumentException>(action);
Assert.Equal("At least one tool type must be provided. (Parameter 'toolTypes')", exception.Message);
}
[Fact]
public void Throws_Exception_For_Empty_Types()
{
var action = () => _builder.Object.WithTools(toolTypes: []);
var exception = Assert.Throws<ArgumentException>(action);
Assert.Equal("At least one tool type must be provided. (Parameter 'toolTypes')", exception.Message);
}
[Fact]
public async Task Register_Tools_From_Current_Assembly()
{
_builder.Object.WithTools();
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
var result = await options.ListToolsHandler!(new(Mock.Of<IMcpServer>(), new()), CancellationToken.None);
Assert.NotNull(result);
Assert.NotEmpty(result.Tools);
var tool = result.Tools[0];
Assert.Equal("Echo", tool.Name);
}
[Fact]
public void Throws_Exception_When_No_Tools_Are_Found_In_Given_Assembly()
{
var action = () => _builder.Object.WithToolsFromAssembly(typeof(Mock).Assembly);
var exception = Assert.Throws<ArgumentException>(action);
Assert.Equal("No types with marked methods found in the assembly. (Parameter 'assembly')", exception.Message);
}
[Fact]
public async Task Recognizes_Parameter_Types()
{
_builder.Object.WithTools(typeof(EchoTool));
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
var result = await options.ListToolsHandler!(new(Mock.Of<IMcpServer>(), new()), CancellationToken.None);
Assert.NotNull(result);
Assert.NotEmpty(result.Tools);
var tool = result.Tools.First(t => t.Name == "TestTool");
Assert.Equal("TestTool", tool.Name);
Assert.Null(tool.Description);
Assert.NotNull(tool.InputSchema);
Assert.Equal("object", tool.InputSchema.Type);
Assert.NotNull(tool.InputSchema.Properties);
Assert.NotEmpty(tool.InputSchema.Properties);
Assert.Contains("number", tool.InputSchema.Properties);
Assert.Equal("number", tool.InputSchema.Properties["number"].Type);
Assert.Contains("otherNumber", tool.InputSchema.Properties);
Assert.Equal("number", tool.InputSchema.Properties["otherNumber"].Type);
Assert.Contains("someCheck", tool.InputSchema.Properties);
Assert.Equal("boolean", tool.InputSchema.Properties["someCheck"].Type);
Assert.Contains("someDate", tool.InputSchema.Properties);
Assert.Equal("string", tool.InputSchema.Properties["someDate"].Type);
Assert.Contains("someOtherDate", tool.InputSchema.Properties);
Assert.Equal("string", tool.InputSchema.Properties["someOtherDate"].Type);
Assert.Contains("data", tool.InputSchema.Properties);
Assert.Equal("array", tool.InputSchema.Properties["data"].Type);
Assert.Contains("complexObject", tool.InputSchema.Properties);
Assert.Equal("object", tool.InputSchema.Properties["complexObject"].Type);
}
[McpToolType]
public static class EchoTool
{
[McpTool, Description("Echoes the input back to the client.")]
public static string Echo([Description("the echoes message")] string message)
{
return "hello " + message;
}
[McpTool("double_echo"), Description("Echoes the input back to the client.")]
public static string Echo2(string message)
{
return "hello hello" + message;
}
[McpTool]
public static string TestTool(int number, double otherNumber, bool someCheck, DateTime someDate, DateTimeOffset someOtherDate, string[] data, ComplexObject complexObject)
{
return "hello hello";
}
[McpTool]
public static string[] EchoArray(string message)
{
return ["hello " + message, "hello2 " + message];
}
[McpTool]
public static string? ReturnNull()
{
return null;
}
[McpTool]
public static JsonElement ReturnJson()
{
return JsonDocument.Parse("{\"SomeProp\": false}").RootElement;
}
[McpTool]
public static int ReturnInteger()
{
return 5;
}
[McpTool]
public static string ReturnError()
{
throw new InvalidOperationException("Test error");
}
[McpTool]
public static int ReturnCancellationToken(CancellationToken cancellationToken)
{
return cancellationToken.GetHashCode();
}
[McpTool]
public static string EchoComplex(ComplexObject complex)
{
return complex.Name!;
}
}
public interface IDependendService
{
}
private class EchoToolWithDi
{
public EchoToolWithDi(IDependendService service)
{
}
[McpTool, Description("Echoes the input back to the client.")]
#pragma warning disable CA1822 // Mark members as static
public Task<string> Echo(string message)
#pragma warning restore CA1822 // Mark members as static
{
return Task.FromResult("hello " + message);
}
}
public class ComplexObject
{
public string? Name { get; set; }
public int Age { get; set; }
}
}

View file

@ -0,0 +1,23 @@
using McpDotNet.Configuration;
using McpDotNet.Protocol.Transport;
using Microsoft.Extensions.DependencyInjection;
using Moq;
namespace McpDotNet.Tests.Configuration;
public class McpServerBuilderExtensionsTransportsTests
{
[Fact]
public void WithStdioServerTransport_Sets_Transport()
{
var services = new ServiceCollection();
var builder = new Mock<IMcpServerBuilder>();
builder.SetupGet(b => b.Services).Returns(services);
builder.Object.WithStdioServerTransport();
var transportType = services.FirstOrDefault(s => s.ServiceType == typeof(IServerTransport));
Assert.NotNull(transportType);
Assert.Equal(typeof(StdioServerTransport), transportType.ImplementationType);
}
}

View file

@ -39,10 +39,10 @@ public class McpServerTests
}
[Fact]
public void Constructor_Should_Initialize_With_Valid_Parameters()
public async Task Constructor_Should_Initialize_With_Valid_Parameters()
{
// Arrange & Act
var server = new McpServer(_serverTransport.Object, _options, _loggerFactory.Object, _serviceProvider);
await using var server = new McpServer(_serverTransport.Object, _options, _loggerFactory.Object, _serviceProvider);
// Assert
Assert.NotNull(server);
@ -70,19 +70,19 @@ public class McpServerTests
}
[Fact]
public void Constructor_Does_Not_Throw_For_Null_ServiceProvider()
public async Task Constructor_Does_Not_Throw_For_Null_ServiceProvider()
{
// Arrange & Act
var server = new McpServer(_serverTransport.Object, _options, _loggerFactory.Object, null);
await using var server = new McpServer(_serverTransport.Object, _options, _loggerFactory.Object, null);
// Assert
Assert.NotNull(server);
}
[Fact]
public void Property_EndpointName_Return_Infos()
public async Task Property_EndpointName_Return_Infos()
{
var server = new McpServer(_serverTransport.Object, _options, _loggerFactory.Object, _serviceProvider);
await using var server = new McpServer(_serverTransport.Object, _options, _loggerFactory.Object, _serviceProvider);
server.ClientInfo = new Implementation { Name = "TestClient", Version = "1.1" };
Assert.Equal("Server (TestServer 1.0), Client (TestClient 1.1)", server.EndpointName);
}
@ -91,7 +91,7 @@ public class McpServerTests
public async Task StartAsync_Should_Throw_InvalidOperationException_If_Already_Initializing()
{
// Arrange
var server = new McpServer(_serverTransport.Object, _options, _loggerFactory.Object, _serviceProvider);
await using var server = new McpServer(_serverTransport.Object, _options, _loggerFactory.Object, _serviceProvider);
server.GetType().GetField("_isInitializing", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.SetValue(server, true);
// Act & Assert
@ -103,7 +103,7 @@ public class McpServerTests
public async Task StartAsync_Should_Do_Nothing_If_Already_Initialized()
{
// Arrange
var server = new McpServer(_serverTransport.Object, _options, _loggerFactory.Object, _serviceProvider);
await using var server = new McpServer(_serverTransport.Object, _options, _loggerFactory.Object, _serviceProvider);
server.IsInitialized = true;
await server.StartAsync();
@ -116,7 +116,7 @@ public class McpServerTests
public async Task StartAsync_ShouldStartListening()
{
// Arrange
var server = new McpServer(_serverTransport.Object, _options, _loggerFactory.Object, _serviceProvider);
await using var server = new McpServer(_serverTransport.Object, _options, _loggerFactory.Object, _serviceProvider);
// Act
await server.StartAsync();
@ -128,8 +128,8 @@ public class McpServerTests
[Fact]
public async Task StartAsync_Sets_Initialized_After_Transport_Responses_Initialized_Notification()
{
var transport = new TestServerTransport();
var server = new McpServer(transport, _options, _loggerFactory.Object, _serviceProvider);
await using var transport = new TestServerTransport();
await using var server = new McpServer(transport, _options, _loggerFactory.Object, _serviceProvider);
await server.StartAsync();
@ -150,7 +150,7 @@ public class McpServerTests
public async Task RequestSamplingAsync_Should_Throw_McpServerException_If_Client_Does_Not_Support_Sampling()
{
// Arrange
var server = new McpServer(_serverTransport.Object, _options, _loggerFactory.Object, _serviceProvider);
await using var server = new McpServer(_serverTransport.Object, _options, _loggerFactory.Object, _serviceProvider);
server.ClientCapabilities = new ClientCapabilities();
var action = () => server.RequestSamplingAsync(new CreateMessageRequestParams { Messages = [] }, CancellationToken.None);
@ -164,7 +164,7 @@ public class McpServerTests
public async Task RequestSamplingAsync_Should_SendRequest()
{
// Arrange
var transport = new TestServerTransport();
await using var transport = new TestServerTransport();
await using var server = new McpServer(transport, _options, _loggerFactory.Object, _serviceProvider);
server.ClientCapabilities = new ClientCapabilities { Sampling = new SamplingCapability() };
@ -183,7 +183,7 @@ public class McpServerTests
public async Task RequestRootsAsync_Should_Throw_McpServerException_If_Client_Does_Not_Support_Roots()
{
// Arrange
var server = new McpServer(_serverTransport.Object, _options, _loggerFactory.Object, _serviceProvider);
await using var server = new McpServer(_serverTransport.Object, _options, _loggerFactory.Object, _serviceProvider);
server.ClientCapabilities = new ClientCapabilities();
// Act & Assert
@ -195,7 +195,7 @@ public class McpServerTests
public async Task RequestRootsAsync_Should_SendRequest()
{
// Arrange
var transport = new TestServerTransport();
await using var transport = new TestServerTransport();
await using var server = new McpServer(transport, _options, _loggerFactory.Object, _serviceProvider);
server.ClientCapabilities = new ClientCapabilities { Roots = new RootsCapability() };
await server.StartAsync();
@ -213,7 +213,7 @@ public class McpServerTests
[Fact]
public async Task Throws_Exception_If_Not_Connected()
{
var server = new McpServer(_serverTransport.Object, _options, _loggerFactory.Object, _serviceProvider);
await using var server = new McpServer(_serverTransport.Object, _options, _loggerFactory.Object, _serviceProvider);
server.ClientCapabilities = new ClientCapabilities { Roots = new RootsCapability() };
_serverTransport.SetupGet(t => t.IsConnected).Returns(false);
@ -309,10 +309,7 @@ public class McpServerTests
{
return Task.FromResult(new ListResourcesResult
{
Resources = new()
{
new() { Uri = "test", Name = "Test Resource" }
}
Resources = [new() { Uri = "test", Name = "Test Resource" }]
});
};
@ -344,7 +341,7 @@ public class McpServerTests
{
return Task.FromResult(new ReadResourceResult
{
Contents = new List<ResourceContents> { new ResourceContents { Text = "test" } }
Contents = [new() { Text = "test" }]
});
};
},
@ -375,7 +372,7 @@ public class McpServerTests
{
return Task.FromResult(new ListPromptsResult
{
Prompts = new List<Prompt> { new Prompt { Name = "test" } }
Prompts = [new() { Name = "test" }]
});
};
},
@ -435,7 +432,7 @@ public class McpServerTests
{
return Task.FromResult(new ListToolsResult
{
Tools = new List<Tool> { new Tool { Name = "test" } }
Tools = [new() { Name = "test" }]
});
};
},
@ -487,10 +484,10 @@ public class McpServerTests
private async Task Can_Handle_Requests(ServerCapabilities? serverCapabilities, string method, Action<IMcpServer> configureServer, Action<object> assertResult)
{
var transport = new TestServerTransport();
await using var transport = new TestServerTransport();
var options = serverCapabilities == null ? _options : CreateOptions(serverCapabilities);
var server = new McpServer(transport, options, _loggerFactory.Object, _serviceProvider);
await using var server = new McpServer(transport, options, _loggerFactory.Object, _serviceProvider);
await server.StartAsync();
@ -521,10 +518,10 @@ public class McpServerTests
private async Task Throws_Exception_If_No_Handler_Assigned(ServerCapabilities serverCapabilities, string method, string expectedError)
{
var transport = new TestServerTransport();
await using var transport = new TestServerTransport();
var options = CreateOptions(serverCapabilities);
var server = new McpServer(transport, options, _loggerFactory.Object, _serviceProvider);
await using var server = new McpServer(transport, options, _loggerFactory.Object, _serviceProvider);
await server.StartAsync();

View file

@ -1,57 +0,0 @@
using McpDotNet.Client;
using McpDotNet.Configuration;
using McpDotNet.Protocol.Transport;
using Microsoft.Extensions.Logging;
namespace McpDotNet.Tests;
public class ServerIntegrationTestFixture : IDisposable
{
public ILoggerFactory LoggerFactory { get; }
public McpClientFactory Factory { get; }
public McpClientOptions DefaultOptions { get; }
public McpServerConfig DefaultConfig { get; }
public ServerIntegrationTestFixture()
{
LoggerFactory = Microsoft.Extensions.Logging.LoggerFactory.Create(builder =>
builder.AddConsole()
.SetMinimumLevel(LogLevel.Debug));
DefaultOptions = new()
{
ClientInfo = new() { Name = "IntegrationTestClient", Version = "1.0.0" },
Capabilities = new() { Sampling = new(), Roots = new() }
};
DefaultConfig = new McpServerConfig
{
Id = "test_server",
Name = "TestServer",
TransportType = TransportTypes.StdIo,
TransportOptions = new Dictionary<string, string>
{
["command"] = OperatingSystem.IsWindows() ? "TestServer.exe" : "dotnet",
// Change to ["arguments"] = "mcp-server-everything" if you want to run the server locally after creating a symlink
}
};
if (!OperatingSystem.IsWindows())
DefaultConfig.TransportOptions["arguments"] = "TestServer.dll";
// Inject the mock transport into the factory
Factory = new McpClientFactory(
[DefaultConfig],
DefaultOptions,
LoggerFactory
);
}
public void Dispose()
{
var client = Factory.GetClientAsync("test_server").Result;
client.DisposeAsync().AsTask().Wait();
LoggerFactory?.Dispose();
GC.SuppressFinalize(this);
}
}

Some files were not shown because too many files have changed in this diff Show more