Add request options bag to high level requests and include Meta (#970)

Co-authored-by: Stephen Toub <stoub@microsoft.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Mike Kistler 2025-12-04 14:32:12 -08:00 • committed by GitHub
commit c447973972
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 882 additions and 164 deletions

View file

@ -0,0 +1,141 @@
using Microsoft.Extensions.DependencyInjection;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
using System.Text.Json.Nodes;
namespace ModelContextProtocol.Tests.Client;
public class McpClientMetaTests : ClientServerTestBase
{
public McpClientMetaTests(ITestOutputHelper outputHelper)
: base(outputHelper)
{
}
protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder)
{
services.Configure<McpServerOptions>(o =>
{
o.ServerInfo = new Implementation
{
Name = "test-server",
Version = "1.0.0",
Description = "A test server for unit testing",
WebsiteUrl = "https://example.com",
};
o.ToolCollection = new ();
o.ResourceCollection = new ();
o.PromptCollection = new ();
});
}
[Fact]
public async Task ToolCallWithMetaFields()
{
Server.ServerOptions.ToolCollection?.Add(McpServerTool.Create(
async (RequestContext<CallToolRequestParams> context) =>
{
// Access the foo property of _meta field from the request parameters
var metaFoo = context.Params?.Meta?["foo"]?.ToString();
// Assert that the meta foo is correctly passed
Assert.NotNull(metaFoo);
return $"Meta foo is {metaFoo}";
},
new () { Name = "meta_tool" }));
await using McpClient client = await CreateMcpClientForServer();
var requestOptions = new RequestOptions()
{
Meta = new JsonObject()
{
{ "foo", "bar baz" }
}
};
var result = await client.CallToolAsync("meta_tool", options: requestOptions, cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.Null(result.IsError);
var textContent = result.Content.OfType<TextContentBlock>().FirstOrDefault();
Assert.NotNull(textContent);
Assert.Contains("bar baz", textContent.Text);
}
[Fact]
public async Task ResourceReadWithMetaFields()
{
Server.ServerOptions.ResourceCollection?.Add(McpServerResource.Create(
(RequestContext<ReadResourceRequestParams> context) =>
{
// Access the foo property of _meta field from the request parameters
var metaFoo = context.Params?.Meta?["foo"]?.ToString();
// Assert that the meta foo is correctly passed
Assert.NotNull(metaFoo);
return $"Resource with Meta foo is {metaFoo}";
},
new () { UriTemplate = "test://meta_resource" }));
await using McpClient client = await CreateMcpClientForServer();
var requestOptions = new RequestOptions()
{
Meta = new JsonObject()
{
{ "foo", "bar baz" }
}
};
var result = await client.ReadResourceAsync("test://meta_resource", options: requestOptions, cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(result);
var textContent = result.Contents.OfType<TextResourceContents>().FirstOrDefault();
Assert.NotNull(textContent);
Assert.Contains("bar baz", textContent.Text);
}
[Fact]
public async Task PromptGetWithMetaFields()
{
Server.ServerOptions.PromptCollection?.Add(McpServerPrompt.Create(
(RequestContext<GetPromptRequestParams> context) =>
{
// Access the foo property of _meta field from the request parameters
var metaFoo = context.Params?.Meta?["foo"]?.ToString();
// Assert that the meta foo is correctly passed
Assert.NotNull(metaFoo);
return $"Prompt with Meta foo is {metaFoo}";
},
new () { Name = "meta_prompt" }));
await using McpClient client = await CreateMcpClientForServer();
var requestOptions = new RequestOptions()
{
Meta = new JsonObject()
{
{ "foo", "bar baz" }
}
};
var result = await client.GetPromptAsync("meta_prompt", options: requestOptions, cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.NotEmpty(result.Messages);
var message = result.Messages.First();
Assert.NotNull(message.Content);
var textContent = message.Content as TextContentBlock;
Assert.NotNull(textContent);
Assert.Contains("bar baz", textContent.Text);
}
}

View file

@ -75,7 +75,7 @@ public partial class McpClientResourceTemplateTests : ClientServerTestBase
{
await using McpClient client = await CreateMcpClientForServer();
var result = await client.ReadResourceAsync(uriTemplate, variables, TestContext.Current.CancellationToken);
var result = await client.ReadResourceAsync(uriTemplate, variables, null, TestContext.Current.CancellationToken);
Assert.NotNull(result);
var actualUri = Assert.IsType<TextResourceContents>(Assert.Single(result.Contents)).Text;
@ -110,13 +110,13 @@ public partial class McpClientResourceTemplateTests : ClientServerTestBase
// The JSON from the test case files has been extracted below.
// Copyright 2011- The Authors
//
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//
// http://www.apache.org/licenses/LICENSE-2.0
//
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -255,7 +255,7 @@ public partial class McpClientResourceTemplateTests : ClientServerTestBase
["X{.var:3}", "X.val"],
["X{.list}", "X.red,green,blue"],
["X{.list*}", "X.red.green.blue"],
["X{.keys}", [
["X{.keys}", [
"X.comma,%2C,dot,.,semi,%3B",
"X.comma,%2C,semi,%3B,dot,.",
"X.dot,.,comma,%2C,semi,%3B",
@ -275,7 +275,7 @@ public partial class McpClientResourceTemplateTests : ClientServerTestBase
"/semi,%3B,comma,%2C,dot,.",
"/semi,%3B,dot,.,comma,%2C"
]],
["{/keys*}", [
["{/keys*}", [
"/comma=%2C/dot=./semi=%3B",
"/comma=%2C/semi=%3B/dot=.",
"/dot=./comma=%2C/semi=%3B",
@ -286,7 +286,7 @@ public partial class McpClientResourceTemplateTests : ClientServerTestBase
["{;hello:5}", ";hello=Hello"],
["{;list}", ";list=red,green,blue"],
["{;list*}", ";list=red;list=green;list=blue"],
["{;keys}", [
["{;keys}", [
";keys=comma,%2C,dot,.,semi,%3B",
";keys=comma,%2C,semi,%3B,dot,.",
";keys=dot,.,comma,%2C,semi,%3B",
@ -294,7 +294,7 @@ public partial class McpClientResourceTemplateTests : ClientServerTestBase
";keys=semi,%3B,comma,%2C,dot,.",
";keys=semi,%3B,dot,.,comma,%2C"
]],
["{;keys*}", [
["{;keys*}", [
";comma=%2C;dot=.;semi=%3B",
";comma=%2C;semi=%3B;dot=.",
";dot=.;comma=%2C;semi=%3B",
@ -305,7 +305,7 @@ public partial class McpClientResourceTemplateTests : ClientServerTestBase
["{?var:3}", "?var=val"],
["{?list}", "?list=red,green,blue"],
["{?list*}", "?list=red&list=green&list=blue"],
["{?keys}", [
["{?keys}", [
"?keys=comma,%2C,dot,.,semi,%3B",
"?keys=comma,%2C,semi,%3B,dot,.",
"?keys=dot,.,comma,%2C,semi,%3B",
@ -313,7 +313,7 @@ public partial class McpClientResourceTemplateTests : ClientServerTestBase
"?keys=semi,%3B,comma,%2C,dot,.",
"?keys=semi,%3B,dot,.,comma,%2C"
]],
["{?keys*}", [
["{?keys*}", [
"?comma=%2C&dot=.&semi=%3B",
"?comma=%2C&semi=%3B&dot=.",
"?dot=.&comma=%2C&semi=%3B",
@ -324,7 +324,7 @@ public partial class McpClientResourceTemplateTests : ClientServerTestBase
["{&var:3}", "&var=val"],
["{&list}", "&list=red,green,blue"],
["{&list*}", "&list=red&list=green&list=blue"],
["{&keys}", [
["{&keys}", [
"&keys=comma,%2C,dot,.,semi,%3B",
"&keys=comma,%2C,semi,%3B,dot,.",
"&keys=dot,.,comma,%2C,semi,%3B",
@ -332,7 +332,7 @@ public partial class McpClientResourceTemplateTests : ClientServerTestBase
"&keys=semi,%3B,comma,%2C,dot,.",
"&keys=semi,%3B,dot,.,comma,%2C"
]],
["{&keys*}", [
["{&keys*}", [
"&comma=%2C&dot=.&semi=%3B",
"&comma=%2C&semi=%3B&dot=.",
"&dot=.&comma=%2C&semi=%3B",
@ -636,7 +636,7 @@ public partial class McpClientResourceTemplateTests : ClientServerTestBase
"/semi,%3B,comma,%2C,dot,.",
"/semi,%3B,dot,.,comma,%2C"
]],
["{/keys*}", [
["{/keys*}", [
"/comma=%2C/dot=./semi=%3B",
"/comma=%2C/semi=%3B/dot=.",
"/dot=./comma=%2C/semi=%3B",
@ -679,7 +679,7 @@ public partial class McpClientResourceTemplateTests : ClientServerTestBase
["{;x,y,undef}", ";x=1024;y=768"],
["{;list}", ";list=red,green,blue"],
["{;list*}", ";list=red;list=green;list=blue"],
["{;keys}", [
["{;keys}", [
";keys=comma,%2C,dot,.,semi,%3B",
";keys=comma,%2C,semi,%3B,dot,.",
";keys=dot,.,comma,%2C,semi,%3B",
@ -687,7 +687,7 @@ public partial class McpClientResourceTemplateTests : ClientServerTestBase
";keys=semi,%3B,comma,%2C,dot,.",
";keys=semi,%3B,dot,.,comma,%2C"
]],
["{;keys*}", [
["{;keys*}", [
";comma=%2C;dot=.;semi=%3B",
";comma=%2C;semi=%3B;dot=.",
";dot=.;comma=%2C;semi=%3B",
@ -727,7 +727,7 @@ public partial class McpClientResourceTemplateTests : ClientServerTestBase
["{?var:3}", "?var=val"],
["{?list}", "?list=red,green,blue"],
["{?list*}", "?list=red&list=green&list=blue"],
["{?keys}", [
["{?keys}", [
"?keys=comma,%2C,dot,.,semi,%3B",
"?keys=comma,%2C,semi,%3B,dot,.",
"?keys=dot,.,comma,%2C,semi,%3B",
@ -735,7 +735,7 @@ public partial class McpClientResourceTemplateTests : ClientServerTestBase
"?keys=semi,%3B,comma,%2C,dot,.",
"?keys=semi,%3B,dot,.,comma,%2C"
]],
["{?keys*}", [
["{?keys*}", [
"?comma=%2C&dot=.&semi=%3B",
"?comma=%2C&semi=%3B&dot=.",
"?dot=.&comma=%2C&semi=%3B",
@ -775,7 +775,7 @@ public partial class McpClientResourceTemplateTests : ClientServerTestBase
["{&x,y,undef}", "&x=1024&y=768"],
["{&list}", "&list=red,green,blue"],
["{&list*}", "&list=red&list=green&list=blue"],
["{&keys}", [
["{&keys}", [
"&keys=comma,%2C,dot,.,semi,%3B",
"&keys=comma,%2C,semi,%3B,dot,.",
"&keys=dot,.,comma,%2C,semi,%3B",
@ -783,7 +783,7 @@ public partial class McpClientResourceTemplateTests : ClientServerTestBase
"&keys=semi,%3B,comma,%2C,dot,.",
"&keys=semi,%3B,dot,.,comma,%2C"
]],
["{&keys*}", [
["{&keys*}", [
"&comma=%2C&dot=.&semi=%3B",
"&comma=%2C&semi=%3B&dot=.",
"&dot=.&comma=%2C&semi=%3B",

View file

@ -310,7 +310,7 @@ public class McpClientTests : ClientServerTestBase
JsonSerializerOptions emptyOptions = new() { TypeInfoResolver = JsonTypeInfoResolver.Combine() };
await using McpClient client = await CreateMcpClientForServer();
await Assert.ThrowsAsync<NotSupportedException>(async () => await client.GetPromptAsync("Prompt", new Dictionary<string, object?> { ["i"] = 42 }, emptyOptions, cancellationToken: TestContext.Current.CancellationToken));
await Assert.ThrowsAsync<NotSupportedException>(async () => await client.GetPromptAsync("Prompt", new Dictionary<string, object?> { ["i"] = 42 }, new RequestOptions { JsonSerializerOptions = emptyOptions }, cancellationToken: TestContext.Current.CancellationToken));
}
[Fact]
@ -319,7 +319,7 @@ public class McpClientTests : ClientServerTestBase
JsonSerializerOptions options = new(JsonSerializerOptions.Default);
await using McpClient client = await CreateMcpClientForServer();
var tool = (await client.ListToolsAsync(options, TestContext.Current.CancellationToken)).First();
var tool = (await client.ListToolsAsync(new RequestOptions { JsonSerializerOptions = options }, TestContext.Current.CancellationToken)).First();
var originalName = tool.Name;
var renamedTool = tool.WithName("RenamedTool");
@ -333,7 +333,7 @@ public class McpClientTests : ClientServerTestBase
{
JsonSerializerOptions options = new(JsonSerializerOptions.Default);
await using McpClient client = await CreateMcpClientForServer();
var tool = (await client.ListToolsAsync(options, TestContext.Current.CancellationToken)).FirstOrDefault();
var tool = (await client.ListToolsAsync(new RequestOptions { JsonSerializerOptions = options }, TestContext.Current.CancellationToken)).FirstOrDefault();
var originalDescription = tool?.Description;
var redescribedTool = tool?.WithDescription("ToolWithNewDescription");
Assert.NotNull(redescribedTool);
@ -407,7 +407,7 @@ public class McpClientTests : ClientServerTestBase
Assert.False(logger.IsEnabled(LogLevel.Error));
Assert.False(logger.IsEnabled(LogLevel.Critical));
await client.SetLoggingLevel(LoggingLevel.Info, TestContext.Current.CancellationToken);
await client.SetLoggingLevel(LoggingLevel.Info, options: null, TestContext.Current.CancellationToken);
DateTime start = DateTime.UtcNow;
while (Server.LoggingLevel is null)