Skip to content

Commit 93eec5c

Browse files
Copilotstephentoub
andcommitted
Reproduce PR #1370 changes: McpClientPrompt/Resource/ResourceTemplate RequestOptions + tests
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
1 parent 1bf6eb0 commit 93eec5c

6 files changed

Lines changed: 115 additions & 12 deletions

File tree

src/ModelContextProtocol.Core/Client/McpClientPrompt.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using ModelContextProtocol.Protocol;
2-
using System.Text.Json;
32

43
namespace ModelContextProtocol.Client;
54

@@ -74,7 +73,7 @@ public McpClientPrompt(McpClient client, Prompt prompt)
7473
/// Gets this prompt's content by sending a request to the server with optional arguments.
7574
/// </summary>
7675
/// <param name="arguments">Optional arguments to pass to the prompt. Keys are parameter names, and values are the argument values.</param>
77-
/// <param name="serializerOptions">The serialization options governing argument serialization.</param>
76+
/// <param name="options">Optional request options including metadata, serialization settings, and progress tracking.</param>
7877
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
7978
/// <returns>A <see cref="ValueTask"/> containing the prompt's result with content and messages.</returns>
8079
/// <exception cref="McpException">The request failed or the server returned an error response.</exception>
@@ -91,13 +90,13 @@ public McpClientPrompt(McpClient client, Prompt prompt)
9190
/// </remarks>
9291
public async ValueTask<GetPromptResult> GetAsync(
9392
IEnumerable<KeyValuePair<string, object?>>? arguments = null,
94-
JsonSerializerOptions? serializerOptions = null,
93+
RequestOptions? options = null,
9594
CancellationToken cancellationToken = default)
9695
{
9796
IReadOnlyDictionary<string, object?>? argDict =
9897
arguments as IReadOnlyDictionary<string, object?> ??
9998
arguments?.ToDictionary();
10099

101-
return await _client.GetPromptAsync(ProtocolPrompt.Name, argDict, new RequestOptions() { JsonSerializerOptions = serializerOptions }, cancellationToken).ConfigureAwait(false);
100+
return await _client.GetPromptAsync(ProtocolPrompt.Name, argDict, options, cancellationToken).ConfigureAwait(false);
102101
}
103102
}

src/ModelContextProtocol.Core/Client/McpClientResource.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ public McpClientResource(McpClient client, Resource resource)
7474
/// <summary>
7575
/// Gets this resource's content by sending a request to the server.
7676
/// </summary>
77+
/// <param name="options">Optional request options including metadata, serialization settings, and progress tracking.</param>
7778
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
7879
/// <returns>A <see cref="ValueTask{ReadResourceResult}"/> containing the resource's result with content and messages.</returns>
7980
/// <exception cref="McpException">The request failed or the server returned an error response.</exception>
@@ -83,6 +84,7 @@ public McpClientResource(McpClient client, Resource resource)
8384
/// </para>
8485
/// </remarks>
8586
public ValueTask<ReadResourceResult> ReadAsync(
87+
RequestOptions? options = null,
8688
CancellationToken cancellationToken = default) =>
87-
_client.ReadResourceAsync(Uri, cancellationToken: cancellationToken);
89+
_client.ReadResourceAsync(Uri, options, cancellationToken);
8890
}

src/ModelContextProtocol.Core/Client/McpClientResourceTemplate.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,13 @@ public McpClientResourceTemplate(McpClient client, ResourceTemplate resourceTemp
7878
/// <param name="arguments">A dictionary of arguments to pass to the tool. Each key represents a parameter name,
7979
/// and its associated value represents the argument value.
8080
/// </param>
81+
/// <param name="options">Optional request options including metadata, serialization settings, and progress tracking.</param>
8182
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
8283
/// <returns>A <see cref="ValueTask{ReadResourceResult}"/> containing the resource template's result with content and messages.</returns>
8384
/// <exception cref="McpException">The request failed or the server returned an error response.</exception>
8485
public ValueTask<ReadResourceResult> ReadAsync(
8586
IReadOnlyDictionary<string, object?> arguments,
87+
RequestOptions? options = null,
8688
CancellationToken cancellationToken = default) =>
87-
_client.ReadResourceAsync(UriTemplate, arguments, cancellationToken: cancellationToken);
89+
_client.ReadResourceAsync(UriTemplate, arguments, options, cancellationToken);
8890
}

tests/ModelContextProtocol.Tests/Client/McpClientPromptTests.cs

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using ModelContextProtocol.Protocol;
55
using ModelContextProtocol.Server;
66
using System.ComponentModel;
7+
using System.Text.Json.Nodes;
78

89
namespace ModelContextProtocol.Tests.Client;
910

@@ -25,6 +26,10 @@ private sealed class GreetingPrompts
2526
[McpServerPrompt, Description("Generates a greeting prompt")]
2627
public static ChatMessage Greeting([Description("The name to greet")] string name) =>
2728
new(ChatRole.User, $"Hello, {name}!");
29+
30+
[McpServerPrompt, Description("Echoes back the metadata it receives")]
31+
public static ChatMessage MetadataEcho(RequestContext<GetPromptRequestParams> context) =>
32+
new(ChatRole.User, context.Params?.Meta?.ToJsonString() ?? "{}");
2833
}
2934

3035
[Fact]
@@ -33,7 +38,7 @@ public async Task Constructor_WithValidParameters_CreatesInstance()
3338
await using McpClient client = await CreateMcpClientForServer();
3439

3540
var prompts = await client.ListPromptsAsync(cancellationToken: TestContext.Current.CancellationToken);
36-
var originalPrompt = prompts.First();
41+
var originalPrompt = prompts.First(p => p.Name == "greeting");
3742
var promptDefinition = originalPrompt.ProtocolPrompt;
3843

3944
var newPrompt = new McpClientPrompt(client, promptDefinition);
@@ -98,7 +103,7 @@ public async Task ReusePromptDefinition_PreservesPromptMetadata()
98103
await using McpClient client = await CreateMcpClientForServer();
99104

100105
var prompts = await client.ListPromptsAsync(cancellationToken: TestContext.Current.CancellationToken);
101-
var originalPrompt = prompts.First();
106+
var originalPrompt = prompts.First(p => p.Name == "greeting");
102107
var promptDefinition = originalPrompt.ProtocolPrompt;
103108

104109
var reusedPrompt = new McpClientPrompt(client, promptDefinition);
@@ -134,4 +139,32 @@ public async Task ManuallyConstructedPrompt_CanBeInvoked()
134139
Assert.NotNull(textContent);
135140
Assert.Equal("Hello, Test!", textContent.Text);
136141
}
142+
143+
[Fact]
144+
public async Task GetAsync_WithRequestOptions_PassesMetaToServer()
145+
{
146+
await using McpClient client = await CreateMcpClientForServer();
147+
148+
var prompts = await client.ListPromptsAsync(cancellationToken: TestContext.Current.CancellationToken);
149+
var prompt = prompts.Single(p => p.Name == "metadata_echo");
150+
151+
RequestOptions requestOptions = new()
152+
{
153+
Meta = new JsonObject
154+
{
155+
["traceId"] = "test-trace-123",
156+
["customKey"] = "customValue"
157+
}
158+
};
159+
160+
var result = await prompt.GetAsync(options: requestOptions, cancellationToken: TestContext.Current.CancellationToken);
161+
162+
Assert.NotNull(result);
163+
var message = result.Messages.First();
164+
var textContent = Assert.IsType<TextContentBlock>(message.Content);
165+
var receivedMetadata = JsonNode.Parse(textContent.Text)?.AsObject();
166+
Assert.NotNull(receivedMetadata);
167+
Assert.Equal("test-trace-123", receivedMetadata["traceId"]?.GetValue<string>());
168+
Assert.Equal("customValue", receivedMetadata["customKey"]?.GetValue<string>());
169+
}
137170
}

tests/ModelContextProtocol.Tests/Client/McpClientResourceTemplateConstructorTests.cs

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using ModelContextProtocol.Protocol;
44
using ModelContextProtocol.Server;
55
using System.ComponentModel;
6+
using System.Text.Json.Nodes;
67

78
namespace ModelContextProtocol.Tests.Client;
89

@@ -23,6 +24,10 @@ private sealed class FileTemplateResources
2324
{
2425
[McpServerResource, Description("A file template")]
2526
public static string FileTemplate([Description("The file path")] string path) => $"Content for {path}";
27+
28+
[McpServerResource, Description("Echoes back the metadata it receives")]
29+
public static string MetadataEcho(RequestContext<ReadResourceRequestParams> context, [Description("An ID")] string id) =>
30+
context.Params?.Meta?.ToJsonString() ?? "{}";
2631
}
2732

2833
[Fact]
@@ -31,7 +36,7 @@ public async Task Constructor_WithValidParameters_CreatesInstance()
3136
await using McpClient client = await CreateMcpClientForServer();
3237

3338
var templates = await client.ListResourceTemplatesAsync(cancellationToken: TestContext.Current.CancellationToken);
34-
var originalTemplate = templates.First();
39+
var originalTemplate = templates.First(t => t.Name == "file_template");
3540
var templateDefinition = originalTemplate.ProtocolResourceTemplate;
3641

3742
var newTemplate = new McpClientResourceTemplate(client, templateDefinition);
@@ -69,7 +74,7 @@ public async Task ReuseResourceTemplateDefinition_PreservesTemplateMetadata()
6974
await using McpClient client = await CreateMcpClientForServer();
7075

7176
var templates = await client.ListResourceTemplatesAsync(cancellationToken: TestContext.Current.CancellationToken);
72-
var originalTemplate = templates.First();
77+
var originalTemplate = templates.First(t => t.Name == "file_template");
7378
var templateDefinition = originalTemplate.ProtocolResourceTemplate;
7479

7580
var reusedTemplate = new McpClientResourceTemplate(client, templateDefinition);
@@ -100,4 +105,34 @@ public async Task ManuallyConstructedResourceTemplate_CreatesValidInstance()
100105
Assert.Equal("A file template", clientTemplate.Description);
101106
Assert.Equal("file:///{path}", clientTemplate.UriTemplate);
102107
}
108+
109+
[Fact]
110+
public async Task ReadAsync_WithRequestOptions_PassesMetaToServer()
111+
{
112+
await using McpClient client = await CreateMcpClientForServer();
113+
114+
var templates = await client.ListResourceTemplatesAsync(cancellationToken: TestContext.Current.CancellationToken);
115+
var template = templates.Single(t => t.Name == "metadata_echo");
116+
117+
RequestOptions requestOptions = new()
118+
{
119+
Meta = new JsonObject
120+
{
121+
["traceId"] = "test-trace-123",
122+
["customKey"] = "customValue"
123+
}
124+
};
125+
126+
var result = await template.ReadAsync(
127+
new Dictionary<string, object?> { ["id"] = "test-id" },
128+
options: requestOptions,
129+
cancellationToken: TestContext.Current.CancellationToken);
130+
131+
Assert.NotNull(result);
132+
var content = Assert.IsType<TextResourceContents>(result.Contents.First());
133+
var receivedMetadata = JsonNode.Parse(content.Text)?.AsObject();
134+
Assert.NotNull(receivedMetadata);
135+
Assert.Equal("test-trace-123", receivedMetadata["traceId"]?.GetValue<string>());
136+
Assert.Equal("customValue", receivedMetadata["customKey"]?.GetValue<string>());
137+
}
103138
}

tests/ModelContextProtocol.Tests/Client/McpClientResourceTests.cs

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using ModelContextProtocol.Protocol;
44
using ModelContextProtocol.Server;
55
using System.ComponentModel;
6+
using System.Text.Json.Nodes;
67

78
namespace ModelContextProtocol.Tests.Client;
89

@@ -23,6 +24,10 @@ private sealed class SampleResources
2324
{
2425
[McpServerResource, Description("A sample resource")]
2526
public static string Sample() => "Sample content";
27+
28+
[McpServerResource, Description("Echoes back the metadata it receives")]
29+
public static string MetadataEcho(RequestContext<ReadResourceRequestParams> context) =>
30+
context.Params?.Meta?.ToJsonString() ?? "{}";
2631
}
2732

2833
[Fact]
@@ -31,7 +36,7 @@ public async Task Constructor_WithValidParameters_CreatesInstance()
3136
await using McpClient client = await CreateMcpClientForServer();
3237

3338
var resources = await client.ListResourcesAsync(cancellationToken: TestContext.Current.CancellationToken);
34-
var originalResource = resources.First();
39+
var originalResource = resources.First(r => r.Name == "sample");
3540
var resourceDefinition = originalResource.ProtocolResource;
3641

3742
var newResource = new McpClientResource(client, resourceDefinition);
@@ -94,7 +99,7 @@ public async Task ReuseResourceDefinition_PreservesResourceMetadata()
9499
await using McpClient client = await CreateMcpClientForServer();
95100

96101
var resources = await client.ListResourcesAsync(cancellationToken: TestContext.Current.CancellationToken);
97-
var originalResource = resources.First();
102+
var originalResource = resources.First(r => r.Name == "sample");
98103
var resourceDefinition = originalResource.ProtocolResource;
99104

100105
var reusedResource = new McpClientResource(client, resourceDefinition);
@@ -125,4 +130,31 @@ public async Task ManuallyConstructedResource_CreatesValidInstance()
125130
Assert.Equal("A sample resource", clientResource.Description);
126131
Assert.Equal("file:///sample.txt", clientResource.Uri);
127132
}
133+
134+
[Fact]
135+
public async Task ReadAsync_WithRequestOptions_PassesMetaToServer()
136+
{
137+
await using McpClient client = await CreateMcpClientForServer();
138+
139+
var resources = await client.ListResourcesAsync(cancellationToken: TestContext.Current.CancellationToken);
140+
var resource = resources.Single(r => r.Name == "metadata_echo");
141+
142+
RequestOptions requestOptions = new()
143+
{
144+
Meta = new JsonObject
145+
{
146+
["traceId"] = "test-trace-123",
147+
["customKey"] = "customValue"
148+
}
149+
};
150+
151+
var result = await resource.ReadAsync(options: requestOptions, cancellationToken: TestContext.Current.CancellationToken);
152+
153+
Assert.NotNull(result);
154+
var content = Assert.IsType<TextResourceContents>(result.Contents.First());
155+
var receivedMetadata = JsonNode.Parse(content.Text)?.AsObject();
156+
Assert.NotNull(receivedMetadata);
157+
Assert.Equal("test-trace-123", receivedMetadata["traceId"]?.GetValue<string>());
158+
Assert.Equal("customValue", receivedMetadata["customKey"]?.GetValue<string>());
159+
}
128160
}

0 commit comments

Comments
 (0)