csharp-sdk/tests/mcpdotnet.Tests/Server/McpServerTests.cs
Stephen Toub 08b0a04dab
Overhaul McpClientFactory/McpServerFactory (#87)
Handlers are now specified before connecting the client/server. Otherwise, race conditions exist. Handlers move into being a part of the McpClientOptions/McpServerOptions, so that they're provided to the factories. Where relevant, the handlers are specified as part of the capability descriptors, so that they go hand in hand.

The factories are no longer stateful. Instead of allocating a factory and then calling a create method on it, you just call a static factory method.
2025-03-16 12:08:52 -04:00

566 lines
20 KiB
C#

using McpDotNet.Client;
using McpDotNet.Protocol.Messages;
using McpDotNet.Protocol.Transport;
using McpDotNet.Protocol.Types;
using McpDotNet.Server;
using McpDotNet.Tests.Utils;
using Microsoft.Extensions.Logging;
using Moq;
namespace McpDotNet.Tests.Server;
public class McpServerTests
{
private readonly Mock<IServerTransport> _serverTransport;
private readonly Mock<ILoggerFactory> _loggerFactory;
private readonly Mock<ILogger> _logger;
private readonly McpServerOptions _options;
private readonly IServiceProvider _serviceProvider;
public McpServerTests()
{
_serverTransport = new Mock<IServerTransport>();
_loggerFactory = new Mock<ILoggerFactory>();
_logger = new Mock<ILogger>();
_loggerFactory.Setup(f => f.CreateLogger(It.IsAny<string>())).Returns(_logger.Object);
_options = CreateOptions();
_serviceProvider = new Mock<IServiceProvider>().Object;
}
private static McpServerOptions CreateOptions(ServerCapabilities? capabilities = null)
{
return new McpServerOptions
{
ServerInfo = new Implementation { Name = "TestServer", Version = "1.0" },
ProtocolVersion = "2024",
InitializationTimeout = TimeSpan.FromSeconds(30),
Capabilities = capabilities,
};
}
[Fact]
public async Task Constructor_Should_Initialize_With_Valid_Parameters()
{
// Arrange & Act
await using var server = new McpServer(_serverTransport.Object, _options, _loggerFactory.Object, _serviceProvider);
// Assert
Assert.NotNull(server);
}
[Fact]
public void Constructor_Throws_For_Null_Transport()
{
// Arrange, Act & Assert
Assert.Throws<ArgumentNullException>(() => new McpServer(null!, _options, _loggerFactory.Object, _serviceProvider));
}
[Fact]
public void Constructor_Throws_For_Null_Options()
{
// Arrange, Act & Assert
Assert.Throws<ArgumentNullException>(() => new McpServer(_serverTransport.Object, null!, _loggerFactory.Object, _serviceProvider));
}
[Fact]
public async Task Constructor_Does_Not_Throw_For_Null_Logger()
{
// Arrange & Act
await using var server = new McpServer(_serverTransport.Object, _options, null, _serviceProvider);
// Assert
Assert.NotNull(server);
}
[Fact]
public async Task Constructor_Does_Not_Throw_For_Null_ServiceProvider()
{
// Arrange & Act
await using var server = new McpServer(_serverTransport.Object, _options, _loggerFactory.Object, null);
// Assert
Assert.NotNull(server);
}
[Fact]
public async Task Property_EndpointName_Return_Infos()
{
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);
}
[Fact]
public async Task StartAsync_Should_Throw_InvalidOperationException_If_Already_Initializing()
{
// Arrange
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
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => server.StartAsync());
Assert.Equal("Server is already initializing", exception.Message);
}
[Fact]
public async Task StartAsync_Should_Do_Nothing_If_Already_Initialized()
{
// Arrange
await using var server = new McpServer(_serverTransport.Object, _options, _loggerFactory.Object, _serviceProvider);
server.IsInitialized = true;
await server.StartAsync();
// Assert
_serverTransport.Verify(t => t.StartListeningAsync(It.IsAny<CancellationToken>()), Times.Never);
}
[Fact]
public async Task StartAsync_ShouldStartListening()
{
// Arrange
await using var server = new McpServer(_serverTransport.Object, _options, _loggerFactory.Object, _serviceProvider);
// Act
await server.StartAsync();
// Assert
_serverTransport.Verify(t => t.StartListeningAsync(It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task StartAsync_Sets_Initialized_After_Transport_Responses_Initialized_Notification()
{
await using var transport = new TestServerTransport();
await using var server = new McpServer(transport, _options, _loggerFactory.Object, _serviceProvider);
await server.StartAsync();
// Send initialized notification
await transport.SendMessageAsync(
new JsonRpcNotification
{
Method = "notifications/initialized"
}
);
await Task.Delay(50);
Assert.True(server.IsInitialized);
}
[Fact]
public async Task RequestSamplingAsync_Should_Throw_McpServerException_If_Client_Does_Not_Support_Sampling()
{
// Arrange
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);
// Act & Assert
var exception = await Assert.ThrowsAsync<McpServerException>(action);
Assert.Equal("Client does not support sampling", exception.Message);
}
[Fact]
public async Task RequestSamplingAsync_Should_SendRequest()
{
// Arrange
await using var transport = new TestServerTransport();
await using var server = new McpServer(transport, _options, _loggerFactory.Object, _serviceProvider);
server.ClientCapabilities = new ClientCapabilities { Sampling = new SamplingCapability() };
await server.StartAsync();
// Act
var result = await server.RequestSamplingAsync(new CreateMessageRequestParams { Messages = [] }, CancellationToken.None);
Assert.NotNull(result);
Assert.NotEmpty(transport.SentMessages);
Assert.IsType<JsonRpcRequest>(transport.SentMessages[0]);
Assert.Equal("sampling/createMessage", ((JsonRpcRequest)transport.SentMessages[0]).Method);
}
[Fact]
public async Task RequestRootsAsync_Should_Throw_McpServerException_If_Client_Does_Not_Support_Roots()
{
// Arrange
await using var server = new McpServer(_serverTransport.Object, _options, _loggerFactory.Object, _serviceProvider);
server.ClientCapabilities = new ClientCapabilities();
// Act & Assert
var exception = await Assert.ThrowsAsync<McpServerException>(() => server.RequestRootsAsync(new ListRootsRequestParams(), CancellationToken.None));
Assert.Equal("Client does not support roots", exception.Message);
}
[Fact]
public async Task RequestRootsAsync_Should_SendRequest()
{
// Arrange
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();
// Act
var result = await server.RequestRootsAsync(new ListRootsRequestParams(), CancellationToken.None);
// Assert
Assert.NotNull(result);
Assert.NotEmpty(transport.SentMessages);
Assert.IsType<JsonRpcRequest>(transport.SentMessages[0]);
Assert.Equal("roots/list", ((JsonRpcRequest)transport.SentMessages[0]).Method);
}
[Fact]
public async Task Throws_Exception_If_Not_Connected()
{
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);
var action = async () => await server.RequestRootsAsync(new ListRootsRequestParams(), CancellationToken.None);
var exception = await Assert.ThrowsAsync<McpClientException>(action);
Assert.Equivalent("Transport is not connected", exception.Message);
}
[Fact]
public async Task Can_Handle_Ping_Requests()
{
await Can_Handle_Requests(
serverCapabilities: null,
method: "ping",
configureOptions: null,
assertResult: response =>
{
Assert.IsType<PingResult>(response);
});
}
[Fact]
public async Task Can_Handle_Initialize_Requests()
{
await Can_Handle_Requests(
serverCapabilities: null,
method: "initialize",
configureOptions: null,
assertResult: response =>
{
Assert.IsType<InitializeResult>(response);
var result = (InitializeResult)response;
Assert.Equal("TestServer", result.ServerInfo.Name);
Assert.Equal("1.0", result.ServerInfo.Version);
Assert.Equal("2024", result.ProtocolVersion);
});
}
[Fact]
public async Task Can_Handle_Completion_Requests()
{
await Can_Handle_Requests(
serverCapabilities: null,
method: "completion/complete",
configureOptions: null,
assertResult: response =>
{
Assert.IsType<CompleteResult>(response);
var result = (CompleteResult)response;
Assert.NotNull(result.Completion);
Assert.Empty(result.Completion.Values);
Assert.Equal(0, result.Completion.Total);
Assert.False(result.Completion.HasMore);
});
}
[Fact]
public async Task Can_Handle_Completion_Requests_With_Handler()
{
await Can_Handle_Requests(
serverCapabilities: null,
method: "completion/complete",
configureOptions: options => options with
{
GetCompletionHandler = (request, ct) =>
Task.FromResult(new CompleteResult
{
Completion = new()
{
Values = ["test"],
Total = 2,
HasMore = true
}
})
},
assertResult: response =>
{
Assert.IsType<CompleteResult>(response);
var result = (CompleteResult)response;
Assert.NotNull(result.Completion);
Assert.NotEmpty(result.Completion.Values);
Assert.Equal("test", result.Completion.Values[0]);
Assert.Equal(2, result.Completion.Total);
Assert.True(result.Completion.HasMore);
});
}
[Fact]
public async Task Can_Handle_Resources_List_Requests()
{
await Can_Handle_Requests(
new ServerCapabilities
{
Resources = new()
{
ListResourcesHandler = (request, ct) =>
{
return Task.FromResult(new ListResourcesResult
{
Resources = [new() { Uri = "test", Name = "Test Resource" }]
});
},
ReadResourceHandler = (request, ct) => throw new NotImplementedException(),
}
},
"resources/list",
configureOptions: null,
assertResult: response =>
{
Assert.IsType<ListResourcesResult>(response);
var result = (ListResourcesResult)response;
Assert.NotNull(result.Resources);
Assert.NotEmpty(result.Resources);
Assert.Equal("test", result.Resources[0].Uri);
});
}
[Fact]
public async Task Can_Handle_Resources_List_Requests_Throws_Exception_If_No_Handler_Assigned()
{
await Throws_Exception_If_No_Handler_Assigned(new ServerCapabilities { Resources = new() }, "resources/list", "ListResources handler not configured");
}
[Fact]
public async Task Can_Handle_ResourcesRead_Requests()
{
await Can_Handle_Requests(
new ServerCapabilities
{
Resources = new()
{
ReadResourceHandler = (request, ct) =>
{
return Task.FromResult(new ReadResourceResult
{
Contents = [new() { Text = "test" }]
});
},
ListResourcesHandler = (request, ct) => throw new NotImplementedException(),
}
},
method: "resources/read",
configureOptions: null,
assertResult: response =>
{
Assert.IsType<ReadResourceResult>(response);
var result = (ReadResourceResult)response;
Assert.NotNull(result.Contents);
Assert.NotEmpty(result.Contents);
Assert.Equal("test", result.Contents[0].Text);
});
}
[Fact]
public async Task Can_Handle_Resources_Read_Requests_Throws_Exception_If_No_Handler_Assigned()
{
await Throws_Exception_If_No_Handler_Assigned(new ServerCapabilities { Resources = new() }, "resources/read", "ReadResource handler not configured");
}
[Fact]
public async Task Can_Handle_List_Prompts_Requests()
{
await Can_Handle_Requests(
new ServerCapabilities
{
Prompts = new()
{
ListPromptsHandler = (request, ct) =>
{
return Task.FromResult(new ListPromptsResult
{
Prompts = [new() { Name = "test" }]
});
},
GetPromptHandler = (request, ct) => throw new NotImplementedException(),
},
},
method: "prompts/list",
configureOptions: null,
assertResult: response =>
{
Assert.IsType<ListPromptsResult>(response);
var result = (ListPromptsResult)response;
Assert.NotNull(result.Prompts);
Assert.NotEmpty(result.Prompts);
Assert.Equal("test", result.Prompts[0].Name);
});
}
[Fact]
public async Task Can_Handle_List_Prompts_Requests_Throws_Exception_If_No_Handler_Assigned()
{
await Throws_Exception_If_No_Handler_Assigned(new ServerCapabilities { Prompts = new() }, "prompts/list", "ListPrompts handler not configured");
}
[Fact]
public async Task Can_Handle_Get_Prompts_Requests()
{
await Can_Handle_Requests(
new ServerCapabilities
{
Prompts = new()
{
GetPromptHandler = (request, ct) => Task.FromResult(new GetPromptResult { Description = "test" }),
ListPromptsHandler = (request, ct) => throw new NotImplementedException(),
}
},
method: "prompts/get",
configureOptions: null,
assertResult: response =>
{
Assert.IsType<GetPromptResult>(response);
var result = (GetPromptResult)response;
Assert.Equal("test", result.Description);
});
}
[Fact]
public async Task Can_Handle_Get_Prompts_Requests_Throws_Exception_If_No_Handler_Assigned()
{
await Throws_Exception_If_No_Handler_Assigned(new ServerCapabilities { Prompts = new() }, "prompts/get", "GetPrompt handler not configured");
}
[Fact]
public async Task Can_Handle_List_Tools_Requests()
{
await Can_Handle_Requests(
new ServerCapabilities
{
Tools = new()
{
ListToolsHandler = (request, ct) =>
{
return Task.FromResult(new ListToolsResult
{
Tools = [new() { Name = "test" }]
});
},
CallToolHandler = (request, ct) => throw new NotImplementedException(),
}
},
method: "tools/list",
configureOptions: null,
assertResult: response =>
{
Assert.IsType<ListToolsResult>(response);
var result = (ListToolsResult)response;
Assert.NotEmpty(result.Tools);
Assert.Equal("test", result.Tools[0].Name);
});
}
[Fact]
public async Task Can_Handle_List_Tools_Requests_Throws_Exception_If_No_Handler_Assigned()
{
await Throws_Exception_If_No_Handler_Assigned(new ServerCapabilities { Tools = new() }, "tools/list", "ListTools handler not configured");
}
[Fact]
public async Task Can_Handle_Call_Tool_Requests()
{
await Can_Handle_Requests(
new ServerCapabilities
{
Tools = new()
{
CallToolHandler = (request, ct) =>
{
return Task.FromResult(new CallToolResponse
{
Content = [new Content { Text = "test" }]
});
},
ListToolsHandler = (request, ct) => throw new NotImplementedException(),
}
},
method: "tools/call",
configureOptions: null,
assertResult: response =>
{
Assert.IsType<CallToolResponse>(response);
var result = (CallToolResponse)response;
Assert.NotEmpty(result.Content);
Assert.Equal("test", result.Content[0].Text);
});
}
[Fact]
public async Task Can_Handle_Call_Tool_Requests_Throws_Exception_If_No_Handler_Assigned()
{
await Throws_Exception_If_No_Handler_Assigned(new ServerCapabilities { Tools = new() }, "tools/call", "CallTool handler not configured");
}
private async Task Can_Handle_Requests(ServerCapabilities? serverCapabilities, string method, Func<McpServerOptions, McpServerOptions>? configureOptions, Action<object> assertResult)
{
await using var transport = new TestServerTransport();
var options = CreateOptions(serverCapabilities);
if (configureOptions is not null)
{
options = configureOptions(options);
}
await using var server = new McpServer(transport, options, _loggerFactory.Object, _serviceProvider);
await server.StartAsync();
var receivedMessage = new TaskCompletionSource<JsonRpcResponse>();
transport.OnMessageSent = (message) =>
{
if (message is JsonRpcResponse response && response.Id.AsNumber == 55)
receivedMessage.SetResult(response);
};
await transport.SendMessageAsync(
new JsonRpcRequest
{
Method = method,
Id = RequestId.FromNumber(55)
}
);
var response = await receivedMessage.Task.WaitAsync(TimeSpan.FromSeconds(1));
Assert.NotNull(response);
Assert.NotNull(response.Result);
assertResult(response.Result);
}
private async Task Throws_Exception_If_No_Handler_Assigned(ServerCapabilities serverCapabilities, string method, string expectedError)
{
await using var transport = new TestServerTransport();
var options = CreateOptions(serverCapabilities);
Assert.Throws<McpServerException>(() => McpServerFactory.Create(transport, options, _loggerFactory.Object, _serviceProvider));
}
}