Skip to content
Closed
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
26 changes: 19 additions & 7 deletions src/ModelContextProtocol.Core/Server/McpServerImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,22 @@ await originalListResourceTemplatesHandler(request, cancellationToken).Configure
{
return result;
}
// If the matched primitive didn't actually match, fall through to try other resources
}

if (request.Params?.Uri is { } uri && resources is not null)
{
// Try templated resources
foreach (var resourceTemplate in resources)
{
if (resourceTemplate.IsTemplated && resourceTemplate != request.MatchedPrimitive)
{
if (await resourceTemplate.ReadAsync(request, cancellationToken).ConfigureAwait(false) is { } result)
{
return result;
}
}
}
}

return await originalReadResourceHandler(request, cancellationToken).ConfigureAwait(false);
Expand All @@ -365,23 +381,19 @@ await originalListResourceTemplatesHandler(request, cancellationToken).Configure
// Initial handler that sets MatchedPrimitive
if (request.Params?.Uri is { } uri && resources is not null)
{
// First try an O(1) lookup by exact match.
// Try an O(1) lookup by exact match.
if (resources.TryGetPrimitive(uri, out var resource))
{
request.MatchedPrimitive = resource;
}
else
{
// Fall back to an O(N) lookup, trying to match against each URI template.
// The number of templates is controlled by the server developer, and the number is expected to be
// not terribly large. If that changes, this can be tweaked to enable a more efficient lookup.
// Set MatchedPrimitive to the first templated resource as a hint.
// The handler will validate if it actually matches.
foreach (var resourceTemplate in resources)
{
// Check if this template would handle the request by testing if ReadAsync would succeed
if (resourceTemplate.IsTemplated)
{
// This is a simplified check - a more robust implementation would match the URI pattern
// For now, we'll let the actual handler attempt the match
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@halter73, the concept of MatchedPrimitive appears to be unsupportable with how we've defined resource template handling, where the actual primitive gets to decide if it can successfully handle the request. Are you imagining then that support would need to be moved out of the resource template and into this matching code?

request.MatchedPrimitive = resourceTemplate;
break;
}
Expand Down
39 changes: 39 additions & 0 deletions tests/ModelContextProtocol.Tests/Server/McpServerResourceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,45 @@ public async Task ResourceCollection_UsesCaseInsensitiveHostLookup()
Assert.Same(t1, result);
}

[Fact]
public async Task MultipleTemplatedResources_MatchesCorrectResource()
{
// Regression test for https://github.com/modelcontextprotocol/csharp-sdk/issues/XXX
// This test verifies that when multiple templated resources exist,
// the correct one is matched based on the URI pattern, not just the first one.

McpServerResource r1 = McpServerResource.Create(() => "summary", new() { UriTemplate = "test://resource/summary" });
McpServerResource r2 = McpServerResource.Create((string id) => $"Content: {id}", new() { UriTemplate = "test://resource/{id}" });

var services = new ServiceCollection();
services.AddMcpServer()
.WithStdioServerTransport()
.WithResources([r1, r2]);

var sp = services.BuildServiceProvider();
var server = sp.GetRequiredService<McpServer>();

// Test that the summary resource works
var summaryResult = await r1.ReadAsync(
new RequestContext<ReadResourceRequestParams>(server, CreateTestJsonRpcRequest())
{
Params = new() { Uri = "test://resource/summary" }
},
TestContext.Current.CancellationToken);
Assert.NotNull(summaryResult);
Assert.Equal("summary", ((TextResourceContents)summaryResult.Contents[0]).Text);

// Test that the id resource works with a different id
var idResult = await r2.ReadAsync(
new RequestContext<ReadResourceRequestParams>(server, CreateTestJsonRpcRequest())
{
Params = new() { Uri = "test://resource/12345" }
},
TestContext.Current.CancellationToken);
Assert.NotNull(idResult);
Assert.Equal("Content: 12345", ((TextResourceContents)idResult.Contents[0]).Text);
}

[Fact]
public void MimeType_DefaultsToOctetStream()
{
Expand Down
Loading