diff --git a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs index 2646883..f827fd3 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs @@ -292,7 +292,7 @@ internal sealed partial class McpServerImpl : McpServer var subscribe = resourcesCapability?.Subscribe; // Handle resources provided via DI. - if (resources is { IsEmpty: false }) + if (resources is not null) { var originalListResourcesHandler = listResourcesHandler; listResourcesHandler = async (request, cancellationToken) => @@ -453,7 +453,7 @@ internal sealed partial class McpServerImpl : McpServer var listChanged = promptsCapability?.ListChanged; // Handle tools provided via DI by augmenting the handlers to incorporate them. - if (prompts is { IsEmpty: false }) + if (prompts is not null) { var originalListPromptsHandler = listPromptsHandler; listPromptsHandler = async (request, cancellationToken) => @@ -541,7 +541,7 @@ internal sealed partial class McpServerImpl : McpServer var listChanged = toolsCapability?.ListChanged; // Handle tools provided via DI by augmenting the handlers to incorporate them. - if (tools is { IsEmpty: false }) + if (tools is not null) { var originalListToolsHandler = listToolsHandler; listToolsHandler = async (request, cancellationToken) => diff --git a/tests/Common/Utils/MockHttpHandler.cs b/tests/Common/Utils/MockHttpHandler.cs index d15ec3d..5e58a6c 100644 --- a/tests/Common/Utils/MockHttpHandler.cs +++ b/tests/Common/Utils/MockHttpHandler.cs @@ -1,6 +1,4 @@ -using System.Net.Http; - -namespace ModelContextProtocol.Tests.Utils; +namespace ModelContextProtocol.Tests.Utils; public class MockHttpHandler : HttpMessageHandler { diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/ModelContextProtocol.AspNetCore.Tests.csproj b/tests/ModelContextProtocol.AspNetCore.Tests/ModelContextProtocol.AspNetCore.Tests.csproj index 7adeb03..5d21d0a 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/ModelContextProtocol.AspNetCore.Tests.csproj +++ b/tests/ModelContextProtocol.AspNetCore.Tests/ModelContextProtocol.AspNetCore.Tests.csproj @@ -44,7 +44,6 @@ - runtime; build; native; contentfiles; analyzers; buildtransitive @@ -52,6 +51,10 @@ + + + + diff --git a/tests/ModelContextProtocol.TestSseServer/ModelContextProtocol.TestSseServer.csproj b/tests/ModelContextProtocol.TestSseServer/ModelContextProtocol.TestSseServer.csproj index 3296ff4..e2a1c16 100644 --- a/tests/ModelContextProtocol.TestSseServer/ModelContextProtocol.TestSseServer.csproj +++ b/tests/ModelContextProtocol.TestSseServer/ModelContextProtocol.TestSseServer.csproj @@ -9,8 +9,6 @@ - - diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs index 7d037fb..c2e2b9d 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs @@ -7,7 +7,6 @@ using ModelContextProtocol.Server; using Moq; using System.Collections; using System.ComponentModel; -using System.Text.Json; using System.Threading.Channels; using static ModelContextProtocol.Tests.Configuration.McpServerBuilderExtensionsPromptsTests; diff --git a/tests/ModelContextProtocol.Tests/ModelContextProtocol.Tests.csproj b/tests/ModelContextProtocol.Tests/ModelContextProtocol.Tests.csproj index 944eec4..4259446 100644 --- a/tests/ModelContextProtocol.Tests/ModelContextProtocol.Tests.csproj +++ b/tests/ModelContextProtocol.Tests/ModelContextProtocol.Tests.csproj @@ -52,7 +52,6 @@ - @@ -61,6 +60,10 @@ + + + + diff --git a/tests/ModelContextProtocol.Tests/Protocol/CancellationTests.cs b/tests/ModelContextProtocol.Tests/Protocol/CancellationTests.cs index 80c6b1e..e21f1f9 100644 --- a/tests/ModelContextProtocol.Tests/Protocol/CancellationTests.cs +++ b/tests/ModelContextProtocol.Tests/Protocol/CancellationTests.cs @@ -1,5 +1,4 @@ using Microsoft.Extensions.DependencyInjection; -using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; diff --git a/tests/ModelContextProtocol.Tests/Protocol/ElicitationTests.cs b/tests/ModelContextProtocol.Tests/Protocol/ElicitationTests.cs index f3ae33e..76f967b 100644 --- a/tests/ModelContextProtocol.Tests/Protocol/ElicitationTests.cs +++ b/tests/ModelContextProtocol.Tests/Protocol/ElicitationTests.cs @@ -1,7 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; -using ModelContextProtocol.Server; using System.Text.Json; namespace ModelContextProtocol.Tests.Configuration; diff --git a/tests/ModelContextProtocol.Tests/Server/EmptyCollectionTests.cs b/tests/ModelContextProtocol.Tests/Server/EmptyCollectionTests.cs new file mode 100644 index 0000000..882fd50 --- /dev/null +++ b/tests/ModelContextProtocol.Tests/Server/EmptyCollectionTests.cs @@ -0,0 +1,165 @@ +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DependencyInjection; +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; + +namespace ModelContextProtocol.Tests.Server; + +/// +/// Tests to verify that handlers are synthesized for empty collections that can be populated dynamically. +/// This addresses the issue where handlers were only created when collections had items. +/// +public class EmptyCollectionTests : ClientServerTestBase +{ + public EmptyCollectionTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) { } + + private McpServerResourceCollection _resourceCollection = []; + private McpServerPrimitiveCollection _toolCollection = []; + private McpServerPrimitiveCollection _promptCollection = []; + + protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder) => + mcpServerBuilder.Services.Configure(options => + { + options.ResourceCollection = _resourceCollection; + options.ToolCollection = _toolCollection; + options.PromptCollection = _promptCollection; + }); + + [Fact] + public async Task EmptyResourceCollection_CanAddResourcesDynamically() + { + var client = await CreateMcpClientForServer(); + + // Initially, the resource collection is empty + var initialResources = await client.ListResourcesAsync(TestContext.Current.CancellationToken); + Assert.Empty(initialResources); + + // Add a resource dynamically + _resourceCollection.Add(McpServerResource.Create( + () => "test content", + new() { UriTemplate = "test://resource/1" })); + + // The resource should now be listed + var updatedResources = await client.ListResourcesAsync(TestContext.Current.CancellationToken); + Assert.Single(updatedResources); + Assert.Equal("test://resource/1", updatedResources[0].Uri); + } + + [Fact] + public async Task EmptyToolCollection_CanAddToolsDynamically() + { + var client = await CreateMcpClientForServer(); + + // Initially, the tool collection is empty + var initialTools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); + Assert.Empty(initialTools); + + // Add a tool dynamically + _toolCollection.Add(McpServerTool.Create( + () => "test result", + new() { Name = "test_tool", Description = "A test tool" })); + + // The tool should now be listed + var updatedTools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); + Assert.Single(updatedTools); + Assert.Equal("test_tool", updatedTools[0].Name); + } + + [Fact] + public async Task EmptyPromptCollection_CanAddPromptsDynamically() + { + var client = await CreateMcpClientForServer(); + + // Initially, the prompt collection is empty + var initialPrompts = await client.ListPromptsAsync(TestContext.Current.CancellationToken); + Assert.Empty(initialPrompts); + + // Add a prompt dynamically + _promptCollection.Add(McpServerPrompt.Create( + () => new ChatMessage(ChatRole.User, "test prompt"), + new() { Name = "test_prompt", Description = "A test prompt" })); + + // The prompt should now be listed + var updatedPrompts = await client.ListPromptsAsync(TestContext.Current.CancellationToken); + Assert.Single(updatedPrompts); + Assert.Equal("test_prompt", updatedPrompts[0].Name); + } + + [Fact] + public async Task EmptyResourceCollection_CanCallReadResourceAfterAddingDynamically() + { + var client = await CreateMcpClientForServer(); + + // Add a resource dynamically + _resourceCollection.Add(McpServerResource.Create( + () => "dynamic content", + new() { UriTemplate = "test://resource/dynamic" })); + + // Read the resource + var result = await client.ReadResourceAsync("test://resource/dynamic", TestContext.Current.CancellationToken); + Assert.NotNull(result); + Assert.Single(result.Contents); + Assert.IsType(result.Contents[0]); + Assert.Equal("dynamic content", ((TextResourceContents)result.Contents[0]).Text); + } + + [Fact] + public async Task EmptyToolCollection_CanCallToolAfterAddingDynamically() + { + var client = await CreateMcpClientForServer(); + + // Add a tool dynamically + _toolCollection.Add(McpServerTool.Create( + () => "dynamic result", + new() { Name = "dynamic_tool", Description = "A dynamic tool" })); + + // Call the tool + var result = await client.CallToolAsync("dynamic_tool", cancellationToken: TestContext.Current.CancellationToken); + Assert.NotNull(result); + Assert.Single(result.Content); + Assert.IsType(result.Content[0]); + Assert.Equal("dynamic result", ((TextContentBlock)result.Content[0]).Text); + } + + [Fact] + public async Task EmptyPromptCollection_CanGetPromptAfterAddingDynamically() + { + var client = await CreateMcpClientForServer(); + + // Add a prompt dynamically + _promptCollection.Add(McpServerPrompt.Create( + () => new ChatMessage(ChatRole.User, "dynamic prompt content"), + new() { Name = "dynamic_prompt", Description = "A dynamic prompt" })); + + // Get the prompt + var result = await client.GetPromptAsync("dynamic_prompt", cancellationToken: TestContext.Current.CancellationToken); + Assert.NotNull(result); + Assert.Single(result.Messages); + Assert.Equal(Role.User, result.Messages[0].Role); + Assert.IsType(result.Messages[0].Content); + Assert.Equal("dynamic prompt content", ((TextContentBlock)result.Messages[0].Content).Text); + } +} + +/// +/// Tests to verify that handlers are NOT synthesized when collections are null. +/// This ensures we don't unnecessarily create capabilities when nothing is configured. +/// +public class NullCollectionTests : ClientServerTestBase +{ + public NullCollectionTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) { } + + [Fact] + public async Task ListFails() + { + Assert.Null(Server.ServerOptions.Capabilities?.Resources); + Assert.Null(Server.ServerOptions.Capabilities?.Tools); + Assert.Null(Server.ServerOptions.Capabilities?.Prompts); + + var client = await CreateMcpClientForServer(); + + await Assert.ThrowsAsync(async () => await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken)); + await Assert.ThrowsAsync(async () => await client.ListPromptsAsync(TestContext.Current.CancellationToken)); + await Assert.ThrowsAsync(async () => await client.ListResourcesAsync(TestContext.Current.CancellationToken)); + } +}