diff --git a/Directory.Packages.props b/Directory.Packages.props
index 656f5fe9..c058881f 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -3,7 +3,7 @@
true
9.0.10
10.0.0-rc.2.25502.107
- 9.10.1
+ 9.10.2
@@ -60,7 +60,7 @@
all
-
+
diff --git a/src/ModelContextProtocol.Core/Client/McpClientTool.cs b/src/ModelContextProtocol.Core/Client/McpClientTool.cs
index 60c2ee89..012bc813 100644
--- a/src/ModelContextProtocol.Core/Client/McpClientTool.cs
+++ b/src/ModelContextProtocol.Core/Client/McpClientTool.cs
@@ -130,6 +130,26 @@ internal McpClientTool(
AIFunctionArguments arguments, CancellationToken cancellationToken)
{
CallToolResult result = await CallAsync(arguments, _progress, JsonSerializerOptions, cancellationToken).ConfigureAwait(false);
+
+ // We want to translate the result content into AIContent, using AIContent as the exchange types, so
+ // that downstream IChatClients can specialize handling based on the content (e.g. sending image content
+ // back to the AI service as a multi-modal tool response). However, when there is additional information
+ // carried by the CallToolResult outside of its ContentBlocks, just returning AIContent from those ContentBlocks
+ // would lose that information. So, we only do the translation if there is no additional information to preserve.
+ if (result.IsError is not true &&
+ result.StructuredContent is null &&
+ result.Meta is not { Count: > 0 })
+ {
+ switch (result.Content.Count)
+ {
+ case 1 when result.Content[0].ToAIContent() is { } aiContent:
+ return aiContent;
+
+ case > 1 when result.Content.Select(c => c.ToAIContent()).ToArray() is { } aiContents && aiContents.All(static c => c is not null):
+ return aiContents;
+ }
+ }
+
return JsonSerializer.SerializeToElement(result, McpJsonUtilities.JsonContext.Default.CallToolResult);
}
diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs
index 3ba56988..45d8a467 100644
--- a/tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs
+++ b/tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs
@@ -1,8 +1,11 @@
+using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
+using System.Text;
using System.Text.Json;
+using System.Text.Json.Nodes;
namespace ModelContextProtocol.Tests.Client;
@@ -15,295 +18,475 @@ public McpClientToolTests(ITestOutputHelper 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" })]);
+ mcpServerBuilder.WithTools();
+ }
+
+ private class TestTools
+ {
+ // Tool that returns only text content
+ [McpServerTool]
+ public static TextContentBlock TextOnlyTool() =>
+ new TextContentBlock { Text = "Simple text result" };
+
+ // Tool that returns only text content (string)
+ [McpServerTool]
+ public static string StringTool() => "Simple string result";
+
+ // Tool that returns image content as single ContentBlock
+ [McpServerTool]
+ public static ImageContentBlock ImageTool() =>
+ new ImageContentBlock { Data = Convert.ToBase64String(Encoding.UTF8.GetBytes("fake-image-data")), MimeType = "image/png" };
+
+ // Tool that returns audio content as single ContentBlock
+ [McpServerTool]
+ public static AudioContentBlock AudioTool() =>
+ new AudioContentBlock { Data = Convert.ToBase64String(Encoding.UTF8.GetBytes("fake-audio-data")), MimeType = "audio/mp3" };
+
+ // Tool that returns embedded resource
+ [McpServerTool]
+ public static EmbeddedResourceBlock EmbeddedResourceTool() =>
+ new EmbeddedResourceBlock { Resource = new TextResourceContents { Uri = "resource-uri", Text = "Resource text content", MimeType = "text/plain" } };
+
+ // Tool that returns mixed content (text + image) using IEnumerable
+ [McpServerTool]
+ public static IEnumerable MixedContentTool()
+ {
+ yield return new TextContent("Description of the image");
+ yield return new DataContent(Encoding.UTF8.GetBytes("fake-image-data"), "image/png");
+ }
+
+ // Tool that returns multiple images using IEnumerable
+ [McpServerTool]
+ public static IEnumerable MultipleImagesTool()
+ {
+ yield return new DataContent(Encoding.UTF8.GetBytes("image1"), "image/png");
+ yield return new DataContent(Encoding.UTF8.GetBytes("image2"), "image/jpeg");
+ }
+
+ // Tool that returns audio + text using IEnumerable
+ [McpServerTool]
+ public static IEnumerable AudioWithTextTool()
+ {
+ yield return new TextContent("Audio transcription");
+ yield return new DataContent(Encoding.UTF8.GetBytes("fake-audio"), "audio/wav");
+ }
+
+ // Tool that returns embedded resource + text using IEnumerable
+ [McpServerTool]
+ public static IEnumerable ResourceWithTextTool()
+ {
+ yield return new TextContentBlock { Text = "Resource description" };
+ yield return new EmbeddedResourceBlock { Resource = new TextResourceContents { Uri = "file://test.txt", Text = "File content", MimeType = "text/plain" } };
+ }
+
+ // Tool that returns all content types using IEnumerable
+ [McpServerTool]
+ public static IEnumerable AllContentTypesTool()
+ {
+ yield return new TextContent("Mixed content");
+ yield return new DataContent(Encoding.UTF8.GetBytes("image"), "image/png");
+ yield return new DataContent(Encoding.UTF8.GetBytes("audio"), "audio/mp3");
+ yield return new DataContent(Encoding.UTF8.GetBytes("blob"), "application/octet-stream");
+ }
+
+ // Tool that returns content that can't be converted to AIContent (ResourceLinkBlock)
+ [McpServerTool]
+ public static ResourceLinkBlock ResourceLinkTool() =>
+ new ResourceLinkBlock { Uri = "file://test.txt", Name = "test.txt" };
+
+ // Tool that returns mixed content where some can't be converted (ResourceLinkBlock + Image)
+ [McpServerTool]
+ public static IEnumerable MixedWithNonConvertibleTool()
+ {
+ yield return new ImageContentBlock { Data = Convert.ToBase64String(Encoding.UTF8.GetBytes("image-data")), MimeType = "image/png" };
+ yield return new ResourceLinkBlock { Uri = "file://linked.txt", Name = "linked.txt" };
+ }
+
+ // Tool that returns CallToolResult with IsError = true
+ [McpServerTool]
+ public static CallToolResult ErrorTool() =>
+ new CallToolResult
+ {
+ IsError = true,
+ Content = [new TextContentBlock { Text = "Error message" }]
+ };
+
+ // Tool that returns CallToolResult with StructuredContent
+ [McpServerTool]
+ public static CallToolResult StructuredContentTool() =>
+ new CallToolResult
+ {
+ Content = [new TextContentBlock { Text = "Regular content" }],
+ StructuredContent = JsonNode.Parse("{\"key\":\"value\"}")
+ };
+
+ // Tool that returns CallToolResult with Meta
+ [McpServerTool]
+ public static CallToolResult MetaTool() =>
+ new CallToolResult
+ {
+ Content = [new TextContentBlock { Text = "Content with meta" }],
+ Meta = new JsonObject { ["customKey"] = "customValue" }
+ };
+
+ // Tool that returns CallToolResult with multiple properties (IsError + Meta)
+ [McpServerTool]
+ public static CallToolResult ErrorWithMetaTool() =>
+ new CallToolResult
+ {
+ IsError = true,
+ Content = [new TextContentBlock { Text = "Error with metadata" }],
+ Meta = new JsonObject { ["errorCode"] = 500 }
+ };
+
+ // Tool that returns binary resource (non-text)
+ [McpServerTool]
+ public static EmbeddedResourceBlock BinaryResourceTool() =>
+ new EmbeddedResourceBlock
+ {
+ Resource = new BlobResourceContents
+ {
+ Uri = "data://blob",
+ Blob = Convert.ToBase64String(Encoding.UTF8.GetBytes("binary-data")),
+ MimeType = "application/octet-stream"
+ }
+ };
}
[Fact]
- public async Task Constructor_WithValidParameters_CreatesInstance()
+ public async Task TextOnlyTool_ReturnsSingleTextContent()
{
+ // Arrange
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;
+ var tool = tools.Single(t => t.Name == "text_only_tool");
- // Create a new McpClientTool using the public constructor
- var newTool = new McpClientTool(client, toolDefinition);
+ // Act
+ var result = await tool.InvokeAsync(cancellationToken: TestContext.Current.CancellationToken);
- Assert.NotNull(newTool);
- Assert.Equal("echo", newTool.Name);
- Assert.Equal("Echoes back the message", newTool.Description);
- Assert.Same(toolDefinition, newTool.ProtocolTool);
+ // Assert - single text content should return TextContent
+ var textContent = Assert.IsType(result);
+ Assert.Equal("Simple text result", textContent.Text);
}
[Fact]
- public async Task Constructor_WithNullClient_ThrowsArgumentNullException()
+ public async Task StringTool_ReturnsSingleTextContent()
{
- var toolDefinition = new Tool
- {
- Name = "test",
- Description = "Test tool"
- };
+ await using McpClient client = await CreateMcpClientForServer();
+ var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
+ var tool = tools.Single(t => t.Name == "string_tool");
- Assert.Throws("client", () => new McpClientTool(null!, toolDefinition));
+ var result = await tool.InvokeAsync(cancellationToken: TestContext.Current.CancellationToken);
+
+ var textContent = Assert.IsType(result);
+ Assert.Equal("Simple string result", textContent.Text);
}
[Fact]
- public async Task Constructor_WithNullTool_ThrowsArgumentNullException()
+ public async Task ImageTool_ReturnsSingleDataContent()
{
await using McpClient client = await CreateMcpClientForServer();
+ var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
+ var tool = tools.Single(t => t.Name == "image_tool");
- Assert.Throws("tool", () => new McpClientTool(client, null!));
+ var result = await tool.InvokeAsync(cancellationToken: TestContext.Current.CancellationToken);
+
+ var dataContent = Assert.IsType(result);
+ Assert.Equal("image/png", dataContent.MediaType);
+ Assert.Equal("fake-image-data", Encoding.UTF8.GetString(dataContent.Data.ToArray()));
}
[Fact]
- public async Task Constructor_WithNullSerializerOptions_UsesDefaultOptions()
+ public async Task AudioTool_ReturnsSingleDataContent()
{
await using McpClient client = await CreateMcpClientForServer();
+ var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
+ var tool = tools.Single(t => t.Name == "audio_tool");
- var toolDefinition = new Tool
- {
- Name = "test",
- Description = "Test tool"
- };
+ var result = await tool.InvokeAsync(cancellationToken: TestContext.Current.CancellationToken);
+
+ var dataContent = Assert.IsType(result);
+ Assert.Equal("audio/mp3", dataContent.MediaType);
+ Assert.Equal("fake-audio-data", Encoding.UTF8.GetString(dataContent.Data.ToArray()));
+ }
+
+ [Fact]
+ public async Task EmbeddedResourceTool_ReturnsSingleTextContent()
+ {
+ await using McpClient client = await CreateMcpClientForServer();
+ var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
+ var tool = tools.Single(t => t.Name == "embedded_resource_tool");
- var tool = new McpClientTool(client, toolDefinition, serializerOptions: null);
+ var result = await tool.InvokeAsync(cancellationToken: TestContext.Current.CancellationToken);
- Assert.NotNull(tool.JsonSerializerOptions);
- Assert.Same(McpJsonUtilities.DefaultOptions, tool.JsonSerializerOptions);
+ var textContent = Assert.IsType(result);
+ Assert.Equal("Resource text content", textContent.Text);
}
[Fact]
- public async Task Constructor_WithCustomSerializerOptions_UsesProvidedOptions()
+ public async Task MixedContentTool_ReturnsAIContentArray()
{
await using McpClient client = await CreateMcpClientForServer();
+ var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
+ var tool = tools.Single(t => t.Name == "mixed_content_tool");
- var toolDefinition = new Tool
- {
- Name = "test",
- Description = "Test tool"
- };
+ var result = await tool.InvokeAsync(cancellationToken: TestContext.Current.CancellationToken);
+
+ var aiContents = Assert.IsType(result);
+ Assert.Equal(2, aiContents.Length);
+
+ var textContent = Assert.IsType(aiContents[0]);
+ Assert.Equal("Description of the image", textContent.Text);
+
+ var dataContent = Assert.IsType(aiContents[1]);
+ Assert.Equal("image/png", dataContent.MediaType);
+ }
- var customOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);
+ [Fact]
+ public async Task MultipleImagesTool_ReturnsAIContentArray()
+ {
+ await using McpClient client = await CreateMcpClientForServer();
+ var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
+ var tool = tools.Single(t => t.Name == "multiple_images_tool");
- var tool = new McpClientTool(client, toolDefinition, customOptions);
+ var result = await tool.InvokeAsync(cancellationToken: TestContext.Current.CancellationToken);
- Assert.NotNull(tool.JsonSerializerOptions);
- Assert.Same(customOptions, tool.JsonSerializerOptions);
+ var aiContents = Assert.IsType(result);
+ Assert.Equal(2, aiContents.Length);
+
+ var dataContent0 = Assert.IsType(aiContents[0]);
+ Assert.Equal("image/png", dataContent0.MediaType);
+ Assert.Equal("image1", Encoding.UTF8.GetString(dataContent0.Data.ToArray()));
+
+ var dataContent1 = Assert.IsType(aiContents[1]);
+ Assert.Equal("image/jpeg", dataContent1.MediaType);
+ Assert.Equal("image2", Encoding.UTF8.GetString(dataContent1.Data.ToArray()));
}
[Fact]
- public async Task ReuseToolDefinition_AcrossDifferentClients_InvokesSuccessfully()
+ public async Task AudioWithTextTool_ReturnsAIContentArray()
{
- // 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;
- }
+ await using McpClient client = await CreateMcpClientForServer();
+ var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
+ var tool = tools.Single(t => t.Name == "audio_with_text_tool");
- // Create second client (simulating reconnect)
- await using McpClient client2 = await CreateMcpClientForServer();
+ var result = await tool.InvokeAsync(cancellationToken: TestContext.Current.CancellationToken);
- // Create new McpClientTool with cached tool definition and new client
- var reusedTool = new McpClientTool(client2, toolDefinition);
+ var aiContents = Assert.IsType(result);
+ Assert.Equal(2, aiContents.Length);
+
+ var textContent = Assert.IsType(aiContents[0]);
+ Assert.Equal("Audio transcription", textContent.Text);
+
+ var dataContent = Assert.IsType(aiContents[1]);
+ Assert.Equal("audio/wav", dataContent.MediaType);
+ }
- // Invoke the tool using the new client
- var result = await reusedTool.CallAsync(
- new Dictionary { ["message"] = "Hello from reused tool" },
- cancellationToken: TestContext.Current.CancellationToken);
+ [Fact]
+ public async Task ResourceWithTextTool_ReturnsAIContentArray()
+ {
+ await using McpClient client = await CreateMcpClientForServer();
+ var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
+ var tool = tools.Single(t => t.Name == "resource_with_text_tool");
+
+ var result = await tool.InvokeAsync(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);
+ var aiContents = Assert.IsType(result);
+ Assert.Equal(2, aiContents.Length);
+
+ var textContent0 = Assert.IsType(aiContents[0]);
+ Assert.Equal("Resource description", textContent0.Text);
+
+ var textContent1 = Assert.IsType(aiContents[1]);
+ Assert.Equal("File content", textContent1.Text);
}
[Fact]
- public async Task ReuseToolDefinition_WithComplexParameters_InvokesSuccessfully()
+ public async Task AllContentTypesTool_ReturnsAIContentArray()
{
- // 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;
- }
+ await using McpClient client = await CreateMcpClientForServer();
+ var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
+ var tool = tools.Single(t => t.Name == "all_content_types_tool");
- // Create second client
- await using McpClient client2 = await CreateMcpClientForServer();
+ var result = await tool.InvokeAsync(cancellationToken: TestContext.Current.CancellationToken);
- // Create new McpClientTool with cached tool definition
- var reusedTool = new McpClientTool(client2, toolDefinition);
+ var aiContents = Assert.IsType(result);
+ Assert.Equal(4, aiContents.Length);
+
+ var textContent = Assert.IsType(aiContents[0]);
+ Assert.Equal("Mixed content", textContent.Text);
+
+ var dataContent1 = Assert.IsType(aiContents[1]);
+ Assert.Equal("image/png", dataContent1.MediaType);
+
+ var dataContent2 = Assert.IsType(aiContents[2]);
+ Assert.Equal("audio/mp3", dataContent2.MediaType);
+
+ var dataContent3 = Assert.IsType(aiContents[3]);
+ Assert.Equal("application/octet-stream", dataContent3.MediaType);
+ }
+
+ [Fact]
+ public async Task SingleAIContent_PreservesRawRepresentation()
+ {
+ await using McpClient client = await CreateMcpClientForServer();
+ var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
+ var tool = tools.Single(t => t.Name == "image_tool");
- // Invoke the tool with integer parameters
- var result = await reusedTool.CallAsync(
- new Dictionary { ["a"] = 5, ["b"] = 7 },
- cancellationToken: TestContext.Current.CancellationToken);
+ var result = await tool.InvokeAsync(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);
+ var dataContent = Assert.IsType(result);
+ Assert.NotNull(dataContent.RawRepresentation);
+ var imageBlock = Assert.IsType(dataContent.RawRepresentation);
+ Assert.Equal("image/png", imageBlock.MimeType);
}
[Fact]
- public async Task ReuseToolDefinition_PreservesToolMetadata()
+ public async Task ResourceLinkTool_ReturnsJsonElement()
{
await using McpClient client = await CreateMcpClientForServer();
+ var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
+ var tool = tools.Single(t => t.Name == "resource_link_tool");
+
+ var result = await tool.InvokeAsync(cancellationToken: TestContext.Current.CancellationToken);
+
+ Assert.IsType(result);
+ var jsonElement = (JsonElement)result!;
+ Assert.True(jsonElement.TryGetProperty("content", out var contentArray));
+ Assert.Equal(JsonValueKind.Array, contentArray.ValueKind);
+ Assert.Equal(1, contentArray.GetArrayLength());
+ var firstContent = contentArray[0];
+ Assert.True(firstContent.TryGetProperty("type", out var typeProperty));
+ Assert.Equal("resource_link", typeProperty.GetString());
+ }
+
+ [Fact]
+ public async Task MixedWithNonConvertibleTool_ReturnsJsonElement()
+ {
+ 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;
+ var tool = tools.Single(t => t.Name == "mixed_with_non_convertible_tool");
- // Create new McpClientTool with cached tool definition
- var reusedTool = new McpClientTool(client, toolDefinition);
+ var result = await tool.InvokeAsync(cancellationToken: TestContext.Current.CancellationToken);
- // 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);
+ var jsonElement = Assert.IsType(result);
+ Assert.True(jsonElement.TryGetProperty("content", out var contentArray));
+ Assert.Equal(JsonValueKind.Array, contentArray.ValueKind);
+ Assert.Equal(2, contentArray.GetArrayLength());
+
+ var firstContent = contentArray[0];
+ Assert.True(firstContent.TryGetProperty("type", out var type1));
+ Assert.Equal("image", type1.GetString());
- // Verify JSON schema is preserved
- Assert.Equal(
- JsonSerializer.Serialize(originalTool.JsonSchema, McpJsonUtilities.DefaultOptions),
- JsonSerializer.Serialize(reusedTool.JsonSchema, McpJsonUtilities.DefaultOptions));
+ var secondContent = contentArray[1];
+ Assert.True(secondContent.TryGetProperty("type", out var type2));
+ Assert.Equal("resource_link", type2.GetString());
}
[Fact]
- public async Task ManuallyConstructedTool_CanBeInvoked()
+ public async Task ErrorTool_ReturnsJsonElement()
{
await using McpClient client = await CreateMcpClientForServer();
+ var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
+ var tool = tools.Single(t => t.Name == "error_tool");
- // 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);
+ var result = await tool.InvokeAsync(cancellationToken: TestContext.Current.CancellationToken);
+
+ Assert.IsType(result);
+ var jsonElement = (JsonElement)result!;
+ Assert.True(jsonElement.TryGetProperty("isError", out var isError));
+ Assert.True(isError.GetBoolean());
+ Assert.True(jsonElement.TryGetProperty("content", out var content));
+ Assert.Equal(JsonValueKind.Array, content.ValueKind);
}
[Fact]
- public async Task ReuseToolDefinition_WithInvokeAsync_WorksCorrectly()
+ public async Task StructuredContentTool_ReturnsJsonElement()
{
- 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 client = await CreateMcpClientForServer();
+ var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
+ var tool = tools.Single(t => t.Name == "structured_content_tool");
- await using McpClient client2 = await CreateMcpClientForServer();
- var reusedTool = new McpClientTool(client2, toolDefinition);
+ var result = await tool.InvokeAsync(cancellationToken: TestContext.Current.CancellationToken);
- // Use AIFunction.InvokeAsync (inherited method)
- var result = await reusedTool.InvokeAsync(
- new() { ["a"] = 10, ["b"] = 20 },
- TestContext.Current.CancellationToken);
+ var jsonElement = Assert.IsType(result);
+ Assert.True(jsonElement.TryGetProperty("structuredContent", out var structuredContent));
+ Assert.True(structuredContent.TryGetProperty("key", out var key));
+ Assert.Equal("value", key.GetString());
+ Assert.True(jsonElement.TryGetProperty("content", out var content));
+ Assert.Equal(JsonValueKind.Array, content.ValueKind);
+ }
+
+ [Fact]
+ public async Task MetaTool_ReturnsJsonElement()
+ {
+ await using McpClient client = await CreateMcpClientForServer();
+ var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
+ var tool = tools.Single(t => t.Name == "meta_tool");
+
+ var result = await tool.InvokeAsync(cancellationToken: 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);
+ Assert.True(jsonElement.TryGetProperty("_meta", out var meta));
+ Assert.True(meta.TryGetProperty("customKey", out var customKey));
+ Assert.Equal("customValue", customKey.GetString());
+ Assert.True(jsonElement.TryGetProperty("content", out var content));
+ Assert.Equal(JsonValueKind.Array, content.ValueKind);
}
[Fact]
- public async Task Constructor_WithToolWithoutDescription_UsesEmptyDescription()
+ public async Task ErrorWithMetaTool_ReturnsJsonElement()
{
await using McpClient client = await CreateMcpClientForServer();
+ var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
+ var tool = tools.Single(t => t.Name == "error_with_meta_tool");
+
+ var result = await tool.InvokeAsync(cancellationToken: TestContext.Current.CancellationToken);
+
+ Assert.IsType(result);
+ var jsonElement = (JsonElement)result!;
+ Assert.True(jsonElement.TryGetProperty("isError", out var isError));
+ Assert.True(isError.GetBoolean());
+ Assert.True(jsonElement.TryGetProperty("_meta", out var meta));
+ Assert.True(meta.TryGetProperty("errorCode", out var errorCode));
+ Assert.Equal(500, errorCode.GetInt32());
+ Assert.True(jsonElement.TryGetProperty("content", out var content));
+ Assert.Equal(JsonValueKind.Array, content.ValueKind);
+ }
- var toolWithoutDescription = new Tool
- {
- Name = "noDescTool",
- Description = null
- };
+ [Fact]
+ public async Task BinaryResourceTool_ReturnsSingleDataContent()
+ {
+ await using McpClient client = await CreateMcpClientForServer();
+ var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
+ var tool = tools.Single(t => t.Name == "binary_resource_tool");
- var clientTool = new McpClientTool(client, toolWithoutDescription);
+ var result = await tool.InvokeAsync(cancellationToken: TestContext.Current.CancellationToken);
- Assert.Equal("noDescTool", clientTool.Name);
- Assert.Equal(string.Empty, clientTool.Description);
+ var dataContent = Assert.IsType(result);
+ Assert.Equal("application/octet-stream", dataContent.MediaType);
+ Assert.Equal("binary-data", Encoding.UTF8.GetString(dataContent.Data.ToArray()));
}
[Fact]
- public async Task ReuseToolDefinition_MultipleClients_AllWorkIndependently()
+ public async Task MultipleAIContent_PreservesRawRepresentation()
{
- // 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;
- }
+ await using McpClient client = await CreateMcpClientForServer();
+ var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
+ var tool = tools.Single(t => t.Name == "mixed_content_tool");
- // 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;
- }
+ var result = await tool.InvokeAsync(cancellationToken: TestContext.Current.CancellationToken);
- // 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;
- }
+ var aiContents = Assert.IsType(result);
+ Assert.Equal(2, aiContents.Length);
+
+ var textContent = Assert.IsType(aiContents[0]);
+ Assert.NotNull(textContent.RawRepresentation);
+ Assert.IsType(textContent.RawRepresentation);
- // Verify both worked
- Assert.Equal("Echo: From client 2", text2);
- Assert.Equal("Echo: From client 3", text3);
+ var dataContent = Assert.IsType(aiContents[1]);
+ Assert.NotNull(dataContent.RawRepresentation);
+ Assert.IsType(dataContent.RawRepresentation);
}
}