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
12 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)

View file

@ -34,7 +34,7 @@ public partial class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIn
// Act
await using var client = await _fixture.CreateClientAsync(clientId);
await client.PingAsync(TestContext.Current.CancellationToken);
await client.PingAsync(null, TestContext.Current.CancellationToken);
// Assert
Assert.NotNull(client);
@ -141,7 +141,7 @@ public partial class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIn
// act
await using var client = await _fixture.CreateClientAsync(clientId);
var prompts = await client.ListPromptsAsync(TestContext.Current.CancellationToken);
var prompts = await client.ListPromptsAsync(null, TestContext.Current.CancellationToken);
// assert
Assert.NotEmpty(prompts);
@ -193,7 +193,7 @@ public partial class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIn
// act
await using var client = await _fixture.CreateClientAsync(clientId);
await Assert.ThrowsAsync<McpProtocolException>(async () =>
await Assert.ThrowsAsync<McpProtocolException>(async () =>
await client.GetPromptAsync("non_existent_prompt", null, cancellationToken: TestContext.Current.CancellationToken));
}
@ -206,7 +206,7 @@ public partial class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIn
// act
await using var client = await _fixture.CreateClientAsync(clientId);
IList<McpClientResourceTemplate> allResourceTemplates = await client.ListResourceTemplatesAsync(TestContext.Current.CancellationToken);
IList<McpClientResourceTemplate> allResourceTemplates = await client.ListResourceTemplatesAsync(null, TestContext.Current.CancellationToken);
// The server provides a single test resource template
Assert.Single(allResourceTemplates);
@ -221,7 +221,7 @@ public partial class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIn
// act
await using var client = await _fixture.CreateClientAsync(clientId);
IList<McpClientResource> allResources = await client.ListResourcesAsync(TestContext.Current.CancellationToken);
IList<McpClientResource> allResources = await client.ListResourcesAsync(null, TestContext.Current.CancellationToken);
// The server provides 100 test resources
Assert.Equal(100, allResources.Count);
@ -237,7 +237,7 @@ public partial class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIn
await using var client = await _fixture.CreateClientAsync(clientId);
// Odd numbered resources are text in the everything server (despite the docs saying otherwise)
// 1 is index 0, which is "even" in the 0-based index
var result = await client.ReadResourceAsync("test://static/resource/1", TestContext.Current.CancellationToken);
var result = await client.ReadResourceAsync("test://static/resource/1", null, TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.Single(result.Contents);
@ -256,7 +256,7 @@ public partial class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIn
await using var client = await _fixture.CreateClientAsync(clientId);
// Even numbered resources are binary in the everything server (despite the docs saying otherwise)
// 2 is index 1, which is "odd" in the 0-based index
var result = await client.ReadResourceAsync("test://static/resource/2", TestContext.Current.CancellationToken);
var result = await client.ReadResourceAsync("test://static/resource/2", null, TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.Single(result.Contents);
@ -290,7 +290,7 @@ public partial class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIn
}
});
await client.SubscribeToResourceAsync("test://static/resource/1", TestContext.Current.CancellationToken);
await client.SubscribeToResourceAsync("test://static/resource/1", null, TestContext.Current.CancellationToken);
await tcs.Task;
}
@ -319,13 +319,13 @@ public partial class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIn
]
}
});
await client.SubscribeToResourceAsync("test://static/resource/1", TestContext.Current.CancellationToken);
await client.SubscribeToResourceAsync("test://static/resource/1", null, TestContext.Current.CancellationToken);
// wait until we received a notification
await receivedNotification.Task;
// unsubscribe
await client.UnsubscribeFromResourceAsync("test://static/resource/1", TestContext.Current.CancellationToken);
await client.UnsubscribeFromResourceAsync("test://static/resource/1", null, TestContext.Current.CancellationToken);
}
[Theory]
@ -408,7 +408,7 @@ public partial class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIn
//[Theory]
//[MemberData(nameof(GetClients))]
//public async Task Roots_Stdio_EverythingServer(string clientId)
//{
//{
// var rootsHandlerCalls = 0;
// var testRoots = new List<Root>
// {
@ -573,7 +573,7 @@ public partial class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIn
});
// act
await client.SetLoggingLevel(LoggingLevel.Debug, TestContext.Current.CancellationToken);
await client.SetLoggingLevel(LoggingLevel.Debug, options: null, TestContext.Current.CancellationToken);
// assert
await receivedNotification.Task;

View file

@ -100,7 +100,7 @@ public partial class McpServerBuilderExtensionsPromptsTests : ClientServerTestBa
{
await using McpClient client = await CreateMcpClientForServer();
var prompts = await client.ListPromptsAsync(TestContext.Current.CancellationToken);
var prompts = await client.ListPromptsAsync(null, TestContext.Current.CancellationToken);
Assert.Equal(6, prompts.Count);
var prompt = prompts.First(t => t.Name == "returns_chat_messages");
@ -129,7 +129,7 @@ public partial class McpServerBuilderExtensionsPromptsTests : ClientServerTestBa
{
await using McpClient client = await CreateMcpClientForServer();
var prompts = await client.ListPromptsAsync(TestContext.Current.CancellationToken);
var prompts = await client.ListPromptsAsync(null, TestContext.Current.CancellationToken);
Assert.Equal(6, prompts.Count);
Channel<JsonRpcNotification> listChanged = Channel.CreateUnbounded<JsonRpcNotification>();
@ -150,7 +150,7 @@ public partial class McpServerBuilderExtensionsPromptsTests : ClientServerTestBa
serverPrompts.Add(newPrompt);
await notificationRead;
prompts = await client.ListPromptsAsync(TestContext.Current.CancellationToken);
prompts = await client.ListPromptsAsync(null, TestContext.Current.CancellationToken);
Assert.Equal(7, prompts.Count);
Assert.Contains(prompts, t => t.Name == "NewPrompt");
@ -160,7 +160,7 @@ public partial class McpServerBuilderExtensionsPromptsTests : ClientServerTestBa
await notificationRead;
}
prompts = await client.ListPromptsAsync(TestContext.Current.CancellationToken);
prompts = await client.ListPromptsAsync(null, TestContext.Current.CancellationToken);
Assert.Equal(6, prompts.Count);
Assert.DoesNotContain(prompts, t => t.Name == "NewPrompt");
}

View file

@ -129,7 +129,7 @@ public partial class McpServerBuilderExtensionsResourcesTests : ClientServerTest
Assert.NotNull(client.ServerCapabilities.Resources);
var resources = await client.ListResourcesAsync(TestContext.Current.CancellationToken);
var resources = await client.ListResourcesAsync(null, TestContext.Current.CancellationToken);
Assert.Equal(5, resources.Count);
var resource = resources.First(t => t.Name == "some_neat_direct_resource");
@ -146,7 +146,7 @@ public partial class McpServerBuilderExtensionsResourcesTests : ClientServerTest
{
await using McpClient client = await CreateMcpClientForServer();
var resources = await client.ListResourceTemplatesAsync(TestContext.Current.CancellationToken);
var resources = await client.ListResourceTemplatesAsync(null, TestContext.Current.CancellationToken);
Assert.Equal(3, resources.Count);
var resource = resources.First(t => t.Name == "some_neat_templated_resource");
@ -163,7 +163,7 @@ public partial class McpServerBuilderExtensionsResourcesTests : ClientServerTest
{
await using McpClient client = await CreateMcpClientForServer();
var resources = await client.ListResourcesAsync(TestContext.Current.CancellationToken);
var resources = await client.ListResourcesAsync(null, TestContext.Current.CancellationToken);
Assert.Equal(5, resources.Count);
Channel<JsonRpcNotification> listChanged = Channel.CreateUnbounded<JsonRpcNotification>();
@ -184,7 +184,7 @@ public partial class McpServerBuilderExtensionsResourcesTests : ClientServerTest
serverResources.Add(newResource);
await notificationRead;
resources = await client.ListResourcesAsync(TestContext.Current.CancellationToken);
resources = await client.ListResourcesAsync(null, TestContext.Current.CancellationToken);
Assert.Equal(6, resources.Count);
Assert.Contains(resources, t => t.Name == "NewResource");
@ -194,7 +194,7 @@ public partial class McpServerBuilderExtensionsResourcesTests : ClientServerTest
await notificationRead;
}
resources = await client.ListResourcesAsync(TestContext.Current.CancellationToken);
resources = await client.ListResourcesAsync(null, TestContext.Current.CancellationToken);
Assert.Equal(5, resources.Count);
Assert.DoesNotContain(resources, t => t.Name == "NewResource");
}

View file

@ -12,6 +12,7 @@ using System.Collections.Concurrent;
using System.ComponentModel;
using System.IO.Pipelines;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using System.Threading.Channels;
@ -686,7 +687,11 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
new CallToolRequestParams
{
Name = progressTool.ProtocolTool.Name,
ProgressToken = new("abc123"),
// Set the progress token in Meta
Meta = new JsonObject
{
["progressToken"] = "abc123",
}
},
cancellationToken: TestContext.Current.CancellationToken);

View file

@ -41,7 +41,7 @@ public class McpServerResourceCapabilityIntegrationTests : ClientServerTestBase
Assert.True(client.ServerCapabilities.Resources.Subscribe, "Server should advertise Subscribe capability when manually set");
// The resources should be exposed and listable
var resources = await client.ListResourcesAsync(TestContext.Current.CancellationToken);
var resources = await client.ListResourcesAsync(null, TestContext.Current.CancellationToken);
Assert.NotEmpty(resources);
Assert.Contains(resources, r => r.Name == "test_resource");
}
@ -56,7 +56,7 @@ public class McpServerResourceCapabilityIntegrationTests : ClientServerTestBase
Assert.NotNull(client.ServerCapabilities.Resources);
// The resources should be exposed and listable
var resources = await client.ListResourcesAsync(TestContext.Current.CancellationToken);
var resources = await client.ListResourcesAsync(null, TestContext.Current.CancellationToken);
Assert.NotEmpty(resources);
}

View file

@ -23,19 +23,19 @@ public sealed class McpServerResourceRoutingTests(ITestOutputHelper testOutputHe
// 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);
var nonTemplatedResult = await client.ReadResourceAsync("test://resource/non-templated", null, TestContext.Current.CancellationToken);
Assert.Equal("static", ((TextResourceContents)nonTemplatedResult.Contents[0]).Text);
var templatedResult = await client.ReadResourceAsync("test://resource/12345", TestContext.Current.CancellationToken);
var templatedResult = await client.ReadResourceAsync("test://resource/12345", null, TestContext.Current.CancellationToken);
Assert.Equal("template: 12345", ((TextResourceContents)templatedResult.Contents[0]).Text);
var exactTemplatedResult = await client.ReadResourceAsync("test://resource/{id}", TestContext.Current.CancellationToken);
var exactTemplatedResult = await client.ReadResourceAsync("test://resource/{id}", null, 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);
var paramsResult = await client.ReadResourceAsync("test://params?a1=a&a2=b&a3=c", null, TestContext.Current.CancellationToken);
Assert.Equal("params: a, b, c", ((TextResourceContents)paramsResult.Contents[0]).Text);
var mcpEx = await Assert.ThrowsAsync<McpProtocolException>(async () => await client.ReadResourceAsync("test://params{?a1,a2,a3}", TestContext.Current.CancellationToken));
var mcpEx = await Assert.ThrowsAsync<McpProtocolException>(async () => await client.ReadResourceAsync("test://params{?a1,a2,a3}", null, TestContext.Current.CancellationToken));
Assert.Equal(McpErrorCode.InvalidParams, mcpEx.ErrorCode);
Assert.Equal("Request failed (remote): Unknown resource URI: 'test://params{?a1,a2,a3}'", mcpEx.Message);
}

View file

@ -24,7 +24,7 @@ public partial class McpServerScopedTests : ClientServerTestBase
{
await using McpClient client = await CreateMcpClientForServer();
var tools = await client.ListToolsAsync(McpServerScopedTestsJsonContext.Default.Options, TestContext.Current.CancellationToken);
var tools = await client.ListToolsAsync(new RequestOptions { JsonSerializerOptions = McpServerScopedTestsJsonContext.Default.Options }, TestContext.Current.CancellationToken);
var tool = tools.First(t => t.Name == "echo_complex");
Assert.DoesNotContain("\"complex\"", JsonSerializer.Serialize(tool.JsonSchema, McpJsonUtilities.DefaultOptions));

View file

@ -0,0 +1,448 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using ModelContextProtocol.Protocol;
namespace ModelContextProtocol.Tests;
public static class RequestOptionsTests
{
[Fact]
public static void RequestOptions_DefaultConstructor()
{
// Arrange & Act
var options = new RequestOptions();
// Assert
// ProgressToken and JsonSerializerOptions should be null by default
Assert.Null(options.ProgressToken);
Assert.Null(options.JsonSerializerOptions);
// Meta should return a new (empty) JsonObject when accessed
Assert.NotNull(options.Meta);
Assert.Empty(options.Meta);
}
[Fact]
public static void RequestOptions_MetaSetter_SetsValue()
{
// Arrange
var options = new RequestOptions();
var metaValue = new JsonObject
{
["key"] = "value"
};
// Act
options.Meta = metaValue;
// Assert
Assert.Same(metaValue, options.Meta);
Assert.Equal("value", options.Meta["key"]?.ToString());
}
[Fact]
public static void RequestOptions_Meta_SetsSingleField()
{
// Arrange
var options = new RequestOptions();
// Act
options.Meta!["key"] = "value";
// Assert
Assert.Equal("value", options.Meta!["key"]?.ToString());
}
[Fact]
public static void RequestOptions_MetaSetter_NullValue_RemovesMeta()
{
// Arrange
var options = new RequestOptions
{
Meta = new JsonObject { ["key"] = "value" }
};
// Act
options.Meta = null;
// Assert - accessing Meta will create a new empty JsonObject
Assert.NotNull(options.Meta);
Assert.Empty(options.Meta);
}
[Fact]
public static void RequestOptions_MetaSetter_WithProgressToken_InMeta()
{
// Arrange
var options = new RequestOptions();
var newMeta = new JsonObject
{
["custom"] = "data",
["progressToken"] = "token"
};
// Act
options.Meta = newMeta;
// Assert
Assert.Equal("token", options.ProgressToken?.ToString());
Assert.Equal("data", options.Meta["custom"]?.ToString());
Assert.True(options.Meta.ContainsKey("progressToken"));
}
[Fact]
public static void RequestOptions_ProgressTokenGetter_ReturnsNull_WhenNotSet()
{
// Arrange
var options = new RequestOptions
{
Meta = new JsonObject { ["other"] = "value" }
};
// Act
var token = options.ProgressToken;
// Assert
Assert.Null(token);
}
[Fact]
public static void RequestOptions_ProgressTokenSetter_StringToken_SetsInMeta()
{
// Arrange
var options = new RequestOptions();
var token = new ProgressToken("my-token");
// Act
options.ProgressToken = token;
// Assert
Assert.Equal("my-token", options.ProgressToken?.ToString());
Assert.NotNull(options.Meta);
Assert.Equal("my-token", options.Meta["progressToken"]?.ToString());
}
[Fact]
public static void RequestOptions_ProgressTokenSetter_LongToken_SetsInMeta()
{
// Arrange
var options = new RequestOptions();
var token = new ProgressToken(42L);
// Act
options.ProgressToken = token;
// Assert
Assert.Equal("42", options.ProgressToken?.ToString());
Assert.NotNull(options.Meta);
Assert.Equal(42L, options.Meta["progressToken"]?.AsValue().GetValue<long>());
}
[Fact]
public static void RequestOptions_ProgressTokenSetter_Null_RemovesFromMeta()
{
// Arrange
var options = new RequestOptions
{
ProgressToken = new ProgressToken("token-to-remove")
};
// Act
options.ProgressToken = null;
// Assert
Assert.Null(options.ProgressToken);
Assert.False(options.Meta!.ContainsKey("progressToken"));
}
[Fact]
public static void RequestOptions_ProgressTokenSetter_Null_WhenNoProgressToken_DoesNothing()
{
// Arrange
var options = new RequestOptions();
// Act
options.ProgressToken = null;
// Assert
Assert.Null(options.ProgressToken);
}
[Fact]
public static void RequestOptions_ProgressTokenGetter_StringValue_ReturnsCorrectToken()
{
// Arrange
var options = new RequestOptions
{
Meta = new JsonObject { ["progressToken"] = "test-token" }
};
// Act
var token = options.ProgressToken;
// Assert
Assert.NotNull(token);
Assert.Equal("test-token", token.Value.ToString());
}
[Fact]
public static void RequestOptions_ProgressTokenGetter_LongValue_ReturnsCorrectToken()
{
// Arrange
var options = new RequestOptions
{
Meta = new JsonObject { ["progressToken"] = 123L }
};
// Act
var token = options.ProgressToken;
// Assert
Assert.NotNull(token);
Assert.Equal("123", token.Value.ToString());
}
[Fact]
public static void RequestOptions_ProgressTokenGetter_InvalidValue_ReturnsNull()
{
// Arrange
var options = new RequestOptions
{
Meta = new JsonObject { ["progressToken"] = new JsonObject() }
};
// Act
var token = options.ProgressToken;
// Assert
Assert.Null(token);
}
[Fact]
public static void RequestOptions_JsonSerializerOptions_GetSet()
{
// Arrange
var options = new RequestOptions();
var serializerOptions = new JsonSerializerOptions { WriteIndented = true };
// Act
options.JsonSerializerOptions = serializerOptions;
// Assert
Assert.Same(serializerOptions, options.JsonSerializerOptions);
}
[Fact]
public static void RequestOptions_MetaAndProgressToken_WorkTogether()
{
// Arrange
var options = new RequestOptions();
// Act - set progress token first
options.ProgressToken = new ProgressToken("token1");
options.Meta!["custom"] = "value1";
// Assert
Assert.Equal("token1", options.ProgressToken?.ToString());
Assert.Equal("value1", options.Meta["custom"]?.ToString());
Assert.True(options.Meta.ContainsKey("progressToken"));
}
[Fact]
public static void RequestOptions_ProgressToken_OverwritesPreviousValue()
{
// Arrange
var options = new RequestOptions
{
ProgressToken = new ProgressToken("old-token")
};
// Act
options.ProgressToken = new ProgressToken("new-token");
// Assert
Assert.Equal("new-token", options.ProgressToken?.ToString());
Assert.Equal("new-token", options.Meta!["progressToken"]?.ToString());
}
[Fact]
public static void RequestOptions_ProgressToken_StringToLong_ChangesType()
{
// Arrange
var options = new RequestOptions
{
ProgressToken = new ProgressToken("string-token")
};
// Act
options.ProgressToken = new ProgressToken(999L);
// Assert
Assert.Equal("999", options.ProgressToken?.ToString());
Assert.Equal(999L, options.Meta!["progressToken"]?.AsValue().GetValue<long>());
}
[Fact]
public static void RequestOptions_Meta_MultipleProperties_Preserved()
{
// Arrange
var options = new RequestOptions
{
Meta = new JsonObject
{
["prop1"] = "value1",
["prop2"] = 42,
["prop3"] = true
}
};
// Act
options.ProgressToken = new ProgressToken("my-token");
// Assert
Assert.Equal("value1", options.Meta["prop1"]?.ToString());
Assert.Equal(42, options.Meta["prop2"]?.AsValue().GetValue<int>());
Assert.True(options.Meta["prop3"]?.AsValue().GetValue<bool>());
Assert.Equal("my-token", options.Meta["progressToken"]?.ToString());
}
[Fact]
public static void RequestOptions_MetaSetter_ReplacesExistingMeta()
{
// Arrange
var options = new RequestOptions
{
Meta = new JsonObject
{
["old1"] = "value1",
["old2"] = "value2"
}
};
var newMeta = new JsonObject
{
["new1"] = "newValue1",
["new2"] = "newValue2"
};
// Act
options.Meta = newMeta;
// Assert
Assert.False(options.Meta.ContainsKey("old1"));
Assert.False(options.Meta.ContainsKey("old2"));
Assert.Equal("newValue1", options.Meta["new1"]?.ToString());
Assert.Equal("newValue2", options.Meta["new2"]?.ToString());
}
[Fact]
public static void RequestOptions_MetaSetter_NullWithNoProgressToken_ClearsMeta()
{
// Arrange
var options = new RequestOptions
{
Meta = new JsonObject { ["key"] = "value" }
};
// Act
options.Meta = null;
// Assert - getter creates new empty object
Assert.NotNull(options.Meta);
Assert.Empty(options.Meta);
}
[Fact]
public static void RequestOptions_ProgressTokenSetter_CreatesMetaIfNeeded()
{
// Arrange
var options = new RequestOptions();
Assert.Null(options.ProgressToken);
// Act
options.ProgressToken = new ProgressToken("create-meta");
// Assert
Assert.NotNull(options.ProgressToken);
Assert.Equal("create-meta", options.ProgressToken?.ToString());
Assert.True(options.Meta!.ContainsKey("progressToken"));
}
[Fact]
public static void RequestOptions_ComplexScenario_SetMetaThenProgressToken()
{
// Arrange
var options = new RequestOptions
{
Meta = new JsonObject
{
["custom1"] = "value1",
["custom2"] = 123
}
};
// Act
options.ProgressToken = new ProgressToken("scenario-token");
// Assert
Assert.Equal("value1", options.Meta["custom1"]?.ToString());
Assert.Equal(123, options.Meta["custom2"]?.AsValue().GetValue<int>());
Assert.Equal("scenario-token", options.ProgressToken?.ToString());
Assert.Equal(3, options.Meta.Count);
}
[Fact]
public static void RequestOptions_ComplexScenario_SetMetaWithProgressToken()
{
// Arrange
var options = new RequestOptions();
// Act
var newMeta = new JsonObject
{
["data1"] = "info1",
["data2"] = false,
["progressToken"] = 456L
};
options.Meta = newMeta;
// Assert
Assert.Equal("info1", options.Meta["data1"]?.ToString());
Assert.False(options.Meta["data2"]?.AsValue().GetValue<bool>());
Assert.Equal(456L, options.ProgressToken?.Token as long?);
Assert.Equal(3, options.Meta.Count);
}
[Fact]
public static void RequestOptions_ComplexScenario_SetMetaWithProgressUpdatesProgress()
{
// Arrange
RequestOptions options = new();
// Act
options.ProgressToken = new ProgressToken("token1");
JsonObject meta = new() { ["progressToken"] = "token2" };
options.Meta = meta;
// Assert
Assert.Equal("token2", options.ProgressToken?.Token as string);
Assert.Equal("token2", options.Meta["progressToken"]?.ToString());
}
[Fact]
public static void RequestOptions_AllProperties_CanBeSetIndependently()
{
// Arrange
var options = new RequestOptions();
var customOptions = new JsonSerializerOptions { WriteIndented = true };
// Act
options.JsonSerializerOptions = customOptions;
options.ProgressToken = new ProgressToken("independent");
options.Meta!["field"] = "value";
// Assert
Assert.Same(customOptions, options.JsonSerializerOptions);
Assert.Equal("independent", options.ProgressToken?.ToString());
Assert.Equal("value", options.Meta["field"]?.ToString());
}
}

View file

@ -31,7 +31,7 @@ public class EmptyCollectionTests : ClientServerTestBase
var client = await CreateMcpClientForServer();
// Initially, the resource collection is empty
var initialResources = await client.ListResourcesAsync(TestContext.Current.CancellationToken);
var initialResources = await client.ListResourcesAsync(options: null, TestContext.Current.CancellationToken);
Assert.Empty(initialResources);
// Add a resource dynamically
@ -40,7 +40,7 @@ public class EmptyCollectionTests : ClientServerTestBase
new() { UriTemplate = "test://resource/1" }));
// The resource should now be listed
var updatedResources = await client.ListResourcesAsync(TestContext.Current.CancellationToken);
var updatedResources = await client.ListResourcesAsync(options: null, TestContext.Current.CancellationToken);
Assert.Single(updatedResources);
Assert.Equal("test://resource/1", updatedResources[0].Uri);
}
@ -71,7 +71,7 @@ public class EmptyCollectionTests : ClientServerTestBase
var client = await CreateMcpClientForServer();
// Initially, the prompt collection is empty
var initialPrompts = await client.ListPromptsAsync(TestContext.Current.CancellationToken);
var initialPrompts = await client.ListPromptsAsync(options: null, TestContext.Current.CancellationToken);
Assert.Empty(initialPrompts);
// Add a prompt dynamically
@ -80,7 +80,7 @@ public class EmptyCollectionTests : ClientServerTestBase
new() { Name = "test_prompt", Description = "A test prompt" }));
// The prompt should now be listed
var updatedPrompts = await client.ListPromptsAsync(TestContext.Current.CancellationToken);
var updatedPrompts = await client.ListPromptsAsync(options: null, TestContext.Current.CancellationToken);
Assert.Single(updatedPrompts);
Assert.Equal("test_prompt", updatedPrompts[0].Name);
}
@ -96,7 +96,7 @@ public class EmptyCollectionTests : ClientServerTestBase
new() { UriTemplate = "test://resource/dynamic" }));
// Read the resource
var result = await client.ReadResourceAsync("test://resource/dynamic", TestContext.Current.CancellationToken);
var result = await client.ReadResourceAsync("test://resource/dynamic", options: null, TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.Single(result.Contents);
Assert.IsType<TextResourceContents>(result.Contents[0]);
@ -159,7 +159,7 @@ public class NullCollectionTests : ClientServerTestBase
var client = await CreateMcpClientForServer();
await Assert.ThrowsAsync<McpProtocolException>(async () => await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken));
await Assert.ThrowsAsync<McpProtocolException>(async () => await client.ListPromptsAsync(TestContext.Current.CancellationToken));
await Assert.ThrowsAsync<McpProtocolException>(async () => await client.ListResourcesAsync(TestContext.Current.CancellationToken));
await Assert.ThrowsAsync<McpProtocolException>(async () => await client.ListPromptsAsync(cancellationToken: TestContext.Current.CancellationToken));
await Assert.ThrowsAsync<McpProtocolException>(async () => await client.ListResourcesAsync(cancellationToken: TestContext.Current.CancellationToken));
}
}