From 350f8e1ee4dff2d44072455b2e9a3b9619a87e00 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 3 Nov 2025 23:05:56 +0000
Subject: [PATCH 1/4] Initial plan
From 44ea87cda1543cc631e525bd2718a96724f87088 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 3 Nov 2025 23:24:23 +0000
Subject: [PATCH 2/4] Add public constructor to McpClientTool for reusing tool
definitions
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
---
.../Client/McpClientTool.cs | 51 +++
.../Client/McpClientToolTests.cs | 311 ++++++++++++++++++
2 files changed, 362 insertions(+)
create mode 100644 tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs
diff --git a/src/ModelContextProtocol.Core/Client/McpClientTool.cs b/src/ModelContextProtocol.Core/Client/McpClientTool.cs
index c7af513ef..9c5f0efd3 100644
--- a/src/ModelContextProtocol.Core/Client/McpClientTool.cs
+++ b/src/ModelContextProtocol.Core/Client/McpClientTool.cs
@@ -53,6 +53,57 @@ internal McpClientTool(
_progress = progress;
}
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The instance to use for invoking the tool.
+ /// The protocol definition describing the tool's metadata and schema.
+ ///
+ /// The JSON serialization options governing argument serialization. If ,
+ /// will be used.
+ ///
+ ///
+ ///
+ /// This constructor enables reusing cached tool definitions across different instances
+ /// without needing to call on every reconnect. This is particularly useful
+ /// in scenarios where tool definitions are stable and network round-trips should be minimized.
+ ///
+ ///
+ /// The provided must represent a tool that is actually available on the server
+ /// associated with the . Attempting to invoke a tool that doesn't exist on the
+ /// server will result in an .
+ ///
+ ///
+ /// is .
+ /// is .
+ ///
+ ///
+ /// // Cache tool definition from first client
+ /// var tools = await client1.ListToolsAsync();
+ /// var toolDefinition = tools[0].ProtocolTool;
+ ///
+ /// // Later, reuse with a different client instance
+ /// var client2 = await McpClient.CreateAsync(transport2);
+ /// var reusedTool = new McpClientTool(client2, toolDefinition);
+ /// var result = await reusedTool.CallAsync(new Dictionary<string, object?> { ["param"] = "value" });
+ ///
+ ///
+ public McpClientTool(
+ McpClient client,
+ Tool tool,
+ JsonSerializerOptions? serializerOptions = null)
+ {
+ Throw.IfNull(client);
+ Throw.IfNull(tool);
+
+ _client = client;
+ ProtocolTool = tool;
+ JsonSerializerOptions = serializerOptions ?? McpJsonUtilities.DefaultOptions;
+ _name = tool.Name;
+ _description = tool.Description ?? string.Empty;
+ _progress = null;
+ }
+
///
/// Gets the protocol type for this instance.
///
diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs
new file mode 100644
index 000000000..f06f23680
--- /dev/null
+++ b/tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs
@@ -0,0 +1,311 @@
+using Microsoft.Extensions.DependencyInjection;
+using ModelContextProtocol.Client;
+using ModelContextProtocol.Protocol;
+using ModelContextProtocol.Server;
+using System.Text.Json;
+
+namespace ModelContextProtocol.Tests.Client;
+
+public class McpClientToolTests : ClientServerTestBase
+{
+ public McpClientToolTests(ITestOutputHelper outputHelper)
+ : base(outputHelper)
+ {
+ }
+
+ protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder)
+ {
+ // Add a simple echo tool for testing
+ mcpServerBuilder.WithTools([McpServerTool.Create((string message) => $"Echo: {message}", new() { Name = "echo", Description = "Echoes back the message" })]);
+
+ // Add a tool with parameters for testing
+ mcpServerBuilder.WithTools([McpServerTool.Create((int a, int b) => a + b, new() { Name = "add", Description = "Adds two numbers" })]);
+
+ // Add a tool that returns complex result
+ mcpServerBuilder.WithTools([McpServerTool.Create((string name, int age) => $"Person: {name}, Age: {age}", new() { Name = "createPerson", Description = "Creates a person description" })]);
+ }
+
+ [Fact]
+ public async Task Constructor_WithValidParameters_CreatesInstance()
+ {
+ await using McpClient client = await CreateMcpClientForServer();
+
+ // Get a tool definition from the server
+ var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
+ var originalTool = tools.First(t => t.Name == "echo");
+ var toolDefinition = originalTool.ProtocolTool;
+
+ // Create a new McpClientTool using the public constructor
+ var newTool = new McpClientTool(client, toolDefinition);
+
+ Assert.NotNull(newTool);
+ Assert.Equal("echo", newTool.Name);
+ Assert.Equal("Echoes back the message", newTool.Description);
+ Assert.Same(toolDefinition, newTool.ProtocolTool);
+ }
+
+ [Fact]
+ public async Task Constructor_WithNullClient_ThrowsArgumentNullException()
+ {
+ var toolDefinition = new Tool
+ {
+ Name = "test",
+ Description = "Test tool"
+ };
+
+ var exception = Assert.Throws(() => new McpClientTool(null!, toolDefinition));
+ Assert.Equal("client", exception.ParamName);
+ }
+
+ [Fact]
+ public async Task Constructor_WithNullTool_ThrowsArgumentNullException()
+ {
+ await using McpClient client = await CreateMcpClientForServer();
+
+ var exception = Assert.Throws(() => new McpClientTool(client, null!));
+ Assert.Equal("tool", exception.ParamName);
+ }
+
+ [Fact]
+ public async Task Constructor_WithNullSerializerOptions_UsesDefaultOptions()
+ {
+ await using McpClient client = await CreateMcpClientForServer();
+
+ var toolDefinition = new Tool
+ {
+ Name = "test",
+ Description = "Test tool"
+ };
+
+ var tool = new McpClientTool(client, toolDefinition, serializerOptions: null);
+
+ Assert.NotNull(tool.JsonSerializerOptions);
+ Assert.Same(McpJsonUtilities.DefaultOptions, tool.JsonSerializerOptions);
+ }
+
+ [Fact]
+ public async Task Constructor_WithCustomSerializerOptions_UsesProvidedOptions()
+ {
+ await using McpClient client = await CreateMcpClientForServer();
+
+ var toolDefinition = new Tool
+ {
+ Name = "test",
+ Description = "Test tool"
+ };
+
+ var customOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);
+
+ var tool = new McpClientTool(client, toolDefinition, customOptions);
+
+ Assert.NotNull(tool.JsonSerializerOptions);
+ Assert.Same(customOptions, tool.JsonSerializerOptions);
+ }
+
+ [Fact]
+ public async Task ReuseToolDefinition_AcrossDifferentClients_InvokesSuccessfully()
+ {
+ // Create first client and get tool definition
+ Tool toolDefinition;
+ {
+ await using McpClient client1 = await CreateMcpClientForServer();
+ var tools = await client1.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
+ var echoTool = tools.First(t => t.Name == "echo");
+ toolDefinition = echoTool.ProtocolTool;
+ }
+
+ // Create second client (simulating reconnect)
+ await using McpClient client2 = await CreateMcpClientForServer();
+
+ // Create new McpClientTool with cached tool definition and new client
+ var reusedTool = new McpClientTool(client2, toolDefinition);
+
+ // Invoke the tool using the new client
+ var result = await reusedTool.CallAsync(
+ new Dictionary { ["message"] = "Hello from reused tool" },
+ cancellationToken: TestContext.Current.CancellationToken);
+
+ Assert.NotNull(result);
+ Assert.NotNull(result.Content);
+ var textContent = result.Content.FirstOrDefault() as TextContentBlock;
+ Assert.NotNull(textContent);
+ Assert.Equal("Echo: Hello from reused tool", textContent.Text);
+ }
+
+ [Fact]
+ public async Task ReuseToolDefinition_WithComplexParameters_InvokesSuccessfully()
+ {
+ // Create first client and get tool definition
+ Tool toolDefinition;
+ {
+ await using McpClient client1 = await CreateMcpClientForServer();
+ var tools = await client1.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
+ var addTool = tools.First(t => t.Name == "add");
+ toolDefinition = addTool.ProtocolTool;
+ }
+
+ // Create second client
+ await using McpClient client2 = await CreateMcpClientForServer();
+
+ // Create new McpClientTool with cached tool definition
+ var reusedTool = new McpClientTool(client2, toolDefinition);
+
+ // Invoke the tool with integer parameters
+ var result = await reusedTool.CallAsync(
+ new Dictionary { ["a"] = 5, ["b"] = 7 },
+ cancellationToken: TestContext.Current.CancellationToken);
+
+ Assert.NotNull(result);
+ Assert.NotNull(result.Content);
+ var textContent = result.Content.FirstOrDefault() as TextContentBlock;
+ Assert.NotNull(textContent);
+ Assert.Equal("12", textContent.Text);
+ }
+
+ [Fact]
+ public async Task ReuseToolDefinition_PreservesToolMetadata()
+ {
+ await using McpClient client = await CreateMcpClientForServer();
+
+ var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
+ var originalTool = tools.First(t => t.Name == "createPerson");
+ var toolDefinition = originalTool.ProtocolTool;
+
+ // Create new McpClientTool with cached tool definition
+ var reusedTool = new McpClientTool(client, toolDefinition);
+
+ // Verify metadata is preserved
+ Assert.Equal(originalTool.Name, reusedTool.Name);
+ Assert.Equal(originalTool.Description, reusedTool.Description);
+ Assert.Equal(originalTool.ProtocolTool.Name, reusedTool.ProtocolTool.Name);
+ Assert.Equal(originalTool.ProtocolTool.Description, reusedTool.ProtocolTool.Description);
+
+ // Verify JSON schema is preserved
+ Assert.Equal(
+ JsonSerializer.Serialize(originalTool.JsonSchema, McpJsonUtilities.DefaultOptions),
+ JsonSerializer.Serialize(reusedTool.JsonSchema, McpJsonUtilities.DefaultOptions));
+ }
+
+ [Fact]
+ public async Task ManuallyConstructedTool_CanBeInvoked()
+ {
+ await using McpClient client = await CreateMcpClientForServer();
+
+ // Manually construct a Tool object matching the server's tool
+ var manualTool = new Tool
+ {
+ Name = "echo",
+ Description = "Echoes back the message",
+ InputSchema = JsonDocument.Parse("""
+ {
+ "type": "object",
+ "properties": {
+ "message": { "type": "string" }
+ }
+ }
+ """).RootElement.Clone()
+ };
+
+ // Create McpClientTool with manually constructed tool
+ var clientTool = new McpClientTool(client, manualTool);
+
+ // Invoke the tool
+ var result = await clientTool.CallAsync(
+ new Dictionary { ["message"] = "Test message" },
+ cancellationToken: TestContext.Current.CancellationToken);
+
+ Assert.NotNull(result);
+ Assert.NotNull(result.Content);
+ var textContent = result.Content.FirstOrDefault() as TextContentBlock;
+ Assert.NotNull(textContent);
+ Assert.Equal("Echo: Test message", textContent.Text);
+ }
+
+ [Fact]
+ public async Task ReuseToolDefinition_WithInvokeAsync_WorksCorrectly()
+ {
+ Tool toolDefinition;
+ {
+ await using McpClient client1 = await CreateMcpClientForServer();
+ var tools = await client1.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
+ var addTool = tools.First(t => t.Name == "add");
+ toolDefinition = addTool.ProtocolTool;
+ }
+
+ await using McpClient client2 = await CreateMcpClientForServer();
+ var reusedTool = new McpClientTool(client2, toolDefinition);
+
+ // Use AIFunction.InvokeAsync (inherited method)
+ var result = await reusedTool.InvokeAsync(
+ new() { ["a"] = 10, ["b"] = 20 },
+ TestContext.Current.CancellationToken);
+
+ Assert.NotNull(result);
+
+ // InvokeAsync returns a JsonElement containing the serialized CallToolResult
+ var jsonElement = Assert.IsType(result);
+ var callToolResult = JsonSerializer.Deserialize(jsonElement, McpJsonUtilities.DefaultOptions);
+
+ Assert.NotNull(callToolResult);
+ Assert.NotNull(callToolResult.Content);
+ var textContent = callToolResult.Content.FirstOrDefault() as TextContentBlock;
+ Assert.NotNull(textContent);
+ Assert.Equal("30", textContent.Text);
+ }
+
+ [Fact]
+ public async Task Constructor_WithToolWithoutDescription_UsesEmptyDescription()
+ {
+ await using McpClient client = await CreateMcpClientForServer();
+
+ var toolWithoutDescription = new Tool
+ {
+ Name = "noDescTool",
+ Description = null
+ };
+
+ var clientTool = new McpClientTool(client, toolWithoutDescription);
+
+ Assert.Equal("noDescTool", clientTool.Name);
+ Assert.Equal(string.Empty, clientTool.Description);
+ }
+
+ [Fact]
+ public async Task ReuseToolDefinition_MultipleClients_AllWorkIndependently()
+ {
+ // Get tool definition from first client
+ Tool toolDefinition;
+ {
+ await using McpClient client1 = await CreateMcpClientForServer();
+ var tools = await client1.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
+ var echoTool = tools.First(t => t.Name == "echo");
+ toolDefinition = echoTool.ProtocolTool;
+ }
+
+ // Create and invoke on second client
+ string text2;
+ {
+ await using McpClient client2 = await CreateMcpClientForServer();
+ var tool2 = new McpClientTool(client2, toolDefinition);
+ var result2 = await tool2.CallAsync(
+ new Dictionary { ["message"] = "From client 2" },
+ cancellationToken: TestContext.Current.CancellationToken);
+ text2 = (result2.Content.FirstOrDefault() as TextContentBlock)?.Text ?? string.Empty;
+ }
+
+ // Create and invoke on third client
+ string text3;
+ {
+ await using McpClient client3 = await CreateMcpClientForServer();
+ var tool3 = new McpClientTool(client3, toolDefinition);
+ var result3 = await tool3.CallAsync(
+ new Dictionary { ["message"] = "From client 3" },
+ cancellationToken: TestContext.Current.CancellationToken);
+ text3 = (result3.Content.FirstOrDefault() as TextContentBlock)?.Text ?? string.Empty;
+ }
+
+ // Verify both worked
+ Assert.Equal("Echo: From client 2", text2);
+ Assert.Equal("Echo: From client 3", text3);
+ }
+}
From 7c6c554389816b72c3457260fcb8f2fc8946e49d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 4 Nov 2025 01:53:52 +0000
Subject: [PATCH 3/4] Fix McpClientTool constructor per review feedback
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
---
.../Client/McpClientTool.cs | 44 +++++++------------
.../Client/McpClientToolTests.cs | 6 +--
2 files changed, 18 insertions(+), 32 deletions(-)
diff --git a/src/ModelContextProtocol.Core/Client/McpClientTool.cs b/src/ModelContextProtocol.Core/Client/McpClientTool.cs
index 9c5f0efd3..60c2ee89d 100644
--- a/src/ModelContextProtocol.Core/Client/McpClientTool.cs
+++ b/src/ModelContextProtocol.Core/Client/McpClientTool.cs
@@ -37,22 +37,6 @@ public sealed class McpClientTool : AIFunction
private readonly string _description;
private readonly IProgress? _progress;
- internal McpClientTool(
- McpClient client,
- Tool tool,
- JsonSerializerOptions serializerOptions,
- string? name = null,
- string? description = null,
- IProgress? progress = null)
- {
- _client = client;
- ProtocolTool = tool;
- JsonSerializerOptions = serializerOptions;
- _name = name ?? tool.Name;
- _description = description ?? tool.Description ?? string.Empty;
- _progress = progress;
- }
-
///
/// Initializes a new instance of the class.
///
@@ -76,18 +60,6 @@ internal McpClientTool(
///
/// is .
/// is .
- ///
- ///
- /// // Cache tool definition from first client
- /// var tools = await client1.ListToolsAsync();
- /// var toolDefinition = tools[0].ProtocolTool;
- ///
- /// // Later, reuse with a different client instance
- /// var client2 = await McpClient.CreateAsync(transport2);
- /// var reusedTool = new McpClientTool(client2, toolDefinition);
- /// var result = await reusedTool.CallAsync(new Dictionary<string, object?> { ["param"] = "value" });
- ///
- ///
public McpClientTool(
McpClient client,
Tool tool,
@@ -104,6 +76,22 @@ public McpClientTool(
_progress = null;
}
+ internal McpClientTool(
+ McpClient client,
+ Tool tool,
+ JsonSerializerOptions serializerOptions,
+ string? name = null,
+ string? description = null,
+ IProgress? progress = null)
+ {
+ _client = client;
+ ProtocolTool = tool;
+ JsonSerializerOptions = serializerOptions;
+ _name = name ?? tool.Name;
+ _description = description ?? tool.Description ?? string.Empty;
+ _progress = progress;
+ }
+
///
/// Gets the protocol type for this instance.
///
diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs
index f06f23680..3ba569883 100644
--- a/tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs
+++ b/tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs
@@ -53,8 +53,7 @@ public async Task Constructor_WithNullClient_ThrowsArgumentNullException()
Description = "Test tool"
};
- var exception = Assert.Throws(() => new McpClientTool(null!, toolDefinition));
- Assert.Equal("client", exception.ParamName);
+ Assert.Throws("client", () => new McpClientTool(null!, toolDefinition));
}
[Fact]
@@ -62,8 +61,7 @@ public async Task Constructor_WithNullTool_ThrowsArgumentNullException()
{
await using McpClient client = await CreateMcpClientForServer();
- var exception = Assert.Throws(() => new McpClientTool(client, null!));
- Assert.Equal("tool", exception.ParamName);
+ Assert.Throws("tool", () => new McpClientTool(client, null!));
}
[Fact]
From 401e5da4cde1a46663b4749156604c7917c6599d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 4 Nov 2025 02:11:56 +0000
Subject: [PATCH 4/4] Add public constructors for McpClientPrompt,
McpClientResource, and McpClientResourceTemplate
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
---
.../Client/McpClientPrompt.cs | 24 ++-
.../Client/McpClientResource.cs | 24 ++-
.../Client/McpClientResourceTemplate.cs | 24 ++-
.../Client/McpClientPromptTests.cs | 137 ++++++++++++++++++
...pClientResourceTemplateConstructorTests.cs | 103 +++++++++++++
.../Client/McpClientResourceTests.cs | 128 ++++++++++++++++
6 files changed, 437 insertions(+), 3 deletions(-)
create mode 100644 tests/ModelContextProtocol.Tests/Client/McpClientPromptTests.cs
create mode 100644 tests/ModelContextProtocol.Tests/Client/McpClientResourceTemplateConstructorTests.cs
create mode 100644 tests/ModelContextProtocol.Tests/Client/McpClientResourceTests.cs
diff --git a/src/ModelContextProtocol.Core/Client/McpClientPrompt.cs b/src/ModelContextProtocol.Core/Client/McpClientPrompt.cs
index 5a618242f..c84e0787b 100644
--- a/src/ModelContextProtocol.Core/Client/McpClientPrompt.cs
+++ b/src/ModelContextProtocol.Core/Client/McpClientPrompt.cs
@@ -22,8 +22,30 @@ public sealed class McpClientPrompt
{
private readonly McpClient _client;
- internal McpClientPrompt(McpClient client, Prompt prompt)
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The instance to use for invoking the prompt.
+ /// The protocol definition describing the prompt's metadata.
+ ///
+ ///
+ /// This constructor enables reusing cached prompt definitions across different instances
+ /// without needing to call on every reconnect. This is particularly useful
+ /// in scenarios where prompt definitions are stable and network round-trips should be minimized.
+ ///
+ ///
+ /// The provided must represent a prompt that is actually available on the server
+ /// associated with the . Attempting to invoke a prompt that doesn't exist on the
+ /// server will result in an .
+ ///
+ ///
+ /// is .
+ /// is .
+ public McpClientPrompt(McpClient client, Prompt prompt)
{
+ Throw.IfNull(client);
+ Throw.IfNull(prompt);
+
_client = client;
ProtocolPrompt = prompt;
}
diff --git a/src/ModelContextProtocol.Core/Client/McpClientResource.cs b/src/ModelContextProtocol.Core/Client/McpClientResource.cs
index 19f11bfdf..504eca088 100644
--- a/src/ModelContextProtocol.Core/Client/McpClientResource.cs
+++ b/src/ModelContextProtocol.Core/Client/McpClientResource.cs
@@ -17,8 +17,30 @@ public sealed class McpClientResource
{
private readonly McpClient _client;
- internal McpClientResource(McpClient client, Resource resource)
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The instance to use for reading the resource.
+ /// The protocol definition describing the resource's metadata.
+ ///
+ ///
+ /// This constructor enables reusing cached resource definitions across different instances
+ /// without needing to call on every reconnect. This is particularly useful
+ /// in scenarios where resource definitions are stable and network round-trips should be minimized.
+ ///
+ ///
+ /// The provided must represent a resource that is actually available on the server
+ /// associated with the . Attempting to read a resource that doesn't exist on the
+ /// server will result in an .
+ ///
+ ///
+ /// is .
+ /// is .
+ public McpClientResource(McpClient client, Resource resource)
{
+ Throw.IfNull(client);
+ Throw.IfNull(resource);
+
_client = client;
ProtocolResource = resource;
}
diff --git a/src/ModelContextProtocol.Core/Client/McpClientResourceTemplate.cs b/src/ModelContextProtocol.Core/Client/McpClientResourceTemplate.cs
index 033f7cf00..134cef570 100644
--- a/src/ModelContextProtocol.Core/Client/McpClientResourceTemplate.cs
+++ b/src/ModelContextProtocol.Core/Client/McpClientResourceTemplate.cs
@@ -17,8 +17,30 @@ public sealed class McpClientResourceTemplate
{
private readonly McpClient _client;
- internal McpClientResourceTemplate(McpClient client, ResourceTemplate resourceTemplate)
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The instance to use for reading the resource template.
+ /// The protocol definition describing the resource template's metadata.
+ ///
+ ///
+ /// This constructor enables reusing cached resource template definitions across different instances
+ /// without needing to call on every reconnect. This is particularly useful
+ /// in scenarios where resource template definitions are stable and network round-trips should be minimized.
+ ///
+ ///
+ /// The provided must represent a resource template that is actually available on the server
+ /// associated with the . Attempting to read a resource template that doesn't exist on the
+ /// server will result in an .
+ ///
+ ///
+ /// is .
+ /// is .
+ public McpClientResourceTemplate(McpClient client, ResourceTemplate resourceTemplate)
{
+ Throw.IfNull(client);
+ Throw.IfNull(resourceTemplate);
+
_client = client;
ProtocolResourceTemplate = resourceTemplate;
}
diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientPromptTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientPromptTests.cs
new file mode 100644
index 000000000..9bad26a6b
--- /dev/null
+++ b/tests/ModelContextProtocol.Tests/Client/McpClientPromptTests.cs
@@ -0,0 +1,137 @@
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.DependencyInjection;
+using ModelContextProtocol.Client;
+using ModelContextProtocol.Protocol;
+using ModelContextProtocol.Server;
+using System.ComponentModel;
+
+namespace ModelContextProtocol.Tests.Client;
+
+public class McpClientPromptTests : ClientServerTestBase
+{
+ public McpClientPromptTests(ITestOutputHelper outputHelper)
+ : base(outputHelper)
+ {
+ }
+
+ protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder)
+ {
+ mcpServerBuilder.WithPrompts();
+ }
+
+ [McpServerPromptType]
+ private sealed class GreetingPrompts
+ {
+ [McpServerPrompt, Description("Generates a greeting prompt")]
+ public static ChatMessage Greeting([Description("The name to greet")] string name) =>
+ new(ChatRole.User, $"Hello, {name}!");
+ }
+
+ [Fact]
+ public async Task Constructor_WithValidParameters_CreatesInstance()
+ {
+ await using McpClient client = await CreateMcpClientForServer();
+
+ var prompts = await client.ListPromptsAsync(cancellationToken: TestContext.Current.CancellationToken);
+ var originalPrompt = prompts.First();
+ var promptDefinition = originalPrompt.ProtocolPrompt;
+
+ var newPrompt = new McpClientPrompt(client, promptDefinition);
+
+ Assert.NotNull(newPrompt);
+ Assert.Equal("greeting", newPrompt.Name);
+ Assert.Equal("Generates a greeting prompt", newPrompt.Description);
+ Assert.Same(promptDefinition, newPrompt.ProtocolPrompt);
+ }
+
+ [Fact]
+ public void Constructor_WithNullClient_ThrowsArgumentNullException()
+ {
+ var promptDefinition = new Prompt
+ {
+ Name = "test",
+ Description = "Test prompt"
+ };
+
+ Assert.Throws("client", () => new McpClientPrompt(null!, promptDefinition));
+ }
+
+ [Fact]
+ public async Task Constructor_WithNullPrompt_ThrowsArgumentNullException()
+ {
+ await using McpClient client = await CreateMcpClientForServer();
+
+ Assert.Throws("prompt", () => new McpClientPrompt(client, null!));
+ }
+
+ [Fact]
+ public async Task ReusePromptDefinition_AcrossDifferentClients_InvokesSuccessfully()
+ {
+ Prompt promptDefinition;
+ {
+ await using McpClient client1 = await CreateMcpClientForServer();
+ var prompts = await client1.ListPromptsAsync(cancellationToken: TestContext.Current.CancellationToken);
+ var greetingPrompt = prompts.First(p => p.Name == "greeting");
+ promptDefinition = greetingPrompt.ProtocolPrompt;
+ }
+
+ await using McpClient client2 = await CreateMcpClientForServer();
+
+ var reusedPrompt = new McpClientPrompt(client2, promptDefinition);
+
+ var result = await reusedPrompt.GetAsync(
+ new Dictionary { ["name"] = "World" },
+ cancellationToken: TestContext.Current.CancellationToken);
+
+ Assert.NotNull(result);
+ Assert.NotNull(result.Messages);
+ var message = result.Messages.First();
+ Assert.NotNull(message.Content);
+ var textContent = message.Content as TextContentBlock;
+ Assert.NotNull(textContent);
+ Assert.Equal("Hello, World!", textContent.Text);
+ }
+
+ [Fact]
+ public async Task ReusePromptDefinition_PreservesPromptMetadata()
+ {
+ await using McpClient client = await CreateMcpClientForServer();
+
+ var prompts = await client.ListPromptsAsync(cancellationToken: TestContext.Current.CancellationToken);
+ var originalPrompt = prompts.First();
+ var promptDefinition = originalPrompt.ProtocolPrompt;
+
+ var reusedPrompt = new McpClientPrompt(client, promptDefinition);
+
+ Assert.Equal(originalPrompt.Name, reusedPrompt.Name);
+ Assert.Equal(originalPrompt.Description, reusedPrompt.Description);
+ Assert.Equal(originalPrompt.ProtocolPrompt.Name, reusedPrompt.ProtocolPrompt.Name);
+ Assert.Equal(originalPrompt.ProtocolPrompt.Description, reusedPrompt.ProtocolPrompt.Description);
+ }
+
+ [Fact]
+ public async Task ManuallyConstructedPrompt_CanBeInvoked()
+ {
+ await using McpClient client = await CreateMcpClientForServer();
+
+ var manualPrompt = new Prompt
+ {
+ Name = "greeting",
+ Description = "Generates a greeting prompt"
+ };
+
+ var clientPrompt = new McpClientPrompt(client, manualPrompt);
+
+ var result = await clientPrompt.GetAsync(
+ new Dictionary { ["name"] = "Test" },
+ cancellationToken: TestContext.Current.CancellationToken);
+
+ Assert.NotNull(result);
+ Assert.NotNull(result.Messages);
+ var message = result.Messages.First();
+ Assert.NotNull(message.Content);
+ var textContent = message.Content as TextContentBlock;
+ Assert.NotNull(textContent);
+ Assert.Equal("Hello, Test!", textContent.Text);
+ }
+}
diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientResourceTemplateConstructorTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientResourceTemplateConstructorTests.cs
new file mode 100644
index 000000000..c47ad69ad
--- /dev/null
+++ b/tests/ModelContextProtocol.Tests/Client/McpClientResourceTemplateConstructorTests.cs
@@ -0,0 +1,103 @@
+using Microsoft.Extensions.DependencyInjection;
+using ModelContextProtocol.Client;
+using ModelContextProtocol.Protocol;
+using ModelContextProtocol.Server;
+using System.ComponentModel;
+
+namespace ModelContextProtocol.Tests.Client;
+
+public class McpClientResourceTemplateConstructorTests : ClientServerTestBase
+{
+ public McpClientResourceTemplateConstructorTests(ITestOutputHelper outputHelper)
+ : base(outputHelper)
+ {
+ }
+
+ protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder)
+ {
+ mcpServerBuilder.WithResources();
+ }
+
+ [McpServerResourceType]
+ private sealed class FileTemplateResources
+ {
+ [McpServerResource, Description("A file template")]
+ public static string FileTemplate([Description("The file path")] string path) => $"Content for {path}";
+ }
+
+ [Fact]
+ public async Task Constructor_WithValidParameters_CreatesInstance()
+ {
+ await using McpClient client = await CreateMcpClientForServer();
+
+ var templates = await client.ListResourceTemplatesAsync(cancellationToken: TestContext.Current.CancellationToken);
+ var originalTemplate = templates.First();
+ var templateDefinition = originalTemplate.ProtocolResourceTemplate;
+
+ var newTemplate = new McpClientResourceTemplate(client, templateDefinition);
+
+ Assert.NotNull(newTemplate);
+ Assert.Equal("file_template", newTemplate.Name);
+ Assert.Equal("A file template", newTemplate.Description);
+ Assert.Same(templateDefinition, newTemplate.ProtocolResourceTemplate);
+ }
+
+ [Fact]
+ public void Constructor_WithNullClient_ThrowsArgumentNullException()
+ {
+ var templateDefinition = new ResourceTemplate
+ {
+ UriTemplate = "file:///{path}",
+ Name = "test",
+ Description = "Test template"
+ };
+
+ Assert.Throws("client", () => new McpClientResourceTemplate(null!, templateDefinition));
+ }
+
+ [Fact]
+ public async Task Constructor_WithNullResourceTemplate_ThrowsArgumentNullException()
+ {
+ await using McpClient client = await CreateMcpClientForServer();
+
+ Assert.Throws("resourceTemplate", () => new McpClientResourceTemplate(client, null!));
+ }
+
+ [Fact]
+ public async Task ReuseResourceTemplateDefinition_PreservesTemplateMetadata()
+ {
+ await using McpClient client = await CreateMcpClientForServer();
+
+ var templates = await client.ListResourceTemplatesAsync(cancellationToken: TestContext.Current.CancellationToken);
+ var originalTemplate = templates.First();
+ var templateDefinition = originalTemplate.ProtocolResourceTemplate;
+
+ var reusedTemplate = new McpClientResourceTemplate(client, templateDefinition);
+
+ Assert.Equal(originalTemplate.Name, reusedTemplate.Name);
+ Assert.Equal(originalTemplate.Description, reusedTemplate.Description);
+ Assert.Equal(originalTemplate.UriTemplate, reusedTemplate.UriTemplate);
+ Assert.Equal(originalTemplate.ProtocolResourceTemplate.Name, reusedTemplate.ProtocolResourceTemplate.Name);
+ Assert.Equal(originalTemplate.ProtocolResourceTemplate.Description, reusedTemplate.ProtocolResourceTemplate.Description);
+ }
+
+ [Fact]
+ public async Task ManuallyConstructedResourceTemplate_CreatesValidInstance()
+ {
+ await using McpClient client = await CreateMcpClientForServer();
+
+ var manualTemplate = new ResourceTemplate
+ {
+ UriTemplate = "file:///{path}",
+ Name = "file_template",
+ Description = "A file template"
+ };
+
+ var clientTemplate = new McpClientResourceTemplate(client, manualTemplate);
+
+ Assert.NotNull(clientTemplate);
+ Assert.Equal("file_template", clientTemplate.Name);
+ Assert.Equal("A file template", clientTemplate.Description);
+ Assert.Equal("file:///{path}", clientTemplate.UriTemplate);
+ }
+}
diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientResourceTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientResourceTests.cs
new file mode 100644
index 000000000..a7b7c739e
--- /dev/null
+++ b/tests/ModelContextProtocol.Tests/Client/McpClientResourceTests.cs
@@ -0,0 +1,128 @@
+using Microsoft.Extensions.DependencyInjection;
+using ModelContextProtocol.Client;
+using ModelContextProtocol.Protocol;
+using ModelContextProtocol.Server;
+using System.ComponentModel;
+
+namespace ModelContextProtocol.Tests.Client;
+
+public class McpClientResourceTests : ClientServerTestBase
+{
+ public McpClientResourceTests(ITestOutputHelper outputHelper)
+ : base(outputHelper)
+ {
+ }
+
+ protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder)
+ {
+ mcpServerBuilder.WithResources();
+ }
+
+ [McpServerResourceType]
+ private sealed class SampleResources
+ {
+ [McpServerResource, Description("A sample resource")]
+ public static string Sample() => "Sample content";
+ }
+
+ [Fact]
+ public async Task Constructor_WithValidParameters_CreatesInstance()
+ {
+ await using McpClient client = await CreateMcpClientForServer();
+
+ var resources = await client.ListResourcesAsync(cancellationToken: TestContext.Current.CancellationToken);
+ var originalResource = resources.First();
+ var resourceDefinition = originalResource.ProtocolResource;
+
+ var newResource = new McpClientResource(client, resourceDefinition);
+
+ Assert.NotNull(newResource);
+ Assert.Equal("sample", newResource.Name);
+ Assert.Equal("A sample resource", newResource.Description);
+ Assert.Same(resourceDefinition, newResource.ProtocolResource);
+ }
+
+ [Fact]
+ public void Constructor_WithNullClient_ThrowsArgumentNullException()
+ {
+ var resourceDefinition = new Resource
+ {
+ Uri = "file:///test.txt",
+ Name = "test",
+ Description = "Test resource"
+ };
+
+ Assert.Throws("client", () => new McpClientResource(null!, resourceDefinition));
+ }
+
+ [Fact]
+ public async Task Constructor_WithNullResource_ThrowsArgumentNullException()
+ {
+ await using McpClient client = await CreateMcpClientForServer();
+
+ Assert.Throws("resource", () => new McpClientResource(client, null!));
+ }
+
+ [Fact]
+ public async Task ReuseResourceDefinition_AcrossDifferentClients_ReadsSuccessfully()
+ {
+ Resource resourceDefinition;
+ {
+ await using McpClient client1 = await CreateMcpClientForServer();
+ var resources = await client1.ListResourcesAsync(cancellationToken: TestContext.Current.CancellationToken);
+ var sampleResource = resources.First(r => r.Name == "sample");
+ resourceDefinition = sampleResource.ProtocolResource;
+ }
+
+ await using McpClient client2 = await CreateMcpClientForServer();
+
+ var reusedResource = new McpClientResource(client2, resourceDefinition);
+
+ var result = await reusedResource.ReadAsync(
+ cancellationToken: TestContext.Current.CancellationToken);
+
+ Assert.NotNull(result);
+ Assert.NotNull(result.Contents);
+ var content = result.Contents.FirstOrDefault() as TextResourceContents;
+ Assert.NotNull(content);
+ Assert.Equal("Sample content", content.Text);
+ }
+
+ [Fact]
+ public async Task ReuseResourceDefinition_PreservesResourceMetadata()
+ {
+ await using McpClient client = await CreateMcpClientForServer();
+
+ var resources = await client.ListResourcesAsync(cancellationToken: TestContext.Current.CancellationToken);
+ var originalResource = resources.First();
+ var resourceDefinition = originalResource.ProtocolResource;
+
+ var reusedResource = new McpClientResource(client, resourceDefinition);
+
+ Assert.Equal(originalResource.Name, reusedResource.Name);
+ Assert.Equal(originalResource.Description, reusedResource.Description);
+ Assert.Equal(originalResource.Uri, reusedResource.Uri);
+ Assert.Equal(originalResource.ProtocolResource.Name, reusedResource.ProtocolResource.Name);
+ Assert.Equal(originalResource.ProtocolResource.Description, reusedResource.ProtocolResource.Description);
+ }
+
+ [Fact]
+ public async Task ManuallyConstructedResource_CreatesValidInstance()
+ {
+ await using McpClient client = await CreateMcpClientForServer();
+
+ var manualResource = new Resource
+ {
+ Uri = "file:///sample.txt",
+ Name = "sample",
+ Description = "A sample resource"
+ };
+
+ var clientResource = new McpClientResource(client, manualResource);
+
+ Assert.NotNull(clientResource);
+ Assert.Equal("sample", clientResource.Name);
+ Assert.Equal("A sample resource", clientResource.Description);
+ Assert.Equal("file:///sample.txt", clientResource.Uri);
+ }
+}