diff --git a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs index 8e264741b..a4a3d6910 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs @@ -295,7 +295,7 @@ subscribeHandler is null && unsubscribeHandler is null && resources is null && 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) => @@ -456,7 +456,7 @@ private void ConfigurePrompts(McpServerOptions options) 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) => @@ -544,7 +544,7 @@ private void ConfigureTools(McpServerOptions options) 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 d15ec3dc0..5e58a6cd5 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 7adeb03d3..5d21d0a0a 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 3296ff481..e2a1c16c2 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 7d037fb2d..c2e2b9d51 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs @@ -7,7 +7,6 @@ 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 944eec4d5..425944624 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 80c6b1ed9..e21f1f952 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 f3ae33ed5..76f967bed 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 000000000..882fd5045 --- /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)); + } +}