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:
parent
63b24a4036
commit
08b0a04dab
11 changed files with 1306 additions and 2243 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue