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:
parent
fe632617dc
commit
b5076263d1
15 changed files with 883 additions and 751 deletions
|
|
@ -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()
|
||||
|
|
|
|||
368
tests/mcpdotnet.Tests/Client/McpClientFactoryTests.cs
Normal file
368
tests/mcpdotnet.Tests/Client/McpClientFactoryTests.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,237 +0,0 @@
|
|||
using McpDotNet.Client;
|
||||
using McpDotNet.Protocol.Types;
|
||||
|
||||
namespace McpDotNet.Tests;
|
||||
|
||||
public class ServerIntegrationTests : IClassFixture<ServerIntegrationTestFixture>
|
||||
{
|
||||
private readonly ServerIntegrationTestFixture _fixture;
|
||||
|
||||
public ServerIntegrationTests(ServerIntegrationTestFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConnectAndPing_Stdio_TestServer()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
var client = await _fixture.Factory.GetClientAsync("test_server");
|
||||
await client.PingAsync(CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(client);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Connect_TestServer_ShouldProvideServerFields()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
var client = await _fixture.Factory.GetClientAsync("test_server");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(client.ServerCapabilities);
|
||||
Assert.NotNull(client.ServerInfo);
|
||||
Assert.NotNull(client.ServerInstructions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListTools_Stdio_TestServer()
|
||||
{
|
||||
// arrange
|
||||
|
||||
// act
|
||||
var client = await _fixture.Factory.GetClientAsync("test_server");
|
||||
var tools = await client.ListToolsAsync();
|
||||
|
||||
// assert
|
||||
Assert.NotNull(tools);
|
||||
Assert.NotEmpty(tools.Tools);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CallTool_Stdio_EchoServer()
|
||||
{
|
||||
// arrange
|
||||
|
||||
// act
|
||||
var client = await _fixture.Factory.GetClientAsync("test_server");
|
||||
var result = await client.CallToolAsync(
|
||||
"echo",
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["message"] = "Hello MCP!"
|
||||
},
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
// assert
|
||||
Assert.NotNull(result);
|
||||
Assert.False(result.IsError);
|
||||
var textContent = Assert.Single(result.Content, c => c.Type == "text");
|
||||
Assert.Equal("Echo: Hello MCP!", textContent.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListResources_Stdio_TestServer()
|
||||
{
|
||||
// arrange
|
||||
|
||||
// act
|
||||
var client = await _fixture.Factory.GetClientAsync("test_server");
|
||||
|
||||
List<Resource> allResources = [];
|
||||
string? cursor = null;
|
||||
do
|
||||
{
|
||||
var resources = await client.ListResourcesAsync(cursor, CancellationToken.None);
|
||||
allResources.AddRange(resources.Resources);
|
||||
cursor = resources.NextCursor;
|
||||
}
|
||||
while (cursor != null);
|
||||
|
||||
// The everything server provides 100 test resources
|
||||
Assert.Equal(100, allResources.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadResource_Stdio_TextResource()
|
||||
{
|
||||
// arrange
|
||||
|
||||
// act
|
||||
var client = await _fixture.Factory.GetClientAsync("test_server");
|
||||
// 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
|
||||
// We copied this oddity to the test server
|
||||
var result = await client.ReadResourceAsync("test://static/resource/1", CancellationToken.None);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Single(result.Contents);
|
||||
Assert.NotNull(result.Contents[0].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadResource_Stdio_BinaryResource()
|
||||
{
|
||||
// arrange
|
||||
|
||||
// act
|
||||
var client = await _fixture.Factory.GetClientAsync("test_server");
|
||||
// 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
|
||||
// We copied this oddity to the test server
|
||||
var result = await client.ReadResourceAsync("test://static/resource/2", CancellationToken.None);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Single(result.Contents);
|
||||
Assert.NotNull(result.Contents[0].Blob);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListPrompts_Stdio_TestServer()
|
||||
{
|
||||
// arrange
|
||||
|
||||
// act
|
||||
var client = await _fixture.Factory.GetClientAsync("test_server");
|
||||
var prompts = await client.ListPromptsAsync();
|
||||
|
||||
// assert
|
||||
Assert.NotNull(prompts);
|
||||
Assert.NotEmpty(prompts.Prompts);
|
||||
// We could add specific assertions for the known prompts
|
||||
Assert.Contains(prompts.Prompts, p => p.Name == "simple_prompt");
|
||||
Assert.Contains(prompts.Prompts, p => p.Name == "complex_prompt");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetPrompt_Stdio_SimplePrompt()
|
||||
{
|
||||
// arrange
|
||||
|
||||
// act
|
||||
var client = await _fixture.Factory.GetClientAsync("test_server");
|
||||
var result = await client.GetPromptAsync("simple_prompt", null, CancellationToken.None);
|
||||
|
||||
// assert
|
||||
Assert.NotNull(result);
|
||||
Assert.NotEmpty(result.Messages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetPrompt_Stdio_ComplexPrompt()
|
||||
{
|
||||
// arrange
|
||||
|
||||
// act
|
||||
var client = await _fixture.Factory.GetClientAsync("test_server");
|
||||
var arguments = new Dictionary<string, object>
|
||||
{
|
||||
{ "temperature", 0.7 },
|
||||
{ "style", "formal" }
|
||||
};
|
||||
var result = await client.GetPromptAsync("complex_prompt", arguments, CancellationToken.None);
|
||||
|
||||
// assert
|
||||
Assert.NotNull(result);
|
||||
Assert.NotEmpty(result.Messages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetPrompt_NonExistent_ThrowsException()
|
||||
{
|
||||
// arrange
|
||||
|
||||
// act
|
||||
var client = await _fixture.Factory.GetClientAsync("test_server");
|
||||
await Assert.ThrowsAsync<McpClientException>(() =>
|
||||
client.GetPromptAsync("non_existent_prompt", null, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Sampling_Stdio_TestServer()
|
||||
{
|
||||
// arrange
|
||||
var client = await _fixture.Factory.GetClientAsync("test_server");
|
||||
|
||||
// Set up the sampling handler
|
||||
int samplingHandlerCalls = 0;
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
client.SamplingHandler = async (_, _) =>
|
||||
{
|
||||
samplingHandlerCalls++;
|
||||
return new CreateMessageResult
|
||||
{
|
||||
Model = "test-model",
|
||||
Role = "assistant",
|
||||
Content = new Content
|
||||
{
|
||||
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(
|
||||
"sampleLLM",
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["prompt"] = "Test prompt",
|
||||
["maxTokens"] = 100
|
||||
}
|
||||
);
|
||||
|
||||
// assert
|
||||
Assert.NotNull(result);
|
||||
var textContent = Assert.Single(result.Content);
|
||||
Assert.Equal("text", textContent.Type);
|
||||
Assert.False(string.IsNullOrEmpty(textContent.Text));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,173 +0,0 @@
|
|||
using System.Net;
|
||||
using McpDotNet.Configuration;
|
||||
using McpDotNet.Protocol.Messages;
|
||||
using McpDotNet.Protocol.Transport;
|
||||
using McpDotNet.Tests.Utils;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace McpDotNet.Tests.Client;
|
||||
|
||||
public class SseClientTransportTests
|
||||
{
|
||||
private readonly McpServerConfig _serverConfig;
|
||||
private readonly SseClientTransportOptions _transportOptions;
|
||||
|
||||
public SseClientTransportTests()
|
||||
{
|
||||
_serverConfig = new McpServerConfig
|
||||
{
|
||||
Id = "test-server",
|
||||
Name = "Test Server",
|
||||
TransportType = TransportTypes.Sse,
|
||||
Location = "http://localhost:8080"
|
||||
};
|
||||
|
||||
_transportOptions = new SseClientTransportOptions
|
||||
{
|
||||
ConnectionTimeout = TimeSpan.FromSeconds(2),
|
||||
MaxReconnectAttempts = 3,
|
||||
ReconnectDelay = TimeSpan.FromMilliseconds(50),
|
||||
AdditionalHeaders = new Dictionary<string, string>
|
||||
{
|
||||
["test"] = "header"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Should_Initialize_With_Valid_Parameters()
|
||||
{
|
||||
// Act
|
||||
var transport = new SseClientTransport(_transportOptions, _serverConfig, NullLoggerFactory.Instance);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(transport);
|
||||
Assert.Equal(TimeSpan.FromSeconds(2), transport.Options.ConnectionTimeout);
|
||||
Assert.Equal(3, transport.Options.MaxReconnectAttempts);
|
||||
Assert.Equal(TimeSpan.FromMilliseconds(50), transport.Options.ReconnectDelay);
|
||||
Assert.NotNull(transport.Options.AdditionalHeaders);
|
||||
Assert.Equal("header", transport.Options.AdditionalHeaders["test"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Throws_For_Null_Options()
|
||||
{
|
||||
var exception = Assert.Throws<ArgumentNullException>(() => new SseClientTransport(null!, _serverConfig, NullLoggerFactory.Instance));
|
||||
Assert.Equal("transportOptions", exception.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Throws_For_Null_Config()
|
||||
{
|
||||
var exception = Assert.Throws<ArgumentNullException>(() => new SseClientTransport(_transportOptions, null!, NullLoggerFactory.Instance));
|
||||
Assert.Equal("serverConfig", exception.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Throws_For_Null_HttpClientg()
|
||||
{
|
||||
var exception = Assert.Throws<ArgumentNullException>(() => new SseClientTransport(_transportOptions, _serverConfig, null!, NullLoggerFactory.Instance));
|
||||
Assert.Equal("httpClient", exception.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConnectAsync_Should_Connect_Successfully()
|
||||
{
|
||||
var mockHttpHandler = new MockHttpHandler();
|
||||
var httpClient = new HttpClient(mockHttpHandler);
|
||||
await using var transport = new SseClientTransport(_transportOptions, _serverConfig, httpClient, NullLoggerFactory.Instance);
|
||||
|
||||
bool firstCall = true;
|
||||
|
||||
mockHttpHandler.RequestHandler = async (request) =>
|
||||
{
|
||||
if (!firstCall)
|
||||
{
|
||||
Assert.True(transport.IsConnected);
|
||||
await transport.CloseAsync();
|
||||
}
|
||||
|
||||
firstCall = false;
|
||||
return new HttpResponseMessage
|
||||
{
|
||||
StatusCode = HttpStatusCode.OK,
|
||||
Content = new StringContent("event: endpoint\r\ndata: http://localhost\r\n\r\n")
|
||||
};
|
||||
};
|
||||
|
||||
await transport.ConnectAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConnectAsync_Throws_Exception_On_Failure()
|
||||
{
|
||||
var mockHttpHandler = new MockHttpHandler();
|
||||
var httpClient = new HttpClient(mockHttpHandler);
|
||||
await using var transport = new SseClientTransport(_transportOptions, _serverConfig, httpClient, NullLoggerFactory.Instance);
|
||||
|
||||
var retries = 0;
|
||||
mockHttpHandler.RequestHandler = (request) =>
|
||||
{
|
||||
retries++;
|
||||
throw new InvalidOperationException("Test exception");
|
||||
};
|
||||
|
||||
var action = async () => await transport.ConnectAsync();
|
||||
|
||||
var exception = await Assert.ThrowsAsync<McpTransportException>(action);
|
||||
Assert.Equal("Exceeded reconnect limit", exception.Message);
|
||||
|
||||
Assert.Equal(_transportOptions.MaxReconnectAttempts, retries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendMessageAsync_Throws_Exception_If_MessageEndpoint_Not_Set()
|
||||
{
|
||||
await using var transport = new SseClientTransport(_transportOptions, _serverConfig, NullLoggerFactory.Instance);
|
||||
|
||||
// Assert
|
||||
Assert.True(string.IsNullOrEmpty(transport.MessageEndpoint?.ToString()));
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => transport.SendMessageAsync(new JsonRpcRequest() { Method = "test" }, CancellationToken.None));
|
||||
}
|
||||
|
||||
//[Fact]
|
||||
//public async Task SendMessageAsync_Sends_Message()
|
||||
//{
|
||||
// var httpClient = new HttpClient(_mockHttpHandler);
|
||||
// await using var transport = new SseClientTransport(_transportOptions, _serverConfig, httpClient, NullLoggerFactory.Instance);
|
||||
|
||||
// _mockHttpHandler.RequestHandler = async (request) =>
|
||||
// {
|
||||
|
||||
// return new HttpResponseMessage
|
||||
// {
|
||||
// StatusCode = HttpStatusCode.OK,
|
||||
// Content = new StringContent("event: endpoint\r\ndata: /sseendpoint\r\n\r\n")
|
||||
// };
|
||||
// };
|
||||
|
||||
// await transport.ConnectAsync();
|
||||
|
||||
// await transport.SendMessageAsync(new JsonRpcRequest() { Method = "test" }, CancellationToken.None);
|
||||
//}
|
||||
|
||||
[Fact]
|
||||
public async Task CloseAsync_Should_Dispose_Resources()
|
||||
{
|
||||
var transport = new SseClientTransport(_transportOptions, _serverConfig, NullLoggerFactory.Instance);
|
||||
|
||||
await transport.CloseAsync();
|
||||
|
||||
Assert.False(transport.IsConnected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisposeAsync_Should_Dispose_Resources()
|
||||
{
|
||||
var transport = new SseClientTransport(_transportOptions, _serverConfig, NullLoggerFactory.Instance);
|
||||
|
||||
await transport.DisposeAsync();
|
||||
|
||||
Assert.False(transport.IsConnected);
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,6 @@ using Microsoft.Extensions.Logging;
|
|||
|
||||
namespace McpDotNet.Tests;
|
||||
|
||||
[Trait("Execution", "Manual")]
|
||||
public class SseIntegrationTests
|
||||
{
|
||||
[Fact]
|
||||
|
|
@ -37,7 +36,7 @@ public class SseIntegrationTests
|
|||
Location = "http://localhost:5000/sse"
|
||||
};
|
||||
|
||||
var factory = new McpClientFactory(
|
||||
using var factory = new McpClientFactory(
|
||||
[defaultConfig],
|
||||
defaultOptions,
|
||||
loggerFactory
|
||||
|
|
@ -80,7 +79,7 @@ public class SseIntegrationTests
|
|||
Location = "http://localhost:3001/sse"
|
||||
};
|
||||
|
||||
var factory = new McpClientFactory(
|
||||
using var factory = new McpClientFactory(
|
||||
[defaultConfig],
|
||||
defaultOptions,
|
||||
loggerFactory
|
||||
|
|
@ -128,7 +127,7 @@ public class SseIntegrationTests
|
|||
Location = "http://localhost:3001/sse"
|
||||
};
|
||||
|
||||
var factory = new McpClientFactory(
|
||||
using var factory = new McpClientFactory(
|
||||
[defaultConfig],
|
||||
defaultOptions,
|
||||
loggerFactory
|
||||
|
|
@ -137,11 +136,10 @@ public class SseIntegrationTests
|
|||
|
||||
// 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",
|
||||
|
|
@ -150,9 +148,8 @@ public class SseIntegrationTests
|
|||
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(
|
||||
|
|
@ -198,7 +195,7 @@ public class SseIntegrationTests
|
|||
Location = "http://localhost:5000/sse"
|
||||
};
|
||||
|
||||
var factory = new McpClientFactory(
|
||||
using var factory = new McpClientFactory(
|
||||
[defaultConfig],
|
||||
defaultOptions,
|
||||
loggerFactory
|
||||
|
|
@ -243,7 +240,7 @@ public class SseIntegrationTests
|
|||
Location = "http://localhost:5000/sse"
|
||||
};
|
||||
|
||||
var factory = new McpClientFactory(
|
||||
using var factory = new McpClientFactory(
|
||||
[defaultConfig],
|
||||
defaultOptions,
|
||||
loggerFactory
|
||||
|
|
@ -298,7 +295,7 @@ public class SseIntegrationTests
|
|||
Location = "http://localhost:5000/sse"
|
||||
};
|
||||
|
||||
var factory = new McpClientFactory(
|
||||
using var factory = new McpClientFactory(
|
||||
[defaultConfig],
|
||||
defaultOptions,
|
||||
loggerFactory
|
||||
|
|
|
|||
280
tests/mcpdotnet.Tests/Transport/SseClientTransportTests.cs
Normal file
280
tests/mcpdotnet.Tests/Transport/SseClientTransportTests.cs
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
using System.Net;
|
||||
using McpDotNet.Configuration;
|
||||
using McpDotNet.Protocol.Messages;
|
||||
using McpDotNet.Protocol.Transport;
|
||||
using McpDotNet.Tests.Utils;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace McpDotNet.Tests.Transport;
|
||||
|
||||
public class SseClientTransportTests
|
||||
{
|
||||
private readonly McpServerConfig _serverConfig;
|
||||
private readonly SseClientTransportOptions _transportOptions;
|
||||
|
||||
public SseClientTransportTests()
|
||||
{
|
||||
_serverConfig = new McpServerConfig
|
||||
{
|
||||
Id = "test-server",
|
||||
Name = "Test Server",
|
||||
TransportType = TransportTypes.Sse,
|
||||
Location = "http://localhost:8080"
|
||||
};
|
||||
|
||||
_transportOptions = new SseClientTransportOptions
|
||||
{
|
||||
ConnectionTimeout = TimeSpan.FromSeconds(2),
|
||||
MaxReconnectAttempts = 3,
|
||||
ReconnectDelay = TimeSpan.FromMilliseconds(50),
|
||||
AdditionalHeaders = new Dictionary<string, string>
|
||||
{
|
||||
["test"] = "header"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Constructor_Should_Initialize_With_Valid_Parameters()
|
||||
{
|
||||
// Act
|
||||
await using var transport = new SseClientTransport(_transportOptions, _serverConfig, NullLoggerFactory.Instance);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(transport);
|
||||
Assert.Equal(TimeSpan.FromSeconds(2), transport.Options.ConnectionTimeout);
|
||||
Assert.Equal(3, transport.Options.MaxReconnectAttempts);
|
||||
Assert.Equal(TimeSpan.FromMilliseconds(50), transport.Options.ReconnectDelay);
|
||||
Assert.NotNull(transport.Options.AdditionalHeaders);
|
||||
Assert.Equal("header", transport.Options.AdditionalHeaders["test"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Throws_For_Null_Options()
|
||||
{
|
||||
var exception = Assert.Throws<ArgumentNullException>(() => new SseClientTransport(null!, _serverConfig, NullLoggerFactory.Instance));
|
||||
Assert.Equal("transportOptions", exception.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Throws_For_Null_Config()
|
||||
{
|
||||
var exception = Assert.Throws<ArgumentNullException>(() => new SseClientTransport(_transportOptions, null!, NullLoggerFactory.Instance));
|
||||
Assert.Equal("serverConfig", exception.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Throws_For_Null_HttpClientg()
|
||||
{
|
||||
var exception = Assert.Throws<ArgumentNullException>(() => new SseClientTransport(_transportOptions, _serverConfig, null!, NullLoggerFactory.Instance));
|
||||
Assert.Equal("httpClient", exception.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConnectAsync_Should_Connect_Successfully()
|
||||
{
|
||||
using var mockHttpHandler = new MockHttpHandler();
|
||||
using var httpClient = new HttpClient(mockHttpHandler);
|
||||
await using var transport = new SseClientTransport(_transportOptions, _serverConfig, httpClient, NullLoggerFactory.Instance);
|
||||
|
||||
bool firstCall = true;
|
||||
|
||||
mockHttpHandler.RequestHandler = async (request) =>
|
||||
{
|
||||
if (!firstCall)
|
||||
{
|
||||
Assert.True(transport.IsConnected);
|
||||
await transport.CloseAsync();
|
||||
}
|
||||
|
||||
firstCall = false;
|
||||
return new HttpResponseMessage
|
||||
{
|
||||
StatusCode = HttpStatusCode.OK,
|
||||
Content = new StringContent("event: endpoint\r\ndata: http://localhost\r\n\r\n")
|
||||
};
|
||||
};
|
||||
|
||||
await transport.ConnectAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConnectAsync_Throws_If_Already_Connected()
|
||||
{
|
||||
await using var transport = new SseClientTransport(_transportOptions, _serverConfig, NullLoggerFactory.Instance);
|
||||
transport.GetType().BaseType!.GetField("_isConnected", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.SetValue(transport, true);
|
||||
|
||||
var action = async () => await transport.ConnectAsync();
|
||||
var exception = await Assert.ThrowsAsync<McpTransportException>(action);
|
||||
Assert.Equal("Transport is already connected", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConnectAsync_Throws_Exception_On_Failure()
|
||||
{
|
||||
using var mockHttpHandler = new MockHttpHandler();
|
||||
using var httpClient = new HttpClient(mockHttpHandler);
|
||||
await using var transport = new SseClientTransport(_transportOptions, _serverConfig, httpClient, NullLoggerFactory.Instance);
|
||||
|
||||
var retries = 0;
|
||||
mockHttpHandler.RequestHandler = (request) =>
|
||||
{
|
||||
retries++;
|
||||
throw new InvalidOperationException("Test exception");
|
||||
};
|
||||
|
||||
var action = async () => await transport.ConnectAsync();
|
||||
|
||||
var exception = await Assert.ThrowsAsync<McpTransportException>(action);
|
||||
Assert.Equal("Exceeded reconnect limit", exception.Message);
|
||||
|
||||
Assert.Equal(_transportOptions.MaxReconnectAttempts, retries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendMessageAsync_Throws_Exception_If_MessageEndpoint_Not_Set()
|
||||
{
|
||||
await using var transport = new SseClientTransport(_transportOptions, _serverConfig, NullLoggerFactory.Instance);
|
||||
|
||||
// Assert
|
||||
Assert.True(string.IsNullOrEmpty(transport.MessageEndpoint?.ToString()));
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => transport.SendMessageAsync(new JsonRpcRequest() { Method = "test" }, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendMessageAsync_Handles_Accepted_Response()
|
||||
{
|
||||
using var mockHttpHandler = new MockHttpHandler();
|
||||
using var httpClient = new HttpClient(mockHttpHandler);
|
||||
await using var transport = new SseClientTransport(_transportOptions, _serverConfig, httpClient, NullLoggerFactory.Instance);
|
||||
|
||||
var firstCall = true;
|
||||
mockHttpHandler.RequestHandler = (request) =>
|
||||
{
|
||||
if (request.Method == HttpMethod.Post && request.RequestUri?.AbsoluteUri == "http://localhost:8080/sseendpoint")
|
||||
{
|
||||
return Task.FromResult(new HttpResponseMessage
|
||||
{
|
||||
StatusCode = HttpStatusCode.OK,
|
||||
Content = new StringContent("accepted")
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!firstCall)
|
||||
throw new IOException("Abort");
|
||||
else
|
||||
firstCall = false;
|
||||
|
||||
return Task.FromResult(new HttpResponseMessage
|
||||
{
|
||||
StatusCode = HttpStatusCode.OK,
|
||||
Content = new StringContent("event: endpoint\r\ndata: /sseendpoint\r\n\r\n")
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
await transport.ConnectAsync();
|
||||
|
||||
await transport.SendMessageAsync(new JsonRpcRequest() { Method = "initialize", Id = RequestId.FromNumber(44) }, CancellationToken.None);
|
||||
Assert.True(true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendMessageAsync_Handles_Accepted_Json_RPC_Response()
|
||||
{
|
||||
using var mockHttpHandler = new MockHttpHandler();
|
||||
using var httpClient = new HttpClient(mockHttpHandler);
|
||||
await using var transport = new SseClientTransport(_transportOptions, _serverConfig, httpClient, NullLoggerFactory.Instance);
|
||||
|
||||
var firstCall = true;
|
||||
mockHttpHandler.RequestHandler = (request) =>
|
||||
{
|
||||
if (request.Method == HttpMethod.Post && request.RequestUri?.AbsoluteUri == "http://localhost:8080/sseendpoint")
|
||||
{
|
||||
return Task.FromResult(new HttpResponseMessage
|
||||
{
|
||||
StatusCode = HttpStatusCode.OK,
|
||||
Content = new StringContent("{\"jsonrpc\":\"2.0\", \"id\": \"44\", \"result\": null}")
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!firstCall)
|
||||
throw new IOException("Abort");
|
||||
else
|
||||
firstCall = false;
|
||||
|
||||
return Task.FromResult(new HttpResponseMessage
|
||||
{
|
||||
StatusCode = HttpStatusCode.OK,
|
||||
Content = new StringContent("event: endpoint\r\ndata: /sseendpoint\r\n\r\n")
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
await transport.ConnectAsync();
|
||||
|
||||
await transport.SendMessageAsync(new JsonRpcRequest() { Method = "initialize", Id = RequestId.FromNumber(44) }, CancellationToken.None);
|
||||
Assert.True(true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReceiveMessagesAsync_Handles_Messages()
|
||||
{
|
||||
using var mockHttpHandler = new MockHttpHandler();
|
||||
using var httpClient = new HttpClient(mockHttpHandler);
|
||||
await using var transport = new SseClientTransport(_transportOptions, _serverConfig, httpClient, NullLoggerFactory.Instance);
|
||||
|
||||
var callIndex = 0;
|
||||
mockHttpHandler.RequestHandler = (request) =>
|
||||
{
|
||||
callIndex++;
|
||||
|
||||
if (callIndex == 1)
|
||||
{
|
||||
return Task.FromResult(new HttpResponseMessage
|
||||
{
|
||||
StatusCode = HttpStatusCode.OK,
|
||||
Content = new StringContent("event: endpoint\r\ndata: /sseendpoint\r\n\r\n")
|
||||
});
|
||||
}
|
||||
else if (callIndex == 2)
|
||||
{
|
||||
return Task.FromResult(new HttpResponseMessage
|
||||
{
|
||||
StatusCode = HttpStatusCode.OK,
|
||||
Content = new StringContent("event: message\r\ndata: {\"jsonrpc\":\"2.0\", \"id\": \"44\", \"method\": \"test\", \"params\": null}\r\n\r\n")
|
||||
});
|
||||
}
|
||||
|
||||
throw new IOException("Abort");
|
||||
};
|
||||
|
||||
await transport.ConnectAsync();
|
||||
Assert.True(transport.MessageReader.TryRead(out var message));
|
||||
Assert.NotNull(message);
|
||||
Assert.IsType<JsonRpcRequest>(message);
|
||||
Assert.Equal("44", ((JsonRpcRequest)message).Id.AsString);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CloseAsync_Should_Dispose_Resources()
|
||||
{
|
||||
await using var transport = new SseClientTransport(_transportOptions, _serverConfig, NullLoggerFactory.Instance);
|
||||
|
||||
await transport.CloseAsync();
|
||||
|
||||
Assert.False(transport.IsConnected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisposeAsync_Should_Dispose_Resources()
|
||||
{
|
||||
await using var transport = new SseClientTransport(_transportOptions, _serverConfig, NullLoggerFactory.Instance);
|
||||
|
||||
await transport.DisposeAsync();
|
||||
|
||||
Assert.False(transport.IsConnected);
|
||||
}
|
||||
}
|
||||
133
tests/mcpdotnet.Tests/Transport/StdioServerTransportTests.cs
Normal file
133
tests/mcpdotnet.Tests/Transport/StdioServerTransportTests.cs
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
using System.Text.Json;
|
||||
using McpDotNet.Protocol.Messages;
|
||||
using McpDotNet.Protocol.Transport;
|
||||
using McpDotNet.Protocol.Types;
|
||||
using McpDotNet.Server;
|
||||
using McpDotNet.Utils.Json;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace McpDotNet.Tests.Transport;
|
||||
|
||||
public class StdioServerTransportTests
|
||||
{
|
||||
private readonly McpServerOptions _serverOptions;
|
||||
|
||||
public StdioServerTransportTests()
|
||||
{
|
||||
_serverOptions = new McpServerOptions
|
||||
{
|
||||
ServerInfo = new Implementation
|
||||
{
|
||||
Name = "Test Server",
|
||||
Version = "1.0"
|
||||
},
|
||||
ProtocolVersion = "2.0",
|
||||
InitializationTimeout = TimeSpan.FromSeconds(10),
|
||||
ServerInstructions = "Test Instructions"
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Constructor_Should_Initialize_With_Valid_Parameters()
|
||||
{
|
||||
// Act
|
||||
await using var transport = new StdioServerTransport(_serverOptions, NullLoggerFactory.Instance, InputOutputStreams.Default);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(transport);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Throws_For_Null_Options()
|
||||
{
|
||||
var exception = Assert.Throws<ArgumentNullException>(() => new StdioServerTransport(serverOptions: null!, NullLoggerFactory.Instance, InputOutputStreams.Default));
|
||||
Assert.Equal("serverOptions", exception.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Throws_For_Null_Streams()
|
||||
{
|
||||
var exception = Assert.Throws<ArgumentNullException>(() => new StdioServerTransport(_serverOptions, NullLoggerFactory.Instance, null!));
|
||||
Assert.Equal("inputOutputStreams", exception.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartListeningAsync_Should_Set_Connected_State()
|
||||
{
|
||||
await using var transport = new StdioServerTransport(_serverOptions, NullLoggerFactory.Instance, InputOutputStreams.Default);
|
||||
|
||||
await transport.StartListeningAsync();
|
||||
|
||||
Assert.True(transport.IsConnected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendMessageAsync_Should_Send_Message()
|
||||
{
|
||||
var streams = new InputOutputStreams { Output = new StringWriter(), Input = new StringReader("") };
|
||||
await using var transport = new StdioServerTransport(_serverOptions, NullLoggerFactory.Instance, streams);
|
||||
await transport.StartListeningAsync();
|
||||
|
||||
var message = new JsonRpcRequest { Method = "test", Id = RequestId.FromNumber(44) };
|
||||
|
||||
|
||||
await transport.SendMessageAsync(message);
|
||||
|
||||
var result = streams.Output.ToString()?.Trim();
|
||||
var expected = JsonSerializer.Serialize(message, JsonSerializerOptionsExtensions.DefaultOptions);
|
||||
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendMessageAsync_Throws_Exception_If_Not_Connected()
|
||||
{
|
||||
await using var transport = new StdioServerTransport(_serverOptions, NullLoggerFactory.Instance, InputOutputStreams.Default);
|
||||
|
||||
var message = new JsonRpcRequest { Method = "test" };
|
||||
|
||||
await Assert.ThrowsAsync<McpTransportException>(() => transport.SendMessageAsync(message));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisposeAsync_Should_Dispose_Resources()
|
||||
{
|
||||
await using var transport = new StdioServerTransport(_serverOptions, NullLoggerFactory.Instance, InputOutputStreams.Default);
|
||||
|
||||
await transport.DisposeAsync();
|
||||
|
||||
Assert.False(transport.IsConnected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadMessagesAsync_Should_Read_Messages()
|
||||
{
|
||||
var message = new JsonRpcRequest { Method = "test", Id = RequestId.FromNumber(44) };
|
||||
var json = JsonSerializer.Serialize(message, JsonSerializerOptionsExtensions.DefaultOptions);
|
||||
|
||||
using var sr = new StringReader(json);
|
||||
|
||||
var streams = new InputOutputStreams { Output = new StringWriter(), Input = new StringReader(json) };
|
||||
await using var transport = new StdioServerTransport(_serverOptions, NullLoggerFactory.Instance, streams);
|
||||
await transport.StartListeningAsync();
|
||||
|
||||
var canRead = await transport.MessageReader.WaitToReadAsync();
|
||||
|
||||
Assert.True(canRead, "Nothing to read here from transport message reader");
|
||||
Assert.True(transport.MessageReader.TryPeek(out var readMessage));
|
||||
Assert.NotNull(readMessage);
|
||||
Assert.IsType<JsonRpcRequest>(readMessage);
|
||||
Assert.Equal(44, ((JsonRpcRequest)readMessage).Id.AsNumber);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CleanupAsync_Should_Cleanup_Resources()
|
||||
{
|
||||
var transport = new StdioServerTransport(_serverOptions, NullLoggerFactory.Instance, InputOutputStreams.Default);
|
||||
await transport.StartListeningAsync();
|
||||
|
||||
await transport.DisposeAsync();
|
||||
|
||||
Assert.False(transport.IsConnected);
|
||||
}
|
||||
}
|
||||
|
|
@ -348,7 +348,7 @@ public sealed class TestSseServer : IAsyncDisposable
|
|||
{
|
||||
try
|
||||
{
|
||||
await _serverTask;
|
||||
await Task.WhenAny(_serverTask, Task.Delay(2000));
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue