Fix McpClientPrompt/Resource types to use RequestOptions like McpClientTool does (#1370)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
This commit is contained in:
Copilot 2026-02-23 23:40:54 -05:00 • committed by GitHub
commit 8c46411f82
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 115 additions and 12 deletions

View file

@ -1,5 +1,4 @@
using ModelContextProtocol.Protocol; using ModelContextProtocol.Protocol;
using System.Text.Json;
namespace ModelContextProtocol.Client; namespace ModelContextProtocol.Client;
@ -74,7 +73,7 @@ public sealed class McpClientPrompt
/// Gets this prompt's content by sending a request to the server with optional arguments. /// Gets this prompt's content by sending a request to the server with optional arguments.
/// </summary> /// </summary>
/// <param name="arguments">Optional arguments to pass to the prompt. Keys are parameter names, and values are the argument values.</param> /// <param name="arguments">Optional arguments to pass to the prompt. Keys are parameter names, and values are the argument values.</param>
/// <param name="serializerOptions">The serialization options governing argument serialization.</param> /// <param name="options">Optional request options including metadata, serialization settings, and progress tracking.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ValueTask"/> containing the prompt's result with content and messages.</returns> /// <returns>A <see cref="ValueTask"/> containing the prompt's result with content and messages.</returns>
/// <exception cref="McpException">The request failed or the server returned an error response.</exception> /// <exception cref="McpException">The request failed or the server returned an error response.</exception>
@ -91,13 +90,13 @@ public sealed class McpClientPrompt
/// </remarks> /// </remarks>
public async ValueTask<GetPromptResult> GetAsync( public async ValueTask<GetPromptResult> GetAsync(
IEnumerable<KeyValuePair<string, object?>>? arguments = null, IEnumerable<KeyValuePair<string, object?>>? arguments = null,
JsonSerializerOptions? serializerOptions = null, RequestOptions? options = null,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
IReadOnlyDictionary<string, object?>? argDict = IReadOnlyDictionary<string, object?>? argDict =
arguments as IReadOnlyDictionary<string, object?> ?? arguments as IReadOnlyDictionary<string, object?> ??
arguments?.ToDictionary(); arguments?.ToDictionary();
return await _client.GetPromptAsync(ProtocolPrompt.Name, argDict, new RequestOptions() { JsonSerializerOptions = serializerOptions }, cancellationToken).ConfigureAwait(false); return await _client.GetPromptAsync(ProtocolPrompt.Name, argDict, options, cancellationToken).ConfigureAwait(false);
} }
} }

View file

@ -74,6 +74,7 @@ public sealed class McpClientResource
/// <summary> /// <summary>
/// Gets this resource's content by sending a request to the server. /// Gets this resource's content by sending a request to the server.
/// </summary> /// </summary>
/// <param name="options">Optional request options including metadata, serialization settings, and progress tracking.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ValueTask{ReadResourceResult}"/> containing the resource's result with content and messages.</returns> /// <returns>A <see cref="ValueTask{ReadResourceResult}"/> containing the resource's result with content and messages.</returns>
/// <exception cref="McpException">The request failed or the server returned an error response.</exception> /// <exception cref="McpException">The request failed or the server returned an error response.</exception>
@ -83,6 +84,7 @@ public sealed class McpClientResource
/// </para> /// </para>
/// </remarks> /// </remarks>
public ValueTask<ReadResourceResult> ReadAsync( public ValueTask<ReadResourceResult> ReadAsync(
RequestOptions? options = null,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
_client.ReadResourceAsync(Uri, cancellationToken: cancellationToken); _client.ReadResourceAsync(Uri, options, cancellationToken);
} }

View file

@ -78,11 +78,13 @@ public sealed class McpClientResourceTemplate
/// <param name="arguments">A dictionary of arguments to pass to the tool. Each key represents a parameter name, /// <param name="arguments">A dictionary of arguments to pass to the tool. Each key represents a parameter name,
/// and its associated value represents the argument value. /// and its associated value represents the argument value.
/// </param> /// </param>
/// <param name="options">Optional request options including metadata, serialization settings, and progress tracking.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ValueTask{ReadResourceResult}"/> containing the resource template's result with content and messages.</returns> /// <returns>A <see cref="ValueTask{ReadResourceResult}"/> containing the resource template's result with content and messages.</returns>
/// <exception cref="McpException">The request failed or the server returned an error response.</exception> /// <exception cref="McpException">The request failed or the server returned an error response.</exception>
public ValueTask<ReadResourceResult> ReadAsync( public ValueTask<ReadResourceResult> ReadAsync(
IReadOnlyDictionary<string, object?> arguments, IReadOnlyDictionary<string, object?> arguments,
RequestOptions? options = null,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
_client.ReadResourceAsync(UriTemplate, arguments, cancellationToken: cancellationToken); _client.ReadResourceAsync(UriTemplate, arguments, options, cancellationToken);
} }

View file

@ -4,6 +4,7 @@ using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol; using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server; using ModelContextProtocol.Server;
using System.ComponentModel; using System.ComponentModel;
using System.Text.Json.Nodes;
namespace ModelContextProtocol.Tests.Client; namespace ModelContextProtocol.Tests.Client;
@ -25,6 +26,10 @@ public class McpClientPromptTests : ClientServerTestBase
[McpServerPrompt, Description("Generates a greeting prompt")] [McpServerPrompt, Description("Generates a greeting prompt")]
public static ChatMessage Greeting([Description("The name to greet")] string name) => public static ChatMessage Greeting([Description("The name to greet")] string name) =>
new(ChatRole.User, $"Hello, {name}!"); new(ChatRole.User, $"Hello, {name}!");
[McpServerPrompt, Description("Echoes back the metadata it receives")]
public static ChatMessage MetadataEcho(RequestContext<GetPromptRequestParams> context) =>
new(ChatRole.User, context.Params?.Meta?.ToJsonString() ?? "{}");
} }
[Fact] [Fact]
@ -33,7 +38,7 @@ public class McpClientPromptTests : ClientServerTestBase
await using McpClient client = await CreateMcpClientForServer(); await using McpClient client = await CreateMcpClientForServer();
var prompts = await client.ListPromptsAsync(cancellationToken: TestContext.Current.CancellationToken); var prompts = await client.ListPromptsAsync(cancellationToken: TestContext.Current.CancellationToken);
var originalPrompt = prompts.First(); var originalPrompt = prompts.First(p => p.Name == "greeting");
var promptDefinition = originalPrompt.ProtocolPrompt; var promptDefinition = originalPrompt.ProtocolPrompt;
var newPrompt = new McpClientPrompt(client, promptDefinition); var newPrompt = new McpClientPrompt(client, promptDefinition);
@ -98,7 +103,7 @@ public class McpClientPromptTests : ClientServerTestBase
await using McpClient client = await CreateMcpClientForServer(); await using McpClient client = await CreateMcpClientForServer();
var prompts = await client.ListPromptsAsync(cancellationToken: TestContext.Current.CancellationToken); var prompts = await client.ListPromptsAsync(cancellationToken: TestContext.Current.CancellationToken);
var originalPrompt = prompts.First(); var originalPrompt = prompts.First(p => p.Name == "greeting");
var promptDefinition = originalPrompt.ProtocolPrompt; var promptDefinition = originalPrompt.ProtocolPrompt;
var reusedPrompt = new McpClientPrompt(client, promptDefinition); var reusedPrompt = new McpClientPrompt(client, promptDefinition);
@ -134,4 +139,32 @@ public class McpClientPromptTests : ClientServerTestBase
Assert.NotNull(textContent); Assert.NotNull(textContent);
Assert.Equal("Hello, Test!", textContent.Text); Assert.Equal("Hello, Test!", textContent.Text);
} }
[Fact]
public async Task GetAsync_WithRequestOptions_PassesMetaToServer()
{
await using McpClient client = await CreateMcpClientForServer();
var prompts = await client.ListPromptsAsync(cancellationToken: TestContext.Current.CancellationToken);
var prompt = prompts.Single(p => p.Name == "metadata_echo");
RequestOptions requestOptions = new()
{
Meta = new JsonObject
{
["traceId"] = "test-trace-123",
["customKey"] = "customValue"
}
};
var result = await prompt.GetAsync(options: requestOptions, cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(result);
var message = result.Messages.First();
var textContent = Assert.IsType<TextContentBlock>(message.Content);
var receivedMetadata = JsonNode.Parse(textContent.Text)?.AsObject();
Assert.NotNull(receivedMetadata);
Assert.Equal("test-trace-123", receivedMetadata["traceId"]?.GetValue<string>());
Assert.Equal("customValue", receivedMetadata["customKey"]?.GetValue<string>());
}
} }

View file

@ -3,6 +3,7 @@ using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol; using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server; using ModelContextProtocol.Server;
using System.ComponentModel; using System.ComponentModel;
using System.Text.Json.Nodes;
namespace ModelContextProtocol.Tests.Client; namespace ModelContextProtocol.Tests.Client;
@ -23,6 +24,10 @@ public class McpClientResourceTemplateConstructorTests : ClientServerTestBase
{ {
[McpServerResource, Description("A file template")] [McpServerResource, Description("A file template")]
public static string FileTemplate([Description("The file path")] string path) => $"Content for {path}"; public static string FileTemplate([Description("The file path")] string path) => $"Content for {path}";
[McpServerResource, Description("Echoes back the metadata it receives")]
public static string MetadataEcho(RequestContext<ReadResourceRequestParams> context, [Description("An ID")] string id) =>
context.Params?.Meta?.ToJsonString() ?? "{}";
} }
[Fact] [Fact]
@ -31,7 +36,7 @@ public class McpClientResourceTemplateConstructorTests : ClientServerTestBase
await using McpClient client = await CreateMcpClientForServer(); await using McpClient client = await CreateMcpClientForServer();
var templates = await client.ListResourceTemplatesAsync(cancellationToken: TestContext.Current.CancellationToken); var templates = await client.ListResourceTemplatesAsync(cancellationToken: TestContext.Current.CancellationToken);
var originalTemplate = templates.First(); var originalTemplate = templates.First(t => t.Name == "file_template");
var templateDefinition = originalTemplate.ProtocolResourceTemplate; var templateDefinition = originalTemplate.ProtocolResourceTemplate;
var newTemplate = new McpClientResourceTemplate(client, templateDefinition); var newTemplate = new McpClientResourceTemplate(client, templateDefinition);
@ -69,7 +74,7 @@ public class McpClientResourceTemplateConstructorTests : ClientServerTestBase
await using McpClient client = await CreateMcpClientForServer(); await using McpClient client = await CreateMcpClientForServer();
var templates = await client.ListResourceTemplatesAsync(cancellationToken: TestContext.Current.CancellationToken); var templates = await client.ListResourceTemplatesAsync(cancellationToken: TestContext.Current.CancellationToken);
var originalTemplate = templates.First(); var originalTemplate = templates.First(t => t.Name == "file_template");
var templateDefinition = originalTemplate.ProtocolResourceTemplate; var templateDefinition = originalTemplate.ProtocolResourceTemplate;
var reusedTemplate = new McpClientResourceTemplate(client, templateDefinition); var reusedTemplate = new McpClientResourceTemplate(client, templateDefinition);
@ -100,4 +105,34 @@ public class McpClientResourceTemplateConstructorTests : ClientServerTestBase
Assert.Equal("A file template", clientTemplate.Description); Assert.Equal("A file template", clientTemplate.Description);
Assert.Equal("file:///{path}", clientTemplate.UriTemplate); Assert.Equal("file:///{path}", clientTemplate.UriTemplate);
} }
[Fact]
public async Task ReadAsync_WithRequestOptions_PassesMetaToServer()
{
await using McpClient client = await CreateMcpClientForServer();
var templates = await client.ListResourceTemplatesAsync(cancellationToken: TestContext.Current.CancellationToken);
var template = templates.Single(t => t.Name == "metadata_echo");
RequestOptions requestOptions = new()
{
Meta = new JsonObject
{
["traceId"] = "test-trace-123",
["customKey"] = "customValue"
}
};
var result = await template.ReadAsync(
new Dictionary<string, object?> { ["id"] = "test-id" },
options: requestOptions,
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(result);
var content = Assert.IsType<TextResourceContents>(result.Contents.First());
var receivedMetadata = JsonNode.Parse(content.Text)?.AsObject();
Assert.NotNull(receivedMetadata);
Assert.Equal("test-trace-123", receivedMetadata["traceId"]?.GetValue<string>());
Assert.Equal("customValue", receivedMetadata["customKey"]?.GetValue<string>());
}
} }

View file

@ -3,6 +3,7 @@ using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol; using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server; using ModelContextProtocol.Server;
using System.ComponentModel; using System.ComponentModel;
using System.Text.Json.Nodes;
namespace ModelContextProtocol.Tests.Client; namespace ModelContextProtocol.Tests.Client;
@ -23,6 +24,10 @@ public class McpClientResourceTests : ClientServerTestBase
{ {
[McpServerResource, Description("A sample resource")] [McpServerResource, Description("A sample resource")]
public static string Sample() => "Sample content"; public static string Sample() => "Sample content";
[McpServerResource, Description("Echoes back the metadata it receives")]
public static string MetadataEcho(RequestContext<ReadResourceRequestParams> context) =>
context.Params?.Meta?.ToJsonString() ?? "{}";
} }
[Fact] [Fact]
@ -31,7 +36,7 @@ public class McpClientResourceTests : ClientServerTestBase
await using McpClient client = await CreateMcpClientForServer(); await using McpClient client = await CreateMcpClientForServer();
var resources = await client.ListResourcesAsync(cancellationToken: TestContext.Current.CancellationToken); var resources = await client.ListResourcesAsync(cancellationToken: TestContext.Current.CancellationToken);
var originalResource = resources.First(); var originalResource = resources.First(r => r.Name == "sample");
var resourceDefinition = originalResource.ProtocolResource; var resourceDefinition = originalResource.ProtocolResource;
var newResource = new McpClientResource(client, resourceDefinition); var newResource = new McpClientResource(client, resourceDefinition);
@ -94,7 +99,7 @@ public class McpClientResourceTests : ClientServerTestBase
await using McpClient client = await CreateMcpClientForServer(); await using McpClient client = await CreateMcpClientForServer();
var resources = await client.ListResourcesAsync(cancellationToken: TestContext.Current.CancellationToken); var resources = await client.ListResourcesAsync(cancellationToken: TestContext.Current.CancellationToken);
var originalResource = resources.First(); var originalResource = resources.First(r => r.Name == "sample");
var resourceDefinition = originalResource.ProtocolResource; var resourceDefinition = originalResource.ProtocolResource;
var reusedResource = new McpClientResource(client, resourceDefinition); var reusedResource = new McpClientResource(client, resourceDefinition);
@ -125,4 +130,31 @@ public class McpClientResourceTests : ClientServerTestBase
Assert.Equal("A sample resource", clientResource.Description); Assert.Equal("A sample resource", clientResource.Description);
Assert.Equal("file:///sample.txt", clientResource.Uri); Assert.Equal("file:///sample.txt", clientResource.Uri);
} }
[Fact]
public async Task ReadAsync_WithRequestOptions_PassesMetaToServer()
{
await using McpClient client = await CreateMcpClientForServer();
var resources = await client.ListResourcesAsync(cancellationToken: TestContext.Current.CancellationToken);
var resource = resources.Single(r => r.Name == "metadata_echo");
RequestOptions requestOptions = new()
{
Meta = new JsonObject
{
["traceId"] = "test-trace-123",
["customKey"] = "customValue"
}
};
var result = await resource.ReadAsync(options: requestOptions, cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(result);
var content = Assert.IsType<TextResourceContents>(result.Contents.First());
var receivedMetadata = JsonNode.Parse(content.Text)?.AsObject();
Assert.NotNull(receivedMetadata);
Assert.Equal("test-trace-123", receivedMetadata["traceId"]?.GetValue<string>());
Assert.Equal("customValue", receivedMetadata["customKey"]?.GetValue<string>());
}
} }