From 3e2b5ab6cfe0e02fae5f2385d355699d52f20b73 Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Fri, 17 Oct 2025 12:20:48 -0700 Subject: [PATCH 1/4] Add McpServerResource.CanReadUri --- .../Server/AIFunctionMcpServerResource.cs | 37 +++++++++++------- .../Server/DelegatingMcpServerResource.cs | 5 ++- .../Server/McpServerImpl.cs | 14 ++----- .../Server/McpServerResource.cs | 18 +++++++-- .../McpServerResourceRoutingTests.cs | 39 +++++++++++++++++++ .../Server/McpServerResourceTests.cs | 9 +++-- 6 files changed, 88 insertions(+), 34 deletions(-) create mode 100644 tests/ModelContextProtocol.Tests/Configuration/McpServerResourceRoutingTests.cs diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs index 91dc20421..664d249eb 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs @@ -314,7 +314,27 @@ private AIFunctionMcpServerResource(AIFunction function, ResourceTemplate resour public override IReadOnlyList Metadata => _metadata; /// - public override async ValueTask ReadAsync( + public override bool CanReadUri(string uri) + { + Throw.IfNull(uri); + return TryMatch(uri, out _); + } + + private bool TryMatch(string uri, out Match? match) + { + if (_uriParser is null) + { + // This resource is not templated. + match = null; + return UriTemplate.UriTemplateComparer.Instance.Equals(uri, ProtocolResourceTemplate.UriTemplate); + } + + match = _uriParser.Match(uri); + return match.Success; + } + + /// + public override async ValueTask ReadAsync( RequestContext request, CancellationToken cancellationToken = default) { Throw.IfNull(request); @@ -323,20 +343,9 @@ private AIFunctionMcpServerResource(AIFunction function, ResourceTemplate resour cancellationToken.ThrowIfCancellationRequested(); - // Check to see if this URI template matches the request URI. If it doesn't, return null. - // For templates, use the Regex to parse. For static resources, we can just compare the URIs. - Match? match = null; - if (_uriParser is not null) - { - match = _uriParser.Match(request.Params.Uri); - if (!match.Success) - { - return null; - } - } - else if (!UriTemplate.UriTemplateComparer.Instance.Equals(request.Params.Uri, ProtocolResource!.Uri)) + if (!TryMatch(request.Params.Uri, out Match? match)) { - return null; + throw new InvalidOperationException($"Resource '{ProtocolResourceTemplate.UriTemplate}' does not match the provided URI '{request.Params.Uri}'."); } // Build up the arguments for the AIFunction call, including all of the name/value pairs from the URI. diff --git a/src/ModelContextProtocol.Core/Server/DelegatingMcpServerResource.cs b/src/ModelContextProtocol.Core/Server/DelegatingMcpServerResource.cs index ee321037c..4b491a8f9 100644 --- a/src/ModelContextProtocol.Core/Server/DelegatingMcpServerResource.cs +++ b/src/ModelContextProtocol.Core/Server/DelegatingMcpServerResource.cs @@ -26,7 +26,10 @@ protected DelegatingMcpServerResource(McpServerResource innerResource) public override ResourceTemplate ProtocolResourceTemplate => _innerResource.ProtocolResourceTemplate; /// - public override ValueTask ReadAsync(RequestContext request, CancellationToken cancellationToken = default) => + public override bool CanReadUri(string uri) => _innerResource.CanReadUri(uri); + + /// + public override ValueTask ReadAsync(RequestContext request, CancellationToken cancellationToken = default) => _innerResource.ReadAsync(request, cancellationToken); /// diff --git a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs index f827fd31c..31078a6ba 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs @@ -342,10 +342,7 @@ await originalListResourceTemplatesHandler(request, cancellationToken).Configure { if (request.MatchedPrimitive is McpServerResource matchedResource) { - if (await matchedResource.ReadAsync(request, cancellationToken).ConfigureAwait(false) is { } result) - { - return result; - } + return await matchedResource.ReadAsync(request, cancellationToken).ConfigureAwait(false); } return await originalReadResourceHandler(request, cancellationToken).ConfigureAwait(false); @@ -366,22 +363,17 @@ await originalListResourceTemplatesHandler(request, cancellationToken).Configure if (request.Params?.Uri is { } uri && resources is not null) { // First try an O(1) lookup by exact match. - if (resources.TryGetPrimitive(uri, out var resource)) + if (resources.TryGetPrimitive(uri, out var resource) && !resource.IsTemplated) { 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. foreach (var resourceTemplate in resources) { - // Check if this template would handle the request by testing if ReadAsync would succeed - if (resourceTemplate.IsTemplated) + if (resourceTemplate.CanReadUri(uri)) { - // 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/src/ModelContextProtocol.Core/Server/McpServerResource.cs b/src/ModelContextProtocol.Core/Server/McpServerResource.cs index 00d89c774..8296e7bce 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerResource.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerResource.cs @@ -162,6 +162,14 @@ protected McpServerResource() /// public abstract IReadOnlyList Metadata { get; } + /// + /// Determines whether this resource can read the specified if passed to via + /// . + /// + /// The URI being evaluated for this resource. + /// if the resource is able to handle the URI; otherwise, . + public abstract bool CanReadUri(string uri); + /// /// Gets the resource, rendering it with the provided request parameters and returning the resource result. /// @@ -174,12 +182,14 @@ protected McpServerResource() /// /// /// A representing the asynchronous operation, containing a with - /// the resource content and messages. If and only if this doesn't match the , - /// the method returns . + /// the resource content and messages. /// /// is . - /// The resource implementation returned or an unsupported result type. - public abstract ValueTask ReadAsync( + /// + /// The requested resource URI did not match the template for this resource, the resource implementation returned , or + /// the resource implementation returned an unsupported result type. + /// + public abstract ValueTask ReadAsync( RequestContext request, CancellationToken cancellationToken = default); diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerResourceRoutingTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerResourceRoutingTests.cs new file mode 100644 index 000000000..1c82fcae5 --- /dev/null +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerResourceRoutingTests.cs @@ -0,0 +1,39 @@ +using Microsoft.Extensions.DependencyInjection; +using ModelContextProtocol.Client; +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; + +namespace ModelContextProtocol.Tests.Configuration; + +public sealed class McpServerResourceRoutingTests(ITestOutputHelper testOutputHelper) : ClientServerTestBase(testOutputHelper) +{ + protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder) + { + mcpServerBuilder.WithResources([ + McpServerResource.Create(options: new() { UriTemplate = "test://resource/non-templated" } , method: () => "static"), + McpServerResource.Create(options: new() { UriTemplate = "test://resource/{id}" }, method: (string id) => $"template: {id}"), + McpServerResource.Create(options: new() { UriTemplate = "test://params{?a1,a2,a3}" }, method: (string a1, string a2, string a3) => $"params: {a1}, {a2}, {a3}"), + ]); + } + + [Fact] + public async Task MultipleTemplatedResources_MatchesCorrectResource() + { + // Verify that when multiple templated resources exist, the correct one is matched based on the URI pattern, not just the first one. + // Regression test for https://github.com/modelcontextprotocol/csharp-sdk/issues/821. + await using McpClient client = await CreateMcpClientForServer(); + + var nonTemplatedResult = await client.ReadResourceAsync("test://resource/non-templated", TestContext.Current.CancellationToken); + Assert.Equal("static", ((TextResourceContents)nonTemplatedResult.Contents[0]).Text); + + var templatedResult = await client.ReadResourceAsync("test://resource/12345", TestContext.Current.CancellationToken); + Assert.Equal("template: 12345", ((TextResourceContents)templatedResult.Contents[0]).Text); + + var paramsResult = await client.ReadResourceAsync("test://params?a1=a&a2=b&a3=c", TestContext.Current.CancellationToken); + Assert.Equal("params: a, b, c", ((TextResourceContents)paramsResult.Contents[0]).Text); + + var mcpEx = await Assert.ThrowsAsync(async () => await client.ReadResourceAsync("test://params{?a1,a2,a3}", TestContext.Current.CancellationToken)); + Assert.Equal(McpErrorCode.InvalidParams, mcpEx.ErrorCode); + Assert.Equal("Request failed (remote): Unknown resource URI: 'test://params{?a1,a2,a3}'", mcpEx.Message); + } +} diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerResourceTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerResourceTests.cs index 135633b82..abeb39c3a 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerResourceTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerResourceTests.cs @@ -142,7 +142,7 @@ public async Task UriTemplate_CreatedFromParameters_LotsOfTypesSupported() const string Name = "Hello"; McpServerResource t; - ReadResourceResult? result; + ReadResourceResult result; McpServer server = new Mock().Object; t = McpServerResource.Create(() => "42", new() { Name = Name }); @@ -282,11 +282,12 @@ public async Task UriTemplate_CreatedFromParameters_LotsOfTypesSupported() [InlineData("resource://mcp/Hello?arg1=42&arg2=84")] [InlineData("resource://mcp/Hello?arg1=42&arg2=84&arg3=123")] [InlineData("resource://mcp/Hello#fragment")] - public async Task UriTemplate_NonMatchingUri_ReturnsNull(string uri) + public async Task UriTemplate_NonMatchingUri_DoesNotMatch(string uri) { McpServerResource t = McpServerResource.Create((string arg1) => arg1, new() { Name = "Hello" }); Assert.Equal("resource://mcp/Hello{?arg1}", t.ProtocolResourceTemplate.UriTemplate); - Assert.Null(await t.ReadAsync( + Assert.False(t.CanReadUri(uri)); + await Assert.ThrowsAsync(async () => await t.ReadAsync( new RequestContext(new Mock().Object, CreateTestJsonRpcRequest()) { Params = new() { Uri = uri } }, TestContext.Current.CancellationToken)); } @@ -337,7 +338,7 @@ public async Task UriTemplate_MissingOptionalParameter_Succeeds() McpServerResource t = McpServerResource.Create((string? arg1 = null, int? arg2 = null) => arg1 + arg2, new() { Name = "Hello" }); Assert.Equal("resource://mcp/Hello{?arg1,arg2}", t.ProtocolResourceTemplate.UriTemplate); - ReadResourceResult? result; + ReadResourceResult result; result = await t.ReadAsync( new RequestContext(new Mock().Object, CreateTestJsonRpcRequest()) { Params = new() { Uri = "resource://mcp/Hello" } }, From 83234513c05e046f52ceeba25d672dcbec79eb60 Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Fri, 17 Oct 2025 14:59:12 -0700 Subject: [PATCH 2/4] Improve comments --- .../Server/AIFunctionMcpServerResource.cs | 1 + .../Server/McpServerResource.cs | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs index 664d249eb..c0edb9d6a 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs @@ -322,6 +322,7 @@ public override bool CanReadUri(string uri) private bool TryMatch(string uri, out Match? match) { + // For templates, use the Regex to parse. For static resources, we can just compare the URIs. if (_uriParser is null) { // This resource is not templated. diff --git a/src/ModelContextProtocol.Core/Server/McpServerResource.cs b/src/ModelContextProtocol.Core/Server/McpServerResource.cs index 8296e7bce..cceccc027 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerResource.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerResource.cs @@ -163,11 +163,11 @@ protected McpServerResource() public abstract IReadOnlyList Metadata { get; } /// - /// Determines whether this resource can read the specified if passed to via - /// . + /// Evaluates whether the matches the + /// and can be used as the passed to . /// /// The URI being evaluated for this resource. - /// if the resource is able to handle the URI; otherwise, . + /// if the matches the ; otherwise, . public abstract bool CanReadUri(string uri); /// @@ -186,8 +186,8 @@ protected McpServerResource() /// /// is . /// - /// The requested resource URI did not match the template for this resource, the resource implementation returned , or - /// the resource implementation returned an unsupported result type. + /// The did not match the for this resource, + /// the resource implementation returned , or the resource implementation returned an unsupported result type. /// public abstract ValueTask ReadAsync( RequestContext request, From 0b8790e0b296200829c4419c8d3d786273c46a7e Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Mon, 20 Oct 2025 10:46:06 -0700 Subject: [PATCH 3/4] Rename CanReadUri to IsMatch - Improve comments --- .../HttpServerTransportOptions.cs | 7 ++++--- src/ModelContextProtocol.Core/McpException.cs | 4 ++-- .../Server/AIFunctionMcpServerResource.cs | 2 +- .../Server/DelegatingMcpServerResource.cs | 2 +- .../Server/McpServerImpl.cs | 2 +- .../Server/McpServerResource.cs | 6 ++++-- .../Server/McpServerResourceTests.cs | 2 +- .../Server/McpServerTests.cs | 14 +++++++------- 8 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/ModelContextProtocol.AspNetCore/HttpServerTransportOptions.cs b/src/ModelContextProtocol.AspNetCore/HttpServerTransportOptions.cs index aa7659dc1..3fe08a01b 100644 --- a/src/ModelContextProtocol.AspNetCore/HttpServerTransportOptions.cs +++ b/src/ModelContextProtocol.AspNetCore/HttpServerTransportOptions.cs @@ -23,11 +23,12 @@ public class HttpServerTransportOptions public Func? RunSessionHandler { get; set; } /// - /// Gets or sets whether the server should run in a stateless mode which allows for load balancing without session affinity. + /// Gets or sets whether the server should run in a stateless mode which does not track state between requests + /// allowing for load balancing without session affinity. /// /// - /// If , is called once for every request for each request, - /// the "/sse" endpoint will be disabled, and the "MCP-Session-Id" header will not be used. + /// If , will be null, and the "MCP-Session-Id" header will not be used, + /// the will be called once for for each request, and the "/sse" endpoint will be disabled. /// Unsolicited server-to-client messages and all server-to-client requests are also unsupported, because any responses /// may arrive at another ASP.NET Core application process. /// Client sampling and roots capabilities are also disabled in stateless mode, because the server cannot make requests. diff --git a/src/ModelContextProtocol.Core/McpException.cs b/src/ModelContextProtocol.Core/McpException.cs index 6b2342ea1..71b1bc3da 100644 --- a/src/ModelContextProtocol.Core/McpException.cs +++ b/src/ModelContextProtocol.Core/McpException.cs @@ -12,8 +12,8 @@ namespace ModelContextProtocol; /// /// This exception type can be thrown by MCP tools or tool call filters to propagate detailed error messages /// from when a tool execution fails via a . -/// For non-tool calls, this exception controls the message propogated via a . -/// +/// For non-tool calls, this exception controls the message propagated via a . +/// /// is a derived type that can be used to also specify the /// that should be used for the resulting . /// diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs index c0edb9d6a..288a864fc 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs @@ -314,7 +314,7 @@ private AIFunctionMcpServerResource(AIFunction function, ResourceTemplate resour public override IReadOnlyList Metadata => _metadata; /// - public override bool CanReadUri(string uri) + public override bool IsMatch(string uri) { Throw.IfNull(uri); return TryMatch(uri, out _); diff --git a/src/ModelContextProtocol.Core/Server/DelegatingMcpServerResource.cs b/src/ModelContextProtocol.Core/Server/DelegatingMcpServerResource.cs index 4b491a8f9..56759b0ec 100644 --- a/src/ModelContextProtocol.Core/Server/DelegatingMcpServerResource.cs +++ b/src/ModelContextProtocol.Core/Server/DelegatingMcpServerResource.cs @@ -26,7 +26,7 @@ protected DelegatingMcpServerResource(McpServerResource innerResource) public override ResourceTemplate ProtocolResourceTemplate => _innerResource.ProtocolResourceTemplate; /// - public override bool CanReadUri(string uri) => _innerResource.CanReadUri(uri); + public override bool IsMatch(string uri) => _innerResource.IsMatch(uri); /// public override ValueTask ReadAsync(RequestContext request, CancellationToken cancellationToken = default) => diff --git a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs index 31078a6ba..41408c22b 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs @@ -372,7 +372,7 @@ await originalListResourceTemplatesHandler(request, cancellationToken).Configure // Fall back to an O(N) lookup, trying to match against each URI template. foreach (var resourceTemplate in resources) { - if (resourceTemplate.CanReadUri(uri)) + if (resourceTemplate.IsMatch(uri)) { request.MatchedPrimitive = resourceTemplate; break; diff --git a/src/ModelContextProtocol.Core/Server/McpServerResource.cs b/src/ModelContextProtocol.Core/Server/McpServerResource.cs index cceccc027..991c90779 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerResource.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerResource.cs @@ -167,8 +167,10 @@ protected McpServerResource() /// and can be used as the passed to . /// /// The URI being evaluated for this resource. - /// if the matches the ; otherwise, . - public abstract bool CanReadUri(string uri); + /// + /// if the matches the ; otherwise, . + /// + public abstract bool IsMatch(string uri); /// /// Gets the resource, rendering it with the provided request parameters and returning the resource result. diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerResourceTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerResourceTests.cs index abeb39c3a..1f4f26326 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerResourceTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerResourceTests.cs @@ -286,7 +286,7 @@ public async Task UriTemplate_NonMatchingUri_DoesNotMatch(string uri) { McpServerResource t = McpServerResource.Create((string arg1) => arg1, new() { Name = "Hello" }); Assert.Equal("resource://mcp/Hello{?arg1}", t.ProtocolResourceTemplate.UriTemplate); - Assert.False(t.CanReadUri(uri)); + Assert.False(t.IsMatch(uri)); await Assert.ThrowsAsync(async () => await t.ReadAsync( new RequestContext(new Mock().Object, CreateTestJsonRpcRequest()) { Params = new() { Uri = uri } }, TestContext.Current.CancellationToken)); diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs index 4352570e7..358f965ed 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs @@ -369,7 +369,7 @@ await Can_Handle_Requests( new ServerCapabilities { Resources = new() - }, + }, method: RequestMethods.ResourcesRead, configureOptions: options => { @@ -438,7 +438,7 @@ public async Task Can_Handle_List_Prompts_Requests_Throws_Exception_If_No_Handle public async Task Can_Handle_Get_Prompts_Requests() { await Can_Handle_Requests( - new ServerCapabilities + new ServerCapabilities { Prompts = new() }, @@ -466,7 +466,7 @@ public async Task Can_Handle_Get_Prompts_Requests_Throws_Exception_If_No_Handler public async Task Can_Handle_List_Tools_Requests() { await Can_Handle_Requests( - new ServerCapabilities + new ServerCapabilities { Tools = new() }, @@ -504,7 +504,7 @@ await Can_Handle_Requests( new ServerCapabilities { Tools = new() - }, + }, method: RequestMethods.ToolsCall, configureOptions: options => { @@ -626,7 +626,7 @@ await transport.SendMessageAsync( TestContext.Current.CancellationToken ); - var error = await receivedMessage.Task.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken); + var error = await receivedMessage.Task.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken); Assert.NotNull(error); Assert.NotNull(error.Error); Assert.Equal((int)errorCode, error.Error.Code); @@ -662,7 +662,7 @@ await transport.SendMessageAsync( } ); - var response = await receivedMessage.Task.WaitAsync(TimeSpan.FromSeconds(5)); + var response = await receivedMessage.Task.WaitAsync(TimeSpan.FromSeconds(10)); Assert.NotNull(response); assertResult(server, response.Result); @@ -779,7 +779,7 @@ public override Task SendRequestAsync(JsonRpcRequest request, C }; return Task.FromResult(new JsonRpcResponse - { + { Id = new RequestId("0"), Result = JsonSerializer.SerializeToNode(result, McpJsonUtilities.DefaultOptions), }); From 8168b4db4a4345e5aeab4e9520de56c79e33f757 Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Mon, 20 Oct 2025 10:55:26 -0700 Subject: [PATCH 4/4] Test resource matching template exactly --- .../Configuration/McpServerResourceRoutingTests.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerResourceRoutingTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerResourceRoutingTests.cs index 1c82fcae5..63eb9fb52 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerResourceRoutingTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerResourceRoutingTests.cs @@ -29,6 +29,9 @@ public async Task MultipleTemplatedResources_MatchesCorrectResource() var templatedResult = await client.ReadResourceAsync("test://resource/12345", TestContext.Current.CancellationToken); Assert.Equal("template: 12345", ((TextResourceContents)templatedResult.Contents[0]).Text); + var exactTemplatedResult = await client.ReadResourceAsync("test://resource/{id}", TestContext.Current.CancellationToken); + Assert.Equal("template: {id}", ((TextResourceContents)exactTemplatedResult.Contents[0]).Text); + var paramsResult = await client.ReadResourceAsync("test://params?a1=a&a2=b&a3=c", TestContext.Current.CancellationToken); Assert.Equal("params: a, b, c", ((TextResourceContents)paramsResult.Contents[0]).Text);