Skip to content

Commit 77cc43f

Browse files
Copilotstephentoub
andcommitted
Fix: Allow handlers for empty tool/prompt/resource collections
Changed condition from `IsEmpty: false` to `not null` check in McpServerImpl.cs for resources (line 298), prompts (line 459), and tools (line 547). This allows handlers to be synthesized even when collections are empty, enabling dynamic addition of items after initialization. Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
1 parent cd6f2a9 commit 77cc43f

File tree

2 files changed

+163
-3
lines changed

2 files changed

+163
-3
lines changed

src/ModelContextProtocol.Core/Server/McpServerImpl.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ subscribeHandler is null && unsubscribeHandler is null && resources is null &&
295295
var subscribe = resourcesCapability?.Subscribe;
296296

297297
// Handle resources provided via DI.
298-
if (resources is { IsEmpty: false })
298+
if (resources is not null)
299299
{
300300
var originalListResourcesHandler = listResourcesHandler;
301301
listResourcesHandler = async (request, cancellationToken) =>
@@ -456,7 +456,7 @@ private void ConfigurePrompts(McpServerOptions options)
456456
var listChanged = promptsCapability?.ListChanged;
457457

458458
// Handle tools provided via DI by augmenting the handlers to incorporate them.
459-
if (prompts is { IsEmpty: false })
459+
if (prompts is not null)
460460
{
461461
var originalListPromptsHandler = listPromptsHandler;
462462
listPromptsHandler = async (request, cancellationToken) =>
@@ -544,7 +544,7 @@ private void ConfigureTools(McpServerOptions options)
544544
var listChanged = toolsCapability?.ListChanged;
545545

546546
// Handle tools provided via DI by augmenting the handlers to incorporate them.
547-
if (tools is { IsEmpty: false })
547+
if (tools is not null)
548548
{
549549
var originalListToolsHandler = listToolsHandler;
550550
listToolsHandler = async (request, cancellationToken) =>
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using ModelContextProtocol.Client;
3+
using ModelContextProtocol.Protocol;
4+
using ModelContextProtocol.Server;
5+
6+
namespace ModelContextProtocol.Tests.Server;
7+
8+
/// <summary>
9+
/// Tests to verify that handlers are synthesized for empty collections that can be populated dynamically.
10+
/// This addresses the issue where handlers were only created when collections had items.
11+
/// </summary>
12+
public class EmptyCollectionTests : ClientServerTestBase
13+
{
14+
public EmptyCollectionTests(ITestOutputHelper testOutputHelper)
15+
: base(testOutputHelper)
16+
{
17+
}
18+
19+
private McpServerResourceCollection? _resourceCollection;
20+
private McpServerPrimitiveCollection<McpServerTool>? _toolCollection;
21+
private McpServerPrimitiveCollection<McpServerPrompt>? _promptCollection;
22+
23+
protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder)
24+
{
25+
// Initialize empty collections
26+
_resourceCollection = [];
27+
_toolCollection = [];
28+
_promptCollection = [];
29+
30+
mcpServerBuilder.Services.Configure<McpServerOptions>(options =>
31+
{
32+
options.ServerInfo = new Implementation
33+
{
34+
Name = "empty-collection-test",
35+
Version = "1.0.0"
36+
};
37+
38+
// Set empty collections - handlers should still be synthesized
39+
options.ResourceCollection = _resourceCollection;
40+
options.ToolCollection = _toolCollection;
41+
options.PromptCollection = _promptCollection;
42+
});
43+
}
44+
45+
[Fact]
46+
public async Task EmptyResourceCollection_CanAddResourcesDynamically()
47+
{
48+
using var client = await CreateMcpClientForServer();
49+
50+
// Initially, the resource collection is empty
51+
var initialResources = await client.ListResourcesAsync();
52+
Assert.Empty(initialResources);
53+
54+
// Add a resource dynamically
55+
_resourceCollection!.Add(McpServerResource.Create(
56+
() => "test content",
57+
new() { UriTemplate = "test://resource/1" }));
58+
59+
// The resource should now be listed
60+
var updatedResources = await client.ListResourcesAsync();
61+
Assert.Single(updatedResources);
62+
Assert.Equal("test://resource/1", updatedResources[0].Uri);
63+
}
64+
65+
[Fact]
66+
public async Task EmptyToolCollection_CanAddToolsDynamically()
67+
{
68+
using var client = await CreateMcpClientForServer();
69+
70+
// Initially, the tool collection is empty
71+
var initialTools = await client.ListToolsAsync();
72+
Assert.Empty(initialTools);
73+
74+
// Add a tool dynamically
75+
_toolCollection!.Add(McpServerTool.Create(
76+
() => "test result",
77+
new() { Name = "test_tool", Description = "A test tool" }));
78+
79+
// The tool should now be listed
80+
var updatedTools = await client.ListToolsAsync();
81+
Assert.Single(updatedTools);
82+
Assert.Equal("test_tool", updatedTools[0].Name);
83+
}
84+
85+
[Fact]
86+
public async Task EmptyPromptCollection_CanAddPromptsDynamically()
87+
{
88+
using var client = await CreateMcpClientForServer();
89+
90+
// Initially, the prompt collection is empty
91+
var initialPrompts = await client.ListPromptsAsync();
92+
Assert.Empty(initialPrompts);
93+
94+
// Add a prompt dynamically
95+
_promptCollection!.Add(McpServerPrompt.Create(
96+
() => new ChatMessage(ChatRole.User, "test prompt"),
97+
new() { Name = "test_prompt", Description = "A test prompt" }));
98+
99+
// The prompt should now be listed
100+
var updatedPrompts = await client.ListPromptsAsync();
101+
Assert.Single(updatedPrompts);
102+
Assert.Equal("test_prompt", updatedPrompts[0].Name);
103+
}
104+
105+
[Fact]
106+
public async Task EmptyResourceCollection_CanCallReadResourceAfterAddingDynamically()
107+
{
108+
using var client = await CreateMcpClientForServer();
109+
110+
// Add a resource dynamically
111+
_resourceCollection!.Add(McpServerResource.Create(
112+
() => "dynamic content",
113+
new() { UriTemplate = "test://resource/dynamic" }));
114+
115+
// Read the resource
116+
var result = await client.ReadResourceAsync("test://resource/dynamic");
117+
Assert.NotNull(result);
118+
Assert.Single(result.Contents);
119+
Assert.IsType<TextContent>(result.Contents[0]);
120+
Assert.Equal("dynamic content", ((TextContent)result.Contents[0]).Text);
121+
}
122+
123+
[Fact]
124+
public async Task EmptyToolCollection_CanCallToolAfterAddingDynamically()
125+
{
126+
using var client = await CreateMcpClientForServer();
127+
128+
// Add a tool dynamically
129+
_toolCollection!.Add(McpServerTool.Create(
130+
() => "dynamic result",
131+
new() { Name = "dynamic_tool", Description = "A dynamic tool" }));
132+
133+
// Call the tool
134+
var result = await client.CallToolAsync("dynamic_tool");
135+
Assert.NotNull(result);
136+
Assert.Single(result.Content);
137+
Assert.IsType<TextContent>(result.Content[0]);
138+
Assert.Equal("dynamic result", ((TextContent)result.Content[0]).Text);
139+
}
140+
141+
[Fact]
142+
public async Task EmptyPromptCollection_CanGetPromptAfterAddingDynamically()
143+
{
144+
using var client = await CreateMcpClientForServer();
145+
146+
// Add a prompt dynamically
147+
_promptCollection!.Add(McpServerPrompt.Create(
148+
() => new ChatMessage(ChatRole.User, "dynamic prompt content"),
149+
new() { Name = "dynamic_prompt", Description = "A dynamic prompt" }));
150+
151+
// Get the prompt
152+
var result = await client.GetPromptAsync("dynamic_prompt");
153+
Assert.NotNull(result);
154+
Assert.Single(result.Messages);
155+
Assert.Equal(ChatRole.User, result.Messages[0].Role);
156+
Assert.Single(result.Messages[0].Content);
157+
Assert.IsType<TextContent>(result.Messages[0].Content[0]);
158+
Assert.Equal("dynamic prompt content", ((TextContent)result.Messages[0].Content[0]).Text);
159+
}
160+
}

0 commit comments

Comments
 (0)