Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/ModelContextProtocol.Core/Server/McpServerImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down Expand Up @@ -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) =>
Expand Down Expand Up @@ -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) =>
Expand Down
4 changes: 1 addition & 3 deletions tests/Common/Utils/MockHttpHandler.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using System.Net.Http;

namespace ModelContextProtocol.Tests.Utils;
namespace ModelContextProtocol.Tests.Utils;

public class MockHttpHandler : HttpMessageHandler
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,17 @@
<PackageReference Include="Moq" />
<PackageReference Include="OpenTelemetry" />
<PackageReference Include="OpenTelemetry.Exporter.InMemory" />
<PackageReference Include="System.Linq.AsyncEnumerable" />
<PackageReference Include="xunit.v3" />
<PackageReference Include="xunit.runner.visualstudio">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net10.0'))">
<PackageReference Include="System.Linq.AsyncEnumerable" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\samples\TestServerWithHosting\TestServerWithHosting.csproj" />
<ProjectReference Include="..\..\src\ModelContextProtocol.Core\ModelContextProtocol.Core.csproj" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
<PackageReference Include="Serilog.Extensions.Logging" />
<PackageReference Include="Serilog.Sinks.File" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@
<PackageReference Include="OpenTelemetry" />
<PackageReference Include="OpenTelemetry.Exporter.InMemory" />
<PackageReference Include="Serilog" />
<PackageReference Include="System.Linq.AsyncEnumerable" />
<PackageReference Include="JsonSchema.Net" />
<PackageReference Include="xunit.v3" />
<PackageReference Include="xunit.runner.visualstudio">
Expand All @@ -61,6 +60,10 @@
</PackageReference>
</ItemGroup>

<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net10.0'))">
<PackageReference Include="System.Linq.AsyncEnumerable" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\ModelContextProtocol.Core\ModelContextProtocol.Core.csproj" />
<ProjectReference Include="..\ModelContextProtocol.TestServer\ModelContextProtocol.TestServer.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Microsoft.Extensions.DependencyInjection;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
165 changes: 165 additions & 0 deletions tests/ModelContextProtocol.Tests/Server/EmptyCollectionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;

namespace ModelContextProtocol.Tests.Server;

/// <summary>
/// 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.
/// </summary>
public class EmptyCollectionTests : ClientServerTestBase
{
public EmptyCollectionTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) { }

private McpServerResourceCollection _resourceCollection = [];
private McpServerPrimitiveCollection<McpServerTool> _toolCollection = [];
private McpServerPrimitiveCollection<McpServerPrompt> _promptCollection = [];

protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder) =>
mcpServerBuilder.Services.Configure<McpServerOptions>(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<TextResourceContents>(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<TextContentBlock>(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<TextContentBlock>(result.Messages[0].Content);
Assert.Equal("dynamic prompt content", ((TextContentBlock)result.Messages[0].Content).Text);
}
}

/// <summary>
/// Tests to verify that handlers are NOT synthesized when collections are null.
/// This ensures we don't unnecessarily create capabilities when nothing is configured.
/// </summary>
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<McpProtocolException>(async () => await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken));
await Assert.ThrowsAsync<McpProtocolException>(async () => await client.ListPromptsAsync(TestContext.Current.CancellationToken));
await Assert.ThrowsAsync<McpProtocolException>(async () => await client.ListResourcesAsync(TestContext.Current.CancellationToken));
}
}