diff --git a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs index f827fd31..ec9ad949 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs @@ -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); @@ -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 request.MatchedPrimitive = resourceTemplate; break; } diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerResourceTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerResourceTests.cs index 135633b8..4171650b 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerResourceTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerResourceTests.cs @@ -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(); + + // Test that the summary resource works + var summaryResult = await r1.ReadAsync( + new RequestContext(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(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() {