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.
This commit is contained in:
Stephen Toub 2025-03-16 12:08:52 -04:00 • committed by GitHub
commit 08b0a04dab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 1306 additions and 2243 deletions

View file

@ -1,7 +1,6 @@
using McpDotNet.Client;
using McpDotNet.Configuration;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging.Abstractions;
using OpenAI;
namespace McpDotNet.Extensions.AI.Tests;
@ -35,19 +34,13 @@ public class IntegrationTests
ClientInfo = new() { Name = "McpDotNet.Extensions.AI.Tests", Version = "1.0.0" }
};
var factory = new McpClientFactory(
[GetEverythingServerConfig()],
options,
NullLoggerFactory.Instance
);
return await factory.GetClientAsync("everything");
return await McpClientFactory.CreateAsync(GetEverythingServerConfig(), options);
}
[Fact]
public async Task IntegrateWithMeai_UsingEverythingServer_ToolsAreProperlyCalled()
{
var client = await GetMcpClientAsync();
await using var client = await GetMcpClientAsync();
var mappedTools = await client.ListToolsAsync().Select(t => t.ToAITool(client)).ToListAsync();
IChatClient openaiClient = new OpenAIClient(_openAIKey)

View file

@ -34,24 +34,17 @@ internal static class Program
ServerInfo = new Implementation() { Name = "TestServer", Version = "1.0.0" },
Capabilities = new ServerCapabilities()
{
Tools = new(),
Resources = new(),
Prompts = new(),
Tools = ConfigureTools(),
Resources = ConfigureResources(),
Prompts = ConfigurePrompts(),
},
ProtocolVersion = "2024-11-05",
ServerInstructions = "This is a test server with only stub functionality",
GetCompletionHandler = ConfigureCompletion(),
};
var loggerFactory = CreateLoggerFactory();
McpServerFactory factory = new(new StdioServerTransport("TestServer", loggerFactory), options, loggerFactory);
IMcpServer server = factory.CreateServer();
Log.Logger.Information("Server object created, registering handlers.");
ConfigureTools(server);
ConfigureResources(server);
ConfigurePrompts(server);
ConfigureCompletion(server);
await using IMcpServer server = McpServerFactory.Create(new StdioServerTransport("TestServer", loggerFactory), options, loggerFactory);
Log.Logger.Information("Server initialized.");
@ -60,184 +53,187 @@ internal static class Program
Log.Logger.Information("Server started.");
// Run until process is stopped by the client (parent process)
while (true) // NOSONAR
{
await Task.Delay(1000);
}
await Task.Delay(Timeout.Infinite);
}
private static void ConfigureTools(IMcpServer server)
private static ToolsCapability ConfigureTools()
{
server.SetListToolsHandler((request, cancellationToken) =>
return new()
{
return Task.FromResult(new ListToolsResult()
ListToolsHandler = (request, cancellationToken) =>
{
Tools =
[
new Tool()
{
Name = "echo",
Description = "Echoes the input back to the client.",
InputSchema = new JsonSchema()
return Task.FromResult(new ListToolsResult()
{
Tools =
[
new Tool()
{
Type = "object",
Properties = new Dictionary<string, JsonSchemaProperty>()
Name = "echo",
Description = "Echoes the input back to the client.",
InputSchema = new JsonSchema()
{
["message"] = new JsonSchemaProperty() { Type = "string", Description = "The input to echo back." }
}
},
},
new Tool()
{
Name = "sampleLLM",
Description = "Samples from an LLM using MCP's sampling feature.",
InputSchema = new JsonSchema()
{
Type = "object",
Properties = new Dictionary<string, JsonSchemaProperty>()
{
["prompt"] = new JsonSchemaProperty() { Type = "string", Description = "The prompt to send to the LLM" },
["maxTokens"] = new JsonSchemaProperty() { Type = "number", Description = "Maximum number of tokens to generate" }
}
},
}
]
});
});
server.SetCallToolHandler(async (request, cancellationToken) =>
{
if (request.Params?.Name == "echo")
{
if (request.Params?.Arguments is null || !request.Params.Arguments.TryGetValue("message", out var message))
{
throw new McpServerException("Missing required argument 'message'");
}
return new CallToolResponse()
{
Content = [new Content() { Text = "Echo: " + message?.ToString(), Type = "text" }]
};
}
else if (request.Params?.Name == "sampleLLM")
{
if (request.Params?.Arguments is null ||
!request.Params.Arguments.TryGetValue("prompt", out var prompt) ||
!request.Params.Arguments.TryGetValue("maxTokens", out var maxTokens))
{
throw new McpServerException("Missing required arguments 'prompt' and 'maxTokens'");
}
var sampleResult = await server.RequestSamplingAsync(CreateRequestSamplingParams(prompt?.ToString() ?? "", "sampleLLM", Convert.ToInt32(maxTokens?.ToString())),
cancellationToken);
return new CallToolResponse()
{
Content = [new Content() { Text = $"LLM sampling result: {sampleResult.Content.Text}", Type = "text" }]
};
}
else
{
throw new McpServerException($"Unknown tool: {request.Params?.Name}");
}
});
}
private static void ConfigurePrompts(IMcpServer server)
{
server.SetListPromptsHandler((request, cancellationToken) =>
{
return Task.FromResult(new ListPromptsResult()
{
Prompts = [
new Prompt()
{
Name = "simple_prompt",
Description = "A prompt without arguments"
},
new Prompt()
{
Name = "complex_prompt",
Description = "A prompt with arguments",
Arguments =
[
new PromptArgument()
{
Name = "temperature",
Description = "Temperature setting",
Required = true
Type = "object",
Properties = new Dictionary<string, JsonSchemaProperty>()
{
["message"] = new JsonSchemaProperty() { Type = "string", Description = "The input to echo back." }
}
},
new PromptArgument()
},
new Tool()
{
Name = "sampleLLM",
Description = "Samples from an LLM using MCP's sampling feature.",
InputSchema = new JsonSchema()
{
Name = "style",
Description = "Output style",
Required = false
}
]
}
]
});
});
Type = "object",
Properties = new Dictionary<string, JsonSchemaProperty>()
{
["prompt"] = new JsonSchemaProperty() { Type = "string", Description = "The prompt to send to the LLM" },
["maxTokens"] = new JsonSchemaProperty() { Type = "number", Description = "Maximum number of tokens to generate" }
}
},
}
]
});
},
server.SetGetPromptHandler((request, cancellationToken) =>
{
List<PromptMessage> messages = new();
if (request.Params?.Name == "simple_prompt")
CallToolHandler = async (request, cancellationToken) =>
{
messages.Add(new PromptMessage()
if (request.Params?.Name == "echo")
{
Role = Role.User,
Content = new Content()
if (request.Params?.Arguments is null || !request.Params.Arguments.TryGetValue("message", out var message))
{
Type = "text",
Text = "This is a simple prompt without arguments."
throw new McpServerException("Missing required argument 'message'");
}
});
}
else if (request.Params?.Name == "complex_prompt")
{
string temperature = request.Params.Arguments?["temperature"]?.ToString() ?? "unknown";
string style = request.Params.Arguments?["style"]?.ToString() ?? "unknown";
messages.Add(new PromptMessage()
return new CallToolResponse()
{
Content = [new Content() { Text = "Echo: " + message?.ToString(), Type = "text" }]
};
}
else if (request.Params?.Name == "sampleLLM")
{
Role = Role.User,
Content = new Content()
if (request.Params?.Arguments is null ||
!request.Params.Arguments.TryGetValue("prompt", out var prompt) ||
!request.Params.Arguments.TryGetValue("maxTokens", out var maxTokens))
{
Type = "text",
Text = $"This is a complex prompt with arguments: temperature={temperature}, style={style}"
throw new McpServerException("Missing required arguments 'prompt' and 'maxTokens'");
}
});
messages.Add(new PromptMessage()
{
Role = Role.Assistant,
Content = new Content()
{
Type = "text",
Text = "I understand. You've provided a complex prompt with temperature and style arguments. How would you like me to proceed?"
}
});
messages.Add(new PromptMessage()
{
Role = Role.User,
Content = new Content()
{
Type = "image",
Data = MCP_TINY_IMAGE,
MimeType = "image/png"
}
});
}
else
{
throw new McpServerException($"Unknown prompt: {request.Params?.Name}");
}
var sampleResult = await request.Server.RequestSamplingAsync(CreateRequestSamplingParams(prompt?.ToString() ?? "", "sampleLLM", Convert.ToInt32(maxTokens?.ToString())),
cancellationToken);
return Task.FromResult(new GetPromptResult()
{
Messages = messages
});
});
return new CallToolResponse()
{
Content = [new Content() { Text = $"LLM sampling result: {sampleResult.Content.Text}", Type = "text" }]
};
}
else
{
throw new McpServerException($"Unknown tool: {request.Params?.Name}");
}
}
};
}
private static void ConfigureResources(IMcpServer server)
private static PromptsCapability ConfigurePrompts()
{
return new()
{
ListPromptsHandler = (request, cancellationToken) =>
{
return Task.FromResult(new ListPromptsResult()
{
Prompts = [
new Prompt()
{
Name = "simple_prompt",
Description = "A prompt without arguments"
},
new Prompt()
{
Name = "complex_prompt",
Description = "A prompt with arguments",
Arguments =
[
new PromptArgument()
{
Name = "temperature",
Description = "Temperature setting",
Required = true
},
new PromptArgument()
{
Name = "style",
Description = "Output style",
Required = false
}
]
}
]
});
},
GetPromptHandler = (request, cancellationToken) =>
{
List<PromptMessage> messages = [];
if (request.Params?.Name == "simple_prompt")
{
messages.Add(new PromptMessage()
{
Role = Role.User,
Content = new Content()
{
Type = "text",
Text = "This is a simple prompt without arguments."
}
});
}
else if (request.Params?.Name == "complex_prompt")
{
string temperature = request.Params.Arguments?["temperature"]?.ToString() ?? "unknown";
string style = request.Params.Arguments?["style"]?.ToString() ?? "unknown";
messages.Add(new PromptMessage()
{
Role = Role.User,
Content = new Content()
{
Type = "text",
Text = $"This is a complex prompt with arguments: temperature={temperature}, style={style}"
}
});
messages.Add(new PromptMessage()
{
Role = Role.Assistant,
Content = new Content()
{
Type = "text",
Text = "I understand. You've provided a complex prompt with temperature and style arguments. How would you like me to proceed?"
}
});
messages.Add(new PromptMessage()
{
Role = Role.User,
Content = new Content()
{
Type = "image",
Data = MCP_TINY_IMAGE,
MimeType = "image/png"
}
});
}
else
{
throw new McpServerException($"Unknown prompt: {request.Params?.Name}");
}
return Task.FromResult(new GetPromptResult()
{
Messages = messages
});
}
};
}
private static ResourcesCapability ConfigureResources()
{
List<Resource> resources = [];
List<ResourceContents> resourceContents = [];
@ -279,54 +275,56 @@ internal static class Program
const int pageSize = 10;
server.SetListResourcesHandler((request, cancellationToken) =>
return new()
{
int startIndex = 0;
request ??= new(server, new());
if (request.Params?.Cursor is not null)
ListResourcesHandler = (request, cancellationToken) =>
{
try
int startIndex = 0;
if (request.Params?.Cursor is not null)
{
var startIndexAsString = Encoding.UTF8.GetString(Convert.FromBase64String(request.Params.Cursor));
startIndex = Convert.ToInt32(startIndexAsString);
try
{
var startIndexAsString = Encoding.UTF8.GetString(Convert.FromBase64String(request.Params.Cursor));
startIndex = Convert.ToInt32(startIndexAsString);
}
catch
{
throw new McpServerException("Invalid cursor");
}
}
catch
int endIndex = Math.Min(startIndex + pageSize, resources.Count);
string? nextCursor = null;
if (endIndex < resources.Count)
{
throw new McpServerException("Invalid cursor");
nextCursor = Convert.ToBase64String(Encoding.UTF8.GetBytes(endIndex.ToString()));
}
return Task.FromResult(new ListResourcesResult()
{
NextCursor = nextCursor,
Resources = resources.GetRange(startIndex, endIndex - startIndex)
});
},
ReadResourceHandler = (request, cancellationToken) =>
{
if (request.Params?.Uri is null)
{
throw new McpServerException("Missing required argument 'uri'");
}
ResourceContents contents = resourceContents.FirstOrDefault(r => r.Uri == request.Params.Uri)
?? throw new McpServerException("Resource not found");
return Task.FromResult(new ReadResourceResult()
{
Contents = [contents]
});
}
int endIndex = Math.Min(startIndex + pageSize, resources.Count);
string? nextCursor = null;
if (endIndex < resources.Count)
{
nextCursor = Convert.ToBase64String(Encoding.UTF8.GetBytes(endIndex.ToString()));
}
return Task.FromResult(new ListResourcesResult()
{
NextCursor = nextCursor,
Resources = resources.GetRange(startIndex, endIndex - startIndex)
});
});
server.SetReadResourceHandler((request, cancellationToken) =>
{
if (request.Params?.Uri is null)
{
throw new McpServerException("Missing required argument 'uri'");
}
ResourceContents contents = resourceContents.FirstOrDefault(r => r.Uri == request.Params.Uri)
?? throw new McpServerException("Resource not found");
return Task.FromResult(new ReadResourceResult()
{
Contents = [contents]
});
});
};
}
private static void ConfigureCompletion(IMcpServer server)
private static Func<RequestContext<CompleteRequestParams>, CancellationToken, Task<CompleteResult>> ConfigureCompletion()
{
List<string> sampleResourceIds = ["1", "2", "3", "4", "5"];
Dictionary<string, List<string>> exampleCompletions = new()
@ -335,7 +333,7 @@ internal static class Program
{"temperature", ["0", "0.5", "0.7", "1.0"]},
};
server.SetGetCompletionHandler((request, cancellationToken) =>
return (request, cancellationToken) =>
{
if (request.Params?.Ref?.Type == "ref/resource")
{
@ -344,7 +342,7 @@ internal static class Program
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();
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 } });
}
@ -360,7 +358,7 @@ internal static class Program
}
throw new McpServerException($"Unknown reference type: {request.Params?.Ref.Type}");
});
};
}
static CreateMessageRequestParams CreateRequestSamplingParams(string context, string uri, int maxTokens = 100)

View file

@ -3,8 +3,7 @@ using McpDotNet.Client;
using McpDotNet.Configuration;
using McpDotNet.Protocol.Messages;
using McpDotNet.Protocol.Transport;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using McpDotNet.Protocol.Types;
namespace McpDotNet.Tests.Client;
@ -16,10 +15,43 @@ public class McpClientFactoryTests
};
[Fact]
public async Task GetClientAsync_WithValidStdioConfig_CreatesNewClient()
public async Task CreateAsync_WithInvalidArgs_Throws()
{
await Assert.ThrowsAsync<ArgumentNullException>("serverConfig", () => McpClientFactory.CreateAsync((McpServerConfig)null!, _defaultOptions));
await Assert.ThrowsAsync<ArgumentNullException>("clientOptions", () => McpClientFactory.CreateAsync(
new McpServerConfig()
{
Name = "name",
Id = "id",
TransportType = TransportTypes.StdIo,
}, (McpClientOptions)null!));
await Assert.ThrowsAsync<ArgumentException>("serverConfig", () => McpClientFactory.CreateAsync(
new McpServerConfig()
{
Name = "name",
Id = "id",
TransportType = "somethingunsupported",
},
_defaultOptions));
await Assert.ThrowsAsync<InvalidOperationException>(() => McpClientFactory.CreateAsync(
new McpServerConfig()
{
Name = "name",
Id = "id",
TransportType = TransportTypes.StdIo,
},
_defaultOptions,
(_, __) => null!));
}
[Fact]
public async Task CreateAsync_WithValidStdioConfig_CreatesNewClient()
{
// Arrange
var config = new McpServerConfig
var serverConfig = new McpServerConfig
{
Id = "test-server",
Name = "Test Server",
@ -32,30 +64,11 @@ public class McpClientFactoryTests
}
};
// 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");
var client = await McpClientFactory.CreateAsync(
serverConfig,
_defaultOptions,
(_, __) => new NopTransport());
// Assert
Assert.NotNull(client);
@ -63,137 +76,33 @@ public class McpClientFactoryTests
}
[Fact]
public async Task GetClientAsync_CalledTwice_ReturnsSameInstance()
public async Task CreateAsync_WithNoTransportOptions_CreatesNewClient()
{
// Arrange
var config = new McpServerConfig
var serverConfig = new McpServerConfig
{
Id = "test-server",
Name = "Test Server",
TransportType = TransportTypes.StdIo,
Location = "/path/to/server"
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([],
var client = await McpClientFactory.CreateAsync(
serverConfig,
_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");
(_, __) => new NopTransport());
// Assert
Assert.NotNull(client);
// We could add more assertions here about the client's configuration
}
[Fact]
public void Constructor_WithDuplicateServerIds_ThrowsArgumentException()
public async Task CreateAsync_WithValidSseConfig_CreatesNewClient()
{
// 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
var serverConfig = new McpServerConfig
{
Id = "test-server",
Name = "Test Server",
@ -201,40 +110,22 @@ public class McpClientFactoryTests
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");
var client = await McpClientFactory.CreateAsync(
serverConfig,
_defaultOptions,
(_, __) => new NopTransport());
// Assert
Assert.NotNull(client);
// We could add more assertions here about the client's configuration
}
[Fact]
public void McpFactory_WithSse_CreatesCorrectTransportOptions()
public async Task CreateAsync_WithSse_CreatesCorrectTransportOptions()
{
// Arrange
var config = new McpServerConfig
var serverConfig = new McpServerConfig
{
Id = "test-server",
Name = "Test Server",
@ -250,96 +141,21 @@ public class McpClientFactoryTests
};
// Act
using var factory = new McpClientFactory(
[config],
var client = await McpClientFactory.CreateAsync(
serverConfig,
_defaultOptions,
NullLoggerFactory.Instance
);
var transport = factory.TransportFactoryMethod(config) as SseClientTransport;
(_, __) => new NopTransport());
// 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"]);
Assert.NotNull(client);
// We could add more assertions here about the client's configuration
}
[Theory]
[InlineData("connectionTimeout", "not_a_number")]
[InlineData("maxReconnectAttempts", "invalid")]
[InlineData("reconnectDelay", "bad_value")]
public void McpFactory_WithInvalidTransportOptions_ThrowsFormatException(string key, string value)
public async Task McpFactory_WithInvalidTransportOptions_ThrowsFormatException(string key, string value)
{
// arrange
var config = new McpServerConfig
@ -354,15 +170,46 @@ public class McpClientFactoryTests
}
};
// Act
using var factory = new McpClientFactory(
[config],
_defaultOptions,
NullLoggerFactory.Instance
);
// act & assert
Assert.Throws<FormatException>(() =>
factory.TransportFactoryMethod(config));
await Assert.ThrowsAsync<ArgumentException>(() => McpClientFactory.CreateAsync(config, _defaultOptions));
}
private sealed class NopTransport : IClientTransport
{
private readonly Channel<IJsonRpcMessage> _channel = Channel.CreateUnbounded<IJsonRpcMessage>();
public bool IsConnected => true;
public ChannelReader<IJsonRpcMessage> MessageReader => _channel.Reader;
public Task ConnectAsync(CancellationToken cancellationToken = default) =>
Task.CompletedTask;
public ValueTask DisposeAsync() => default;
public Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default)
{
switch (message)
{
case JsonRpcRequest request:
_channel.Writer.TryWrite(new JsonRpcResponse
{
Id = ((JsonRpcRequest)message).Id,
Result = new InitializeResult()
{
Capabilities = new ServerCapabilities(),
ProtocolVersion = "2024-11-05",
ServerInfo = new Implementation()
{
Name = "NopTransport",
Version = "1.0.0"
},
}
});
break;
}
return Task.CompletedTask;
}
}
}

View file

@ -8,8 +8,11 @@ namespace McpDotNet.Tests;
public class ClientIntegrationTestFixture : IDisposable
{
public ILoggerFactory LoggerFactory { get; }
public McpClientFactory Factory { get; }
public McpClientOptions DefaultOptions { get; }
public McpServerConfig EverythingServerConfig { get; }
public McpServerConfig TestServerConfig { get; }
public static IEnumerable<string> ClientIds => ["everything", "test_server"];
public ClientIntegrationTestFixture()
{
@ -20,10 +23,9 @@ public class ClientIntegrationTestFixture : IDisposable
DefaultOptions = new()
{
ClientInfo = new() { Name = "IntegrationTestClient", Version = "1.0.0" },
Capabilities = new() { Sampling = new(), Roots = new() }
};
var everythingServerConfig = new McpServerConfig
EverythingServerConfig = new()
{
Id = "everything",
Name = "Everything",
@ -36,7 +38,7 @@ public class ClientIntegrationTestFixture : IDisposable
}
};
var testServerConfig = new McpServerConfig
TestServerConfig = new()
{
Id = "test_server",
Name = "TestServer",
@ -49,19 +51,21 @@ public class ClientIntegrationTestFixture : IDisposable
};
if (!OperatingSystem.IsWindows())
testServerConfig.TransportOptions["arguments"] = "TestServer.dll";
// Inject the mock transport into the factory
Factory = new McpClientFactory(
[everythingServerConfig, testServerConfig],
DefaultOptions,
LoggerFactory
);
{
TestServerConfig.TransportOptions["arguments"] = "TestServer.dll";
}
}
public Task<IMcpClient> CreateClientAsync(string clientId, McpClientOptions? clientOptions = null) =>
McpClientFactory.CreateAsync(clientId switch
{
"everything" => EverythingServerConfig,
"test_server" => TestServerConfig,
_ => throw new ArgumentException($"Unknown client ID: {clientId}")
}, clientOptions ?? DefaultOptions, loggerFactory: LoggerFactory);
public void Dispose()
{
Factory?.Dispose();
LoggerFactory?.Dispose();
GC.SuppressFinalize(this);
}

View file

@ -15,11 +15,8 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
_fixture = fixture;
}
public static IEnumerable<object[]> GetClients()
{
yield return ["everything"];
yield return ["test_server"];
}
public static IEnumerable<object[]> GetClients() =>
ClientIntegrationTestFixture.ClientIds.Select(id => new object[] { id });
[Theory]
[MemberData(nameof(GetClients))]
@ -28,7 +25,7 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
// Arrange
// Act
var client = await _fixture.Factory.GetClientAsync(clientId);
await using var client = await _fixture.CreateClientAsync(clientId);
await client.PingAsync(CancellationToken.None);
// Assert
@ -42,7 +39,7 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
// Arrange
// Act
var client = await _fixture.Factory.GetClientAsync(clientId);
await using var client = await _fixture.CreateClientAsync(clientId);
// Assert
Assert.NotNull(client.ServerCapabilities);
@ -58,7 +55,7 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
// arrange
// act
var client = await _fixture.Factory.GetClientAsync(clientId);
await using var client = await _fixture.CreateClientAsync(clientId);
var tools = await client.ListToolsAsync().ToListAsync();
// assert
@ -73,7 +70,7 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
// arrange
// act
var client = await _fixture.Factory.GetClientAsync(clientId);
await using var client = await _fixture.CreateClientAsync(clientId);
var result = await client.CallToolAsync(
"echo",
new Dictionary<string, object>
@ -97,7 +94,7 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
// arrange
// act
var client = await _fixture.Factory.GetClientAsync(clientId);
await using var client = await _fixture.CreateClientAsync(clientId);
var prompts = await client.ListPromptsAsync().ToListAsync();
// assert
@ -114,7 +111,7 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
// arrange
// act
var client = await _fixture.Factory.GetClientAsync(clientId);
await using var client = await _fixture.CreateClientAsync(clientId);
var result = await client.GetPromptAsync("simple_prompt", null, CancellationToken.None);
// assert
@ -129,7 +126,7 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
// arrange
// act
var client = await _fixture.Factory.GetClientAsync(clientId);
await using var client = await _fixture.CreateClientAsync(clientId);
var arguments = new Dictionary<string, object>
{
{ "temperature", "0.7" },
@ -149,7 +146,7 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
// arrange
// act
var client = await _fixture.Factory.GetClientAsync(clientId);
await using var client = await _fixture.CreateClientAsync(clientId);
await Assert.ThrowsAsync<McpClientException>(() =>
client.GetPromptAsync("non_existent_prompt", null, CancellationToken.None));
}
@ -161,7 +158,7 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
// arrange
// act
var client = await _fixture.Factory.GetClientAsync(clientId);
await using var client = await _fixture.CreateClientAsync(clientId);
List<Resource> allResources = [];
string? cursor = null;
@ -184,7 +181,7 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
// arrange
// act
var client = await _fixture.Factory.GetClientAsync(clientId);
await using var client = await _fixture.CreateClientAsync(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);
@ -201,7 +198,7 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
// arrange
// act
var client = await _fixture.Factory.GetClientAsync(clientId);
await using var client = await _fixture.CreateClientAsync(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);
@ -218,7 +215,7 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
// arrange
// act
var client = await _fixture.Factory.GetClientAsync(clientId);
await using var client = await _fixture.CreateClientAsync(clientId);
var result = await client.GetCompletionAsync(new Reference
{
Type = "ref/resource",
@ -240,7 +237,7 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
// arrange
// act
var client = await _fixture.Factory.GetClientAsync(clientId);
await using var client = await _fixture.CreateClientAsync(clientId);
var result = await client.GetCompletionAsync(new Reference
{
Type = "ref/prompt",
@ -258,24 +255,32 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
[Theory]
[MemberData(nameof(GetClients))]
public async Task Sampling_Stdio(string clientId)
{
var client = await _fixture.Factory.GetClientAsync(clientId);
{
// Set up the sampling handler
int samplingHandlerCalls = 0;
client.SetSamplingHandler((_, _) =>
await using var client = await _fixture.CreateClientAsync(clientId, new()
{
samplingHandlerCalls++;
return Task.FromResult(new CreateMessageResult
ClientInfo = new() { Name = "Sampling_Stdio", Version = "1.0.0" },
Capabilities = new()
{
Model = "test-model",
Role = "assistant",
Content = new Content
Sampling = new()
{
Type = "text",
Text = "Test response"
}
});
SamplingHandler = (_, _) =>
{
samplingHandlerCalls++;
return Task.FromResult(new CreateMessageResult
{
Model = "test-model",
Role = "assistant",
Content = new Content
{
Type = "text",
Text = "Test response"
}
});
},
},
},
});
// Call the server's sampleLLM tool which should trigger our sampling handler
@ -306,7 +311,7 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
// new() { Uri = "file:///test/root2", Name = "Test Root 2" }
// };
// var client = await _fixture.Factory.GetClientAsync(clientId);
// await using var client = await _fixture.Factory.GetClientAsync(clientId);
// // Set up the roots handler
// client.SetRootsHandler((request, ct) =>
@ -330,9 +335,7 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
[MemberData(nameof(GetClients))]
public async Task Notifications_Stdio(string clientId)
{
var client = await _fixture.Factory.GetClientAsync(clientId);
await client.ConnectAsync();
await using var client = await _fixture.CreateClientAsync(clientId);
// Verify we can send notifications without errors
await client.SendNotificationAsync(NotificationMethods.RootsUpdatedNotification);
@ -347,7 +350,7 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
public async Task CallTool_Stdio_MemoryServer()
{
// arrange
var config = new McpServerConfig
McpServerConfig serverConfig = new()
{
Id = "memory",
Name = "memory",
@ -359,21 +362,17 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
}
};
var options = new McpClientOptions
McpClientOptions clientOptions = new()
{
ClientInfo = new() { Name = "IntegrationTestClient", Version = "1.0.0" }
};
using var factory = new McpClientFactory([config], options, _fixture.LoggerFactory);
var client = await factory.GetClientAsync("memory");
await client.ConnectAsync();
await using var client = await McpClientFactory.CreateAsync(serverConfig, clientOptions, loggerFactory: _fixture.LoggerFactory);
// act
var result = await client.CallToolAsync(
"read_graph",
[],
CancellationToken.None
[]
);
// assert

View file

@ -27,7 +27,7 @@ public class McpServerBuilderExtensionsHandlerTests
_builder.Object.WithListToolsHandler(handler);
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
var options = serviceProvider.GetRequiredService<IOptions<McpServerHandlers>>().Value;
Assert.Equal(handler, options.ListToolsHandler);
}
@ -40,7 +40,7 @@ public class McpServerBuilderExtensionsHandlerTests
_builder.Object.WithCallToolHandler(handler);
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
var options = serviceProvider.GetRequiredService<IOptions<McpServerHandlers>>().Value;
Assert.Equal(handler, options.CallToolHandler);
}
@ -53,7 +53,7 @@ public class McpServerBuilderExtensionsHandlerTests
_builder.Object.WithListPromptsHandler(handler);
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
var options = serviceProvider.GetRequiredService<IOptions<McpServerHandlers>>().Value;
Assert.Equal(handler, options.ListPromptsHandler);
}
@ -66,7 +66,7 @@ public class McpServerBuilderExtensionsHandlerTests
_builder.Object.WithGetPromptHandler(handler);
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
var options = serviceProvider.GetRequiredService<IOptions<McpServerHandlers>>().Value;
Assert.Equal(handler, options.GetPromptHandler);
}
@ -79,7 +79,7 @@ public class McpServerBuilderExtensionsHandlerTests
_builder.Object.WithListResourcesHandler(handler);
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
var options = serviceProvider.GetRequiredService<IOptions<McpServerHandlers>>().Value;
Assert.Equal(handler, options.ListResourcesHandler);
}
@ -92,7 +92,7 @@ public class McpServerBuilderExtensionsHandlerTests
_builder.Object.WithReadResourceHandler(handler);
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
var options = serviceProvider.GetRequiredService<IOptions<McpServerHandlers>>().Value;
Assert.Equal(handler, options.ReadResourceHandler);
}
@ -105,7 +105,7 @@ public class McpServerBuilderExtensionsHandlerTests
_builder.Object.WithGetCompletionHandler(handler);
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
var options = serviceProvider.GetRequiredService<IOptions<McpServerHandlers>>().Value;
Assert.Equal(handler, options.GetCompletionHandler);
}
@ -118,7 +118,7 @@ public class McpServerBuilderExtensionsHandlerTests
_builder.Object.WithSubscribeToResourcesHandler(handler);
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
var options = serviceProvider.GetRequiredService<IOptions<McpServerHandlers>>().Value;
Assert.Equal(handler, options.SubscribeToResourcesHandler);
}
@ -131,7 +131,7 @@ public class McpServerBuilderExtensionsHandlerTests
_builder.Object.WithUnsubscribeFromResourcesHandler(handler);
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
var options = serviceProvider.GetRequiredService<IOptions<McpServerHandlers>>().Value;
Assert.Equal(handler, options.UnsubscribeFromResourcesHandler);
}

View file

@ -26,7 +26,7 @@ public class McpServerBuilderExtensionsToolsTests
_builder.Object.WithTools(typeof(EchoTool));
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
var options = serviceProvider.GetRequiredService<IOptions<McpServerHandlers>>().Value;
Assert.NotNull(options.ListToolsHandler);
Assert.NotNull(options.CallToolHandler);
@ -38,7 +38,7 @@ public class McpServerBuilderExtensionsToolsTests
_builder.Object.WithTools(typeof(EchoTool));
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
var options = serviceProvider.GetRequiredService<IOptions<McpServerHandlers>>().Value;
var result = await options.ListToolsHandler!(new(Mock.Of<IMcpServer>(), new()), CancellationToken.None);
Assert.NotNull(result);
@ -69,7 +69,7 @@ public class McpServerBuilderExtensionsToolsTests
_builder.Object.WithTools(typeof(EchoTool));
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
var options = serviceProvider.GetRequiredService<IOptions<McpServerHandlers>>().Value;
var result = await options.CallToolHandler!(new(Mock.Of<IMcpServer>(), new() { Name = "Echo", Arguments = new() { { "message", "Peter" } } }), CancellationToken.None);
Assert.NotNull(result);
@ -86,7 +86,7 @@ public class McpServerBuilderExtensionsToolsTests
_builder.Object.WithTools(typeof(EchoTool));
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
var options = serviceProvider.GetRequiredService<IOptions<McpServerHandlers>>().Value;
var result = await options.CallToolHandler!(new(Mock.Of<IMcpServer>(), new() { Name = "EchoArray", Arguments = new() { { "message", "Peter" } } }), CancellationToken.None);
Assert.NotNull(result);
@ -103,7 +103,7 @@ public class McpServerBuilderExtensionsToolsTests
_builder.Object.WithTools(typeof(EchoTool));
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
var options = serviceProvider.GetRequiredService<IOptions<McpServerHandlers>>().Value;
var result = await options.CallToolHandler!(new(Mock.Of<IMcpServer>(), new() { Name = "ReturnNull" }), CancellationToken.None);
Assert.NotNull(result);
@ -120,7 +120,7 @@ public class McpServerBuilderExtensionsToolsTests
_builder.Object.WithTools(typeof(EchoTool));
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
var options = serviceProvider.GetRequiredService<IOptions<McpServerHandlers>>().Value;
var result = await options.CallToolHandler!(new(Mock.Of<IMcpServer>(), new() { Name = "ReturnJson" }), CancellationToken.None);
Assert.NotNull(result);
@ -137,7 +137,7 @@ public class McpServerBuilderExtensionsToolsTests
_builder.Object.WithTools(typeof(EchoTool));
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
var options = serviceProvider.GetRequiredService<IOptions<McpServerHandlers>>().Value;
var result = await options.CallToolHandler!(new(Mock.Of<IMcpServer>(), new() { Name = "ReturnInteger" }), CancellationToken.None);
Assert.NotNull(result);
@ -154,7 +154,7 @@ public class McpServerBuilderExtensionsToolsTests
_builder.Object.WithTools(typeof(EchoTool));
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
var options = serviceProvider.GetRequiredService<IOptions<McpServerHandlers>>().Value;
using var cts = new CancellationTokenSource();
var token = cts.Token;
@ -173,7 +173,7 @@ public class McpServerBuilderExtensionsToolsTests
_builder.Object.WithTools(typeof(EchoTool));
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
var options = serviceProvider.GetRequiredService<IOptions<McpServerHandlers>>().Value;
using var cts = new CancellationTokenSource();
var token = cts.Token;
@ -190,7 +190,7 @@ public class McpServerBuilderExtensionsToolsTests
_builder.Object.WithTools(typeof(EchoTool));
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
var options = serviceProvider.GetRequiredService<IOptions<McpServerHandlers>>().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);
@ -207,7 +207,7 @@ public class McpServerBuilderExtensionsToolsTests
_builder.Object.WithTools(typeof(EchoTool));
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
var options = serviceProvider.GetRequiredService<IOptions<McpServerHandlers>>().Value;
var action = async () => await options.CallToolHandler!(new(Mock.Of<IMcpServer>(), new() { Name = "ReturnError" }), CancellationToken.None);
@ -228,7 +228,7 @@ public class McpServerBuilderExtensionsToolsTests
_builder.Object.WithTool<EchoToolWithDi>();
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
var options = serviceProvider.GetRequiredService<IOptions<McpServerHandlers>>().Value;
var mcpServer = new Mock<IMcpServer>();
mcpServer.SetupGet(s => s.ServiceProvider).Returns(serviceProvider);
@ -249,7 +249,7 @@ public class McpServerBuilderExtensionsToolsTests
_builder.Object.WithTools(typeof(EchoTool));
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
var options = serviceProvider.GetRequiredService<IOptions<McpServerHandlers>>().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);
@ -261,7 +261,7 @@ public class McpServerBuilderExtensionsToolsTests
_builder.Object.WithTools(typeof(EchoTool));
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
var options = serviceProvider.GetRequiredService<IOptions<McpServerHandlers>>().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);
@ -291,7 +291,7 @@ public class McpServerBuilderExtensionsToolsTests
_builder.Object.WithTools();
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
var options = serviceProvider.GetRequiredService<IOptions<McpServerHandlers>>().Value;
var result = await options.ListToolsHandler!(new(Mock.Of<IMcpServer>(), new()), CancellationToken.None);
Assert.NotNull(result);
@ -316,7 +316,7 @@ public class McpServerBuilderExtensionsToolsTests
_builder.Object.WithTools(typeof(EchoTool));
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerDelegates>>().Value;
var options = serviceProvider.GetRequiredService<IOptions<McpServerHandlers>>().Value;
var result = await options.ListToolsHandler!(new(Mock.Of<IMcpServer>(), new()), CancellationToken.None);
Assert.NotNull(result);

View file

@ -1,54 +1,43 @@
using McpDotNet.Protocol.Messages;
using McpDotNet.Protocol.Types;
using McpDotNet.Protocol.Types;
using McpDotNet.Server;
namespace McpDotNet.Tests.Server;
public class McpServerDelegatesTests
public class McpServerHandlerTests
{
[Fact]
public void Applies_All_Given_Delegates()
public void AllPropertiesAreSettable()
{
var container = new McpServerDelegates();
var server = new ExposeSetHandlersServer();
var handlers = new McpServerHandlers();
container.ListToolsHandler = (p, c) => Task.FromResult(new ListToolsResult());
container.CallToolHandler = (p, c) => Task.FromResult(new CallToolResponse());
container.ListPromptsHandler = (p, c) => Task.FromResult(new ListPromptsResult());
container.GetPromptHandler = (p, c) => Task.FromResult(new GetPromptResult());
container.ListResourcesHandler = (p, c) => Task.FromResult(new ListResourcesResult());
container.ReadResourceHandler = (p, c) => Task.FromResult(new ReadResourceResult());
container.GetCompletionHandler = (p, c) => Task.FromResult(new CompleteResult());
container.SubscribeToResourcesHandler = (s, c) => Task.CompletedTask;
container.UnsubscribeFromResourcesHandler = (s, c) => Task.CompletedTask;
Assert.Null(handlers.ListToolsHandler);
Assert.Null(handlers.CallToolHandler);
Assert.Null(handlers.ListPromptsHandler);
Assert.Null(handlers.GetPromptHandler);
Assert.Null(handlers.ListResourcesHandler);
Assert.Null(handlers.ReadResourceHandler);
Assert.Null(handlers.GetCompletionHandler);
Assert.Null(handlers.SubscribeToResourcesHandler);
Assert.Null(handlers.UnsubscribeFromResourcesHandler);
container.Apply(server);
handlers.ListToolsHandler = (p, c) => Task.FromResult(new ListToolsResult());
handlers.CallToolHandler = (p, c) => Task.FromResult(new CallToolResponse());
handlers.ListPromptsHandler = (p, c) => Task.FromResult(new ListPromptsResult());
handlers.GetPromptHandler = (p, c) => Task.FromResult(new GetPromptResult());
handlers.ListResourcesHandler = (p, c) => Task.FromResult(new ListResourcesResult());
handlers.ReadResourceHandler = (p, c) => Task.FromResult(new ReadResourceResult());
handlers.GetCompletionHandler = (p, c) => Task.FromResult(new CompleteResult());
handlers.SubscribeToResourcesHandler = (s, c) => Task.CompletedTask;
handlers.UnsubscribeFromResourcesHandler = (s, c) => Task.CompletedTask;
Assert.Equal(container.ListToolsHandler, server.Handlers[OperationNames.ListTools]);
Assert.Equal(container.CallToolHandler, server.Handlers[OperationNames.CallTool]);
Assert.Equal(container.ListPromptsHandler, server.Handlers[OperationNames.ListPrompts]);
Assert.Equal(container.GetPromptHandler, server.Handlers[OperationNames.GetPrompt]);
Assert.Equal(container.ListResourcesHandler, server.Handlers[OperationNames.ListResources]);
Assert.Equal(container.ReadResourceHandler, server.Handlers[OperationNames.ReadResource]);
Assert.Equal(container.GetCompletionHandler, server.Handlers[OperationNames.GetCompletion]);
Assert.Equal(container.SubscribeToResourcesHandler, server.Handlers[OperationNames.SubscribeToResources]);
Assert.Equal(container.UnsubscribeFromResourcesHandler, server.Handlers[OperationNames.UnsubscribeFromResources]);
}
private sealed class ExposeSetHandlersServer : IMcpServer
{
public Dictionary<string, Delegate> Handlers = [];
public void SetOperationHandler(string operationName, Delegate handler) => Handlers[operationName] = handler;
public ValueTask DisposeAsync() => default;
public bool IsInitialized => throw new NotImplementedException();
public ClientCapabilities? ClientCapabilities => throw new NotImplementedException();
public Implementation? ClientInfo => throw new NotImplementedException();
public IServiceProvider? ServiceProvider => throw new NotImplementedException();
public void AddNotificationHandler(string method, Func<JsonRpcNotification, Task> handler) => throw new NotImplementedException();
public Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default) => throw new NotImplementedException();
public Task<T> SendRequestAsync<T>(JsonRpcRequest request, CancellationToken cancellationToken) where T : class => throw new NotImplementedException();
public Task StartAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException();
Assert.NotNull(handlers.ListToolsHandler);
Assert.NotNull(handlers.CallToolHandler);
Assert.NotNull(handlers.ListPromptsHandler);
Assert.NotNull(handlers.GetPromptHandler);
Assert.NotNull(handlers.ListResourcesHandler);
Assert.NotNull(handlers.ReadResourceHandler);
Assert.NotNull(handlers.GetCompletionHandler);
Assert.NotNull(handlers.SubscribeToResourcesHandler);
Assert.NotNull(handlers.UnsubscribeFromResourcesHandler);
}
}

View file

@ -1,8 +1,7 @@
using McpDotNet.Protocol.Transport;
using McpDotNet.Protocol.Types;
using McpDotNet.Server;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
namespace McpDotNet.Tests.Server;
@ -10,16 +9,12 @@ namespace McpDotNet.Tests.Server;
public class McpServerFactoryTests
{
private readonly Mock<IServerTransport> _serverTransport;
private readonly Mock<ILoggerFactory> _loggerFactory;
private readonly Mock<IOptions<McpServerDelegates>> _serverDelegates;
private readonly McpServerOptions _options;
private readonly IServiceProvider _serviceProvider;
public McpServerFactoryTests()
{
_serverTransport = new Mock<IServerTransport>();
_loggerFactory = new Mock<ILoggerFactory>();
_serverDelegates = new Mock<IOptions<McpServerDelegates>>();
_options = new McpServerOptions
{
ServerInfo = new Implementation { Name = "TestServer", Version = "1.0" },
@ -30,54 +25,26 @@ public class McpServerFactoryTests
}
[Fact]
public void Constructor_Should_Initialize_With_Valid_Parameters()
public async Task Create_Should_Initialize_With_Valid_Parameters()
{
// Arrange & Act
var factory = new McpServerFactory(_serverTransport.Object, _options, _loggerFactory.Object, _serverDelegates.Object, _serviceProvider);
await using IMcpServer server = McpServerFactory.Create(_serverTransport.Object, _options, NullLoggerFactory.Instance);
// Assert
Assert.NotNull(factory);
Assert.NotNull(server);
}
[Fact]
public void Constructor_Throws_For_Null_ServerTransport()
{
// Arrange, Act & Assert
Assert.Throws<ArgumentNullException>(() => new McpServerFactory(null!, _options, _loggerFactory.Object, _serverDelegates.Object, _serviceProvider));
Assert.Throws<ArgumentNullException>("serverTransport", () => McpServerFactory.Create(null!, _options, NullLoggerFactory.Instance));
}
[Fact]
public void Constructor_Throws_For_Null_Options()
{
// Arrange, Act & Assert
Assert.Throws<ArgumentNullException>(() => new McpServerFactory(_serverTransport.Object, null!, _loggerFactory.Object, _serverDelegates.Object, _serviceProvider));
}
[Fact]
public void Constructor_Does_Not_Throw_For_Null_ServerDelegates()
{
var factory = new McpServerFactory(_serverTransport.Object, _options, _loggerFactory.Object, null, _serviceProvider);
Assert.NotNull(factory);
}
[Fact]
public void Constructor_Does_Not_Throw_For_Null_ServiceProvider()
{
var factory = new McpServerFactory(_serverTransport.Object, _options, _loggerFactory.Object, _serverDelegates.Object, null);
Assert.NotNull(factory);
}
[Fact]
public void CreateServer_Return_IMcpServerInstance()
{
// Arrange
var factory = new McpServerFactory(_serverTransport.Object, _options, _loggerFactory.Object, _serverDelegates.Object, _serviceProvider);
// Act
var server = factory.CreateServer();
// Assert
Assert.NotNull(server);
Assert.IsAssignableFrom<IMcpServer>(server);
Assert.Throws<ArgumentNullException>("serverOptions", () => McpServerFactory.Create(_serverTransport.Object, null!, NullLoggerFactory.Instance));
}
}

View file

@ -34,7 +34,7 @@ public class McpServerTests
ServerInfo = new Implementation { Name = "TestServer", Version = "1.0" },
ProtocolVersion = "2024",
InitializationTimeout = TimeSpan.FromSeconds(30),
Capabilities = capabilities
Capabilities = capabilities,
};
}
@ -63,10 +63,13 @@ public class McpServerTests
}
[Fact]
public void Constructor_Throws_For_Null_LoggerFactory()
public async Task Constructor_Does_Not_Throw_For_Null_Logger()
{
// Arrange, Act & Assert
Assert.Throws<ArgumentNullException>(() => new McpServer(_serverTransport.Object, _options, null!, _serviceProvider));
// Arrange & Act
await using var server = new McpServer(_serverTransport.Object, _options, null, _serviceProvider);
// Assert
Assert.NotNull(server);
}
[Fact]
@ -226,103 +229,114 @@ public class McpServerTests
[Fact]
public async Task Can_Handle_Ping_Requests()
{
await Can_Handle_Requests(null, "ping",
configureServer: server => { },
assertResult: response =>
{
Assert.IsType<PingResult>(response);
});
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(null, "initialize",
configureServer: server => { },
assertResult: response =>
{
Assert.IsType<InitializeResult>(response);
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);
});
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(null, "completion/complete",
configureServer: server => { },
assertResult: response =>
{
Assert.IsType<CompleteResult>(response);
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);
});
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(null, "completion/complete",
configureServer: server =>
{
server.SetGetCompletionHandler((request, ct) =>
{
return Task.FromResult(new CompleteResult
{
Completion = new()
{
Values = ["test"],
Total = 2,
HasMore = true
}
});
});
},
assertResult: response =>
{
Assert.IsType<CompleteResult>(response);
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);
});
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() }, "resources/list",
configureServer: server =>
{
server.SetListResourcesHandler((request, ct) =>
{
return Task.FromResult(new ListResourcesResult
{
Resources = [new() { Uri = "test", Name = "Test Resource" }]
});
});
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);
},
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);
});
var result = (ListResourcesResult)response;
Assert.NotNull(result.Resources);
Assert.NotEmpty(result.Resources);
Assert.Equal("test", result.Resources[0].Uri);
});
}
[Fact]
@ -334,26 +348,32 @@ public class McpServerTests
[Fact]
public async Task Can_Handle_ResourcesRead_Requests()
{
await Can_Handle_Requests(new ServerCapabilities { Resources = new() }, "resources/read",
configureServer: server =>
{
server.SetReadResourceHandler((request, ct) =>
{
return Task.FromResult(new ReadResourceResult
{
Contents = [new() { Text = "test" }]
});
});
},
assertResult: response =>
{
Assert.IsType<ReadResourceResult>(response);
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);
});
var result = (ReadResourceResult)response;
Assert.NotNull(result.Contents);
Assert.NotEmpty(result.Contents);
Assert.Equal("test", result.Contents[0].Text);
});
}
[Fact]
@ -365,26 +385,32 @@ public class McpServerTests
[Fact]
public async Task Can_Handle_List_Prompts_Requests()
{
await Can_Handle_Requests(new ServerCapabilities { Prompts = new() }, "prompts/list",
configureServer: server =>
await Can_Handle_Requests(
new ServerCapabilities
{
server.SetListPromptsHandler((request, ct) =>
Prompts = new()
{
return Task.FromResult(new ListPromptsResult
ListPromptsHandler = (request, ct) =>
{
Prompts = [new() { Name = "test" }]
});
});
return Task.FromResult(new ListPromptsResult
{
Prompts = [new() { Name = "test" }]
});
},
GetPromptHandler = (request, ct) => throw new NotImplementedException(),
},
},
assertResult: response =>
{
Assert.IsType<ListPromptsResult>(response);
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);
});
var result = (ListPromptsResult)response;
Assert.NotNull(result.Prompts);
Assert.NotEmpty(result.Prompts);
Assert.Equal("test", result.Prompts[0].Name);
});
}
[Fact]
@ -396,24 +422,24 @@ public class McpServerTests
[Fact]
public async Task Can_Handle_Get_Prompts_Requests()
{
await Can_Handle_Requests(new ServerCapabilities { Prompts = new() }, "prompts/get",
configureServer: server =>
await Can_Handle_Requests(
new ServerCapabilities
{
server.SetGetPromptHandler((request, ct) =>
Prompts = new()
{
return Task.FromResult(new GetPromptResult
{
Description = "test"
});
});
GetPromptHandler = (request, ct) => Task.FromResult(new GetPromptResult { Description = "test" }),
ListPromptsHandler = (request, ct) => throw new NotImplementedException(),
}
},
assertResult: response =>
{
Assert.IsType<GetPromptResult>(response);
method: "prompts/get",
configureOptions: null,
assertResult: response =>
{
Assert.IsType<GetPromptResult>(response);
var result = (GetPromptResult)response;
Assert.Equal("test", result.Description);
});
var result = (GetPromptResult)response;
Assert.Equal("test", result.Description);
});
}
[Fact]
@ -425,25 +451,31 @@ public class McpServerTests
[Fact]
public async Task Can_Handle_List_Tools_Requests()
{
await Can_Handle_Requests(new ServerCapabilities { Tools = new() }, "tools/list",
configureServer: server =>
await Can_Handle_Requests(
new ServerCapabilities
{
server.SetListToolsHandler((request, ct) =>
Tools = new()
{
return Task.FromResult(new ListToolsResult
ListToolsHandler = (request, ct) =>
{
Tools = [new() { Name = "test" }]
});
});
return Task.FromResult(new ListToolsResult
{
Tools = [new() { Name = "test" }]
});
},
CallToolHandler = (request, ct) => throw new NotImplementedException(),
}
},
assertResult: response =>
{
Assert.IsType<ListToolsResult>(response);
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);
});
var result = (ListToolsResult)response;
Assert.NotEmpty(result.Tools);
Assert.Equal("test", result.Tools[0].Name);
});
}
[Fact]
@ -455,25 +487,31 @@ public class McpServerTests
[Fact]
public async Task Can_Handle_Call_Tool_Requests()
{
await Can_Handle_Requests(new ServerCapabilities { Tools = new() }, "tools/call",
configureServer: server =>
await Can_Handle_Requests(
new ServerCapabilities
{
server.SetCallToolHandler((request, ct) =>
Tools = new()
{
return Task.FromResult(new CallToolResponse
CallToolHandler = (request, ct) =>
{
Content = [new Content { Text = "test" }]
});
});
},
assertResult: response =>
{
Assert.IsType<CallToolResponse>(response);
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);
});
var result = (CallToolResponse)response;
Assert.NotEmpty(result.Content);
Assert.Equal("test", result.Content[0].Text);
});
}
[Fact]
@ -482,17 +520,19 @@ public class McpServerTests
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, Action<IMcpServer> configureServer, Action<object> assertResult)
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 = serverCapabilities == null ? _options : CreateOptions(serverCapabilities);
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();
configureServer(server);
var receivedMessage = new TaskCompletionSource<JsonRpcResponse>();
transport.OnMessageSent = (message) =>
@ -521,32 +561,6 @@ public class McpServerTests
await using var transport = new TestServerTransport();
var options = CreateOptions(serverCapabilities);
await using var server = new McpServer(transport, options, _loggerFactory.Object, _serviceProvider);
await server.StartAsync();
var receivedMessage = new TaskCompletionSource<IJsonRpcMessage>();
transport.OnMessageSent = (message) =>
{
if (message is JsonRpcError 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.IsType<JsonRpcError>(response);
var result = (JsonRpcError)response;
Assert.NotNull(result.Error);
Assert.Equal(expectedError, result.Error.Message);
Assert.Throws<McpServerException>(() => McpServerFactory.Create(transport, options, _loggerFactory.Object, _serviceProvider));
}
}

View file

@ -36,14 +36,8 @@ public class SseIntegrationTests
Location = "http://localhost:5000/sse"
};
using var factory = new McpClientFactory(
[defaultConfig],
defaultOptions,
loggerFactory
);
// Act
var client = await factory.GetClientAsync("test_server");
var client = await McpClientFactory.CreateAsync(defaultConfig, defaultOptions, loggerFactory: loggerFactory);
// Wait for SSE connection to be established
await server.WaitForConnectionAsync(TimeSpan.FromSeconds(10));
@ -82,14 +76,8 @@ public class SseIntegrationTests
Location = $"http://localhost:{port}/sse"
};
using var factory = new McpClientFactory(
[defaultConfig],
defaultOptions,
loggerFactory
);
// Create client and run tests
var client = await factory.GetClientAsync("everything");
var client = await McpClientFactory.CreateAsync(defaultConfig, defaultOptions, loggerFactory: loggerFactory);
var tools = await client.ListToolsAsync().ToListAsync();
// assert
@ -110,6 +98,16 @@ public class SseIntegrationTests
await using var fixture = new EverythingSseServerFixture(port);
await fixture.StartAsync();
var defaultConfig = new McpServerConfig
{
Id = "everything",
Name = "Everything",
TransportType = TransportTypes.Sse,
TransportOptions = [],
Location = $"http://localhost:{port}/sse"
};
int samplingHandlerCalls = 0;
var defaultOptions = new McpClientOptions
{
ClientInfo = new()
@ -120,41 +118,26 @@ public class SseIntegrationTests
Capabilities = new()
{
Sampling = new()
}
};
var defaultConfig = new McpServerConfig
{
Id = "everything",
Name = "Everything",
TransportType = TransportTypes.Sse,
TransportOptions = [],
Location = $"http://localhost:{port}/sse"
};
using var factory = new McpClientFactory(
[defaultConfig],
defaultOptions,
loggerFactory
);
var client = await factory.GetClientAsync("everything");
// Set up the sampling handler
int samplingHandlerCalls = 0;
client.SetSamplingHandler((_, _) =>
{
samplingHandlerCalls++;
return Task.FromResult(new CreateMessageResult
{
Model = "test-model",
Role = "assistant",
Content = new Content
{
Type = "text",
Text = "Test response"
}
});
});
SamplingHandler = (_, _) =>
{
samplingHandlerCalls++;
return Task.FromResult(new CreateMessageResult
{
Model = "test-model",
Role = "assistant",
Content = new Content
{
Type = "text",
Text = "Test response"
}
});
},
},
},
};
var client = await McpClientFactory.CreateAsync(defaultConfig, defaultOptions, loggerFactory: loggerFactory);
// Call the server's sampleLLM tool which should trigger our sampling handler
var result = await client.CallToolAsync(
@ -200,14 +183,8 @@ public class SseIntegrationTests
Location = "http://localhost:5000/sse"
};
using var factory = new McpClientFactory(
[defaultConfig],
defaultOptions,
loggerFactory
);
// Act
var client = await factory.GetClientAsync("test_server");
var client = await McpClientFactory.CreateAsync(defaultConfig, defaultOptions, loggerFactory: loggerFactory);
// Wait for SSE connection to be established
await server.WaitForConnectionAsync(TimeSpan.FromSeconds(10));
@ -245,14 +222,8 @@ public class SseIntegrationTests
Location = "http://localhost:5000/sse"
};
using var factory = new McpClientFactory(
[defaultConfig],
defaultOptions,
loggerFactory
);
// Act
var client = await factory.GetClientAsync("test_server");
var client = await McpClientFactory.CreateAsync(defaultConfig, defaultOptions, loggerFactory: loggerFactory);
// Wait for SSE connection to be established
await server.WaitForConnectionAsync(TimeSpan.FromSeconds(10));
@ -300,14 +271,8 @@ public class SseIntegrationTests
Location = "http://localhost:5000/sse"
};
using var factory = new McpClientFactory(
[defaultConfig],
defaultOptions,
loggerFactory
);
// Act
var client = await factory.GetClientAsync("test_server");
var client = await McpClientFactory.CreateAsync(defaultConfig, defaultOptions, loggerFactory: loggerFactory);
var mcpClient = (McpClient)client;
var transport = (SseClientTransport)mcpClient.Transport;