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/src/ModelContextProtocol.Core/Client/McpClientTool.cs b/src/ModelContextProtocol.Core/Client/McpClientTool.cs
index c7af513ef..60c2ee89d 100644
--- a/src/ModelContextProtocol.Core/Client/McpClientTool.cs
+++ b/src/ModelContextProtocol.Core/Client/McpClientTool.cs
@@ -37,6 +37,45 @@ public sealed class McpClientTool : AIFunction
private readonly string _description;
private readonly IProgress? _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 .
+ 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;
+ }
+
internal McpClientTool(
McpClient client,
Tool tool,
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);
+ }
+}
diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs
new file mode 100644
index 000000000..3ba569883
--- /dev/null
+++ b/tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs
@@ -0,0 +1,309 @@
+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"
+ };
+
+ Assert.Throws("client", () => new McpClientTool(null!, toolDefinition));
+ }
+
+ [Fact]
+ public async Task Constructor_WithNullTool_ThrowsArgumentNullException()
+ {
+ await using McpClient client = await CreateMcpClientForServer();
+
+ Assert.Throws("tool", () => new McpClientTool(client, null!));
+ }
+
+ [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);
+ }
+}