diff --git a/src/ModelContextProtocol.Core/AIContentExtensions.cs b/src/ModelContextProtocol.Core/AIContentExtensions.cs index b1ba32bf..cb2afeab 100644 --- a/src/ModelContextProtocol.Core/AIContentExtensions.cs +++ b/src/ModelContextProtocol.Core/AIContentExtensions.cs @@ -23,6 +23,7 @@ public static class AIContentExtensions /// satisfy sampling requests using the specified . /// /// The with which to satisfy sampling requests. + /// The to use for serializing user-provided objects. If , is used. /// The created handler delegate that can be assigned to . /// /// @@ -36,15 +37,18 @@ public static class AIContentExtensions /// /// is . public static Func, CancellationToken, ValueTask> CreateSamplingHandler( - this IChatClient chatClient) + this IChatClient chatClient, + JsonSerializerOptions? serializerOptions = null) { Throw.IfNull(chatClient); + serializerOptions ??= McpJsonUtilities.DefaultOptions; + return async (requestParams, progress, cancellationToken) => { Throw.IfNull(requestParams); - var (messages, options) = ToChatClientArguments(requestParams); + var (messages, options) = ToChatClientArguments(requestParams, serializerOptions); var progressToken = requestParams.ProgressToken; List updates = []; @@ -75,12 +79,12 @@ public static class AIContentExtensions chatResponse.FinishReason == ChatFinishReason.Length ? CreateMessageResult.StopReasonMaxTokens : chatResponse.FinishReason == ChatFinishReason.ToolCalls ? CreateMessageResult.StopReasonToolUse : chatResponse.FinishReason.ToString(), - Meta = chatResponse.AdditionalProperties?.ToJsonObject(), + Meta = chatResponse.AdditionalProperties?.ToJsonObject(serializerOptions), Role = lastMessage?.Role == ChatRole.User ? Role.User : Role.Assistant, Content = contents, }; - static (IList Messages, ChatOptions? Options) ToChatClientArguments(CreateMessageRequestParams requestParams) + static (IList Messages, ChatOptions? Options) ToChatClientArguments(CreateMessageRequestParams requestParams, JsonSerializerOptions serializerOptions) { ChatOptions? options = null; @@ -126,7 +130,7 @@ public static class AIContentExtensions List messages = []; foreach (var sm in requestParams.Messages) { - if (sm.Content?.Select(b => b.ToAIContent()).OfType().ToList() is { Count: > 0 } aiContents) + if (sm.Content?.Select(b => b.ToAIContent(serializerOptions)).OfType().ToList() is { Count: > 0 } aiContents) { messages.Add(new ChatMessage(sm.Role is Role.Assistant ? ChatRole.Assistant : ChatRole.User, aiContents)); } @@ -138,8 +142,10 @@ public static class AIContentExtensions } /// Converts the specified dictionary to a . - internal static JsonObject? ToJsonObject(this IReadOnlyDictionary properties) => - JsonSerializer.SerializeToNode(properties, McpJsonUtilities.JsonContext.Default.IReadOnlyDictionaryStringObject) as JsonObject; + internal static JsonObject? ToJsonObject(this IReadOnlyDictionary properties, JsonSerializerOptions options) + { + return JsonSerializer.SerializeToNode(properties, options.GetTypeInfo(typeof(IReadOnlyDictionary))) as JsonObject; + } internal static AdditionalPropertiesDictionary ToAdditionalProperties(this JsonObject obj) { @@ -156,17 +162,18 @@ internal static AdditionalPropertiesDictionary ToAdditionalProperties(this JsonO /// Converts a to a object. /// /// The prompt message to convert. + /// The to use for deserialization. If , is used. /// A object created from the prompt message. /// /// This method transforms a protocol-specific from the Model Context Protocol /// into a standard object that can be used with AI client libraries. /// /// is . - public static ChatMessage ToChatMessage(this PromptMessage promptMessage) + public static ChatMessage ToChatMessage(this PromptMessage promptMessage, JsonSerializerOptions? options = null) { Throw.IfNull(promptMessage); - AIContent? content = ToAIContent(promptMessage.Content); + AIContent? content = promptMessage.Content.ToAIContent(options); return new() { @@ -181,6 +188,7 @@ public static ChatMessage ToChatMessage(this PromptMessage promptMessage) /// /// The tool result to convert. /// The identifier for the function call request that triggered the tool invocation. + /// The to use for serialization. If , is used. /// A object created from the tool result. /// /// This method transforms a protocol-specific from the Model Context Protocol @@ -189,12 +197,14 @@ public static ChatMessage ToChatMessage(this PromptMessage promptMessage) /// serialized . /// /// or is . - public static ChatMessage ToChatMessage(this CallToolResult result, string callId) + public static ChatMessage ToChatMessage(this CallToolResult result, string callId, JsonSerializerOptions? options = null) { Throw.IfNull(result); Throw.IfNull(callId); - return new(ChatRole.Tool, [new FunctionResultContent(callId, JsonSerializer.SerializeToElement(result, McpJsonUtilities.JsonContext.Default.CallToolResult)) + options ??= McpJsonUtilities.DefaultOptions; + + return new(ChatRole.Tool, [new FunctionResultContent(callId, JsonSerializer.SerializeToElement(result, options.GetTypeInfo())) { RawRepresentation = result, }]); @@ -248,6 +258,7 @@ public static IList ToPromptMessages(this ChatMessage chatMessage /// Creates a new from the content of a . /// The to convert. + /// The to use for deserialization. If , is used. /// /// The created . If the content can't be converted (such as when it's a resource link), is returned. /// @@ -256,10 +267,12 @@ public static IList ToPromptMessages(this ChatMessage chatMessage /// content types, enabling seamless integration between the protocol and AI client libraries. /// /// is . - public static AIContent? ToAIContent(this ContentBlock content) + public static AIContent? ToAIContent(this ContentBlock content, JsonSerializerOptions? options = null) { Throw.IfNull(content); + options ??= McpJsonUtilities.DefaultOptions; + AIContent? ac = content switch { TextContentBlock textContent => new TextContent(textContent.Text), @@ -271,11 +284,11 @@ public static IList ToPromptMessages(this ChatMessage chatMessage EmbeddedResourceBlock resourceContent => resourceContent.Resource.ToAIContent(), ToolUseContentBlock toolUse => FunctionCallContent.CreateFromParsedArguments(toolUse.Input, toolUse.Id, toolUse.Name, - static json => JsonSerializer.Deserialize(json, McpJsonUtilities.JsonContext.Default.IDictionaryStringObject)), + json => JsonSerializer.Deserialize(json, options.GetTypeInfo>())), ToolResultContentBlock toolResult => new FunctionResultContent( toolResult.ToolUseId, - toolResult.Content.Count == 1 ? toolResult.Content[0].ToAIContent() : toolResult.Content.Select(c => c.ToAIContent()).OfType().ToList()) + toolResult.Content.Count == 1 ? toolResult.Content[0].ToAIContent(options) : toolResult.Content.Select(c => c.ToAIContent(options)).OfType().ToList()) { Exception = toolResult.IsError is true ? new() : null, }, @@ -320,6 +333,7 @@ public static AIContent ToAIContent(this ResourceContents content) /// Creates a list of from a sequence of . /// The instances to convert. + /// The to use for deserialization. If , is used. /// The created instances. /// /// @@ -328,16 +342,16 @@ public static AIContent ToAIContent(this ResourceContents content) /// when processing the contents of a message or response. /// /// - /// Each object is converted using , + /// Each object is converted using , /// preserving the type-specific conversion logic for text, images, audio, and resources. /// /// /// is . - public static IList ToAIContents(this IEnumerable contents) + public static IList ToAIContents(this IEnumerable contents, JsonSerializerOptions? options = null) { Throw.IfNull(contents); - return [.. contents.Select(ToAIContent).OfType()]; + return [.. contents.Select(c => c.ToAIContent(options)).OfType()]; } /// Creates a list of from a sequence of . @@ -365,12 +379,15 @@ public static IList ToAIContents(this IEnumerable c /// Creates a new from the content of an . /// The to convert. + /// The to use for serialization. If , is used. /// The created . /// is . - public static ContentBlock ToContentBlock(this AIContent content) + public static ContentBlock ToContentBlock(this AIContent content, JsonSerializerOptions? options = null) { Throw.IfNull(content); + options ??= McpJsonUtilities.DefaultOptions; + ContentBlock contentBlock = content switch { TextContent textContent => new TextContentBlock @@ -404,7 +421,7 @@ public static ContentBlock ToContentBlock(this AIContent content) { Id = callContent.CallId, Name = callContent.Name, - Input = JsonSerializer.SerializeToElement(callContent.Arguments, McpJsonUtilities.DefaultOptions.GetTypeInfo>()!), + Input = JsonSerializer.SerializeToElement(callContent.Arguments, options.GetTypeInfo>()!), }, FunctionResultContent resultContent => new ToolResultContentBlock() @@ -412,19 +429,19 @@ public static ContentBlock ToContentBlock(this AIContent content) ToolUseId = resultContent.CallId, IsError = resultContent.Exception is not null, Content = - resultContent.Result is AIContent c ? [c.ToContentBlock()] : - resultContent.Result is IEnumerable ec ? [.. ec.Select(c => c.ToContentBlock())] : - [new TextContentBlock { Text = JsonSerializer.Serialize(content, McpJsonUtilities.DefaultOptions.GetTypeInfo()) }], + resultContent.Result is AIContent c ? [c.ToContentBlock(options)] : + resultContent.Result is IEnumerable ec ? [.. ec.Select(c => c.ToContentBlock(options))] : + [new TextContentBlock { Text = JsonSerializer.Serialize(content, options.GetTypeInfo()) }], StructuredContent = resultContent.Result is JsonElement je ? je : null, }, _ => new TextContentBlock { - Text = JsonSerializer.Serialize(content, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object))), + Text = JsonSerializer.Serialize(content, options.GetTypeInfo(typeof(object))), } }; - contentBlock.Meta = content.AdditionalProperties?.ToJsonObject(); + contentBlock.Meta = content.AdditionalProperties?.ToJsonObject(options); return contentBlock; } diff --git a/src/ModelContextProtocol.Core/Client/McpClientTool.cs b/src/ModelContextProtocol.Core/Client/McpClientTool.cs index f4dc060d..e03c347e 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientTool.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientTool.cs @@ -139,10 +139,10 @@ result.StructuredContent is null && { switch (result.Content.Count) { - case 1 when result.Content[0].ToAIContent() is { } aiContent: + case 1 when result.Content[0].ToAIContent(JsonSerializerOptions) is { } aiContent: return aiContent; - case > 1 when result.Content.Select(c => c.ToAIContent()).ToArray() is { } aiContents && aiContents.All(static c => c is not null): + case > 1 when result.Content.Select(c => c.ToAIContent(JsonSerializerOptions)).ToArray() is { } aiContents && aiContents.All(static c => c is not null): return aiContents; } } diff --git a/src/ModelContextProtocol.Core/Protocol/PromptMessage.cs b/src/ModelContextProtocol.Core/Protocol/PromptMessage.cs index 76dede3d..ece4fb3e 100644 --- a/src/ModelContextProtocol.Core/Protocol/PromptMessage.cs +++ b/src/ModelContextProtocol.Core/Protocol/PromptMessage.cs @@ -20,7 +20,7 @@ namespace ModelContextProtocol.Protocol; /// /// objects are typically used in collections within /// to represent complete conversations or prompt sequences. They can be converted to and from -/// objects using the extension methods and +/// objects using the extension methods and /// . /// /// diff --git a/src/ModelContextProtocol.Core/Server/McpServer.Methods.cs b/src/ModelContextProtocol.Core/Server/McpServer.Methods.cs index 7caabf68..490c5ba2 100644 --- a/src/ModelContextProtocol.Core/Server/McpServer.Methods.cs +++ b/src/ModelContextProtocol.Core/Server/McpServer.Methods.cs @@ -73,16 +73,19 @@ public ValueTask SampleAsync( /// /// The messages to send as part of the request. /// The options to use for the request, including model parameters and constraints. + /// The to use for serializing user-provided objects. If , is used. /// The to monitor for cancellation requests. The default is . /// A task containing the chat response from the model. /// is . /// The client does not support sampling. /// The request failed or the client returned an error response. public async Task SampleAsync( - IEnumerable messages, ChatOptions? chatOptions = default, CancellationToken cancellationToken = default) + IEnumerable messages, ChatOptions? chatOptions = default, JsonSerializerOptions? serializerOptions = null, CancellationToken cancellationToken = default) { Throw.IfNull(messages); + serializerOptions ??= McpJsonUtilities.DefaultOptions; + StringBuilder? systemPrompt = null; if (chatOptions?.Instructions is { } instructions) @@ -148,7 +151,7 @@ public async Task SampleAsync( Name = af.Name, Description = af.Description, InputSchema = af.JsonSchema, - Meta = af.AdditionalProperties.ToJsonObject(), + Meta = af.AdditionalProperties.ToJsonObject(serializerOptions), }); } } @@ -172,13 +175,13 @@ public async Task SampleAsync( Temperature = chatOptions?.Temperature, ToolChoice = toolChoice, Tools = tools, - Meta = chatOptions?.AdditionalProperties?.ToJsonObject(), + Meta = chatOptions?.AdditionalProperties?.ToJsonObject(serializerOptions), }, cancellationToken).ConfigureAwait(false); List responseContents = []; foreach (var block in result.Content) { - if (block.ToAIContent() is { } content) + if (block.ToAIContent(serializerOptions) is { } content) { responseContents.Add(content); } @@ -202,13 +205,14 @@ public async Task SampleAsync( /// /// Creates an wrapper that can be used to send sampling requests to the client. /// + /// The to use for serialization. If , is used. /// The that can be used to issue sampling requests to the client. /// The client does not support sampling. - public IChatClient AsSamplingChatClient() + public IChatClient AsSamplingChatClient(JsonSerializerOptions? serializerOptions = null) { ThrowIfSamplingUnsupported(); - return new SamplingChatClient(this); + return new SamplingChatClient(this, serializerOptions ?? McpJsonUtilities.DefaultOptions); } /// Gets an on which logged messages will be sent as notifications to the client. @@ -520,13 +524,14 @@ private void ThrowIfElicitationUnsupported(ElicitRequestParams request) } /// Provides an implementation that's implemented via client sampling. - private sealed class SamplingChatClient(McpServer server) : IChatClient + private sealed class SamplingChatClient(McpServer server, JsonSerializerOptions serializerOptions) : IChatClient { private readonly McpServer _server = server; + private readonly JsonSerializerOptions _serializerOptions = serializerOptions; /// public Task GetResponseAsync(IEnumerable messages, ChatOptions? chatOptions = null, CancellationToken cancellationToken = default) => - _server.SampleAsync(messages, chatOptions, cancellationToken); + _server.SampleAsync(messages, chatOptions, _serializerOptions, cancellationToken); /// async IAsyncEnumerable IChatClient.GetStreamingResponseAsync( diff --git a/src/ModelContextProtocol.Core/Server/McpServerOptions.cs b/src/ModelContextProtocol.Core/Server/McpServerOptions.cs index 937e288c..4c7f7358 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerOptions.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerOptions.cs @@ -160,7 +160,7 @@ public McpServerHandlers Handlers /// The default maximum number of tokens to use for sampling requests. The default value is 1000 tokens. /// /// - /// This value is used in + /// This value is used in /// when is not set in the request options. /// public int MaxSamplingOutputTokens { get; set; } = 1000; diff --git a/src/ModelContextProtocol.Core/Server/McpServerTool.cs b/src/ModelContextProtocol.Core/Server/McpServerTool.cs index cebf7209..f2e75387 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerTool.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerTool.cs @@ -99,7 +99,7 @@ namespace ModelContextProtocol.Server; /// /// /// -/// Converted to a single object using . +/// Converted to a single object using . /// /// /// @@ -111,7 +111,7 @@ namespace ModelContextProtocol.Server; /// /// /// of -/// Each is converted to a object using . +/// Each is converted to a object using . /// /// /// of diff --git a/src/ModelContextProtocol.Core/Server/McpServerToolAttribute.cs b/src/ModelContextProtocol.Core/Server/McpServerToolAttribute.cs index 834e3e4a..11c5a780 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerToolAttribute.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerToolAttribute.cs @@ -90,7 +90,7 @@ namespace ModelContextProtocol.Server; /// /// /// -/// Converted to a single object using . +/// Converted to a single object using . /// /// /// @@ -106,7 +106,7 @@ namespace ModelContextProtocol.Server; /// /// /// of -/// Each is converted to a object using . +/// Each is converted to a object using . /// /// /// of diff --git a/tests/ModelContextProtocol.Tests/AIContentExtensionsTests.cs b/tests/ModelContextProtocol.Tests/AIContentExtensionsTests.cs index 3a57a07c..7d4d797a 100644 --- a/tests/ModelContextProtocol.Tests/AIContentExtensionsTests.cs +++ b/tests/ModelContextProtocol.Tests/AIContentExtensionsTests.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.AI; using ModelContextProtocol.Protocol; using System.Text.Json; +using System.Text.Json.Serialization; namespace ModelContextProtocol.Tests; @@ -148,4 +149,269 @@ public void ToAIContent_ToolResultToFunctionResultRoundTrip() Assert.False(functionResult.Exception != null); Assert.NotNull(functionResult.Result); } -} \ No newline at end of file + + // Tests for anonymous types in AdditionalProperties (sampling pipeline regression fix) + // These tests require reflection-based serialization and will be skipped when reflection is disabled. + + [Fact] + public void ToContentBlock_WithAnonymousTypeInAdditionalProperties_DoesNotThrow() + { + if (!JsonSerializer.IsReflectionEnabledByDefault) + { + return; + } + + // This is the minimal repro from the issue + AIContent c = new() + { + AdditionalProperties = new() + { + ["data"] = new { X = 1.0, Y = 2.0 } + } + }; + + // Should not throw NotSupportedException + var contentBlock = c.ToContentBlock(); + + Assert.NotNull(contentBlock); + Assert.NotNull(contentBlock.Meta); + Assert.True(contentBlock.Meta.ContainsKey("data")); + } + + [Fact] + public void ToContentBlock_WithMultipleAnonymousTypes_DoesNotThrow() + { + if (!JsonSerializer.IsReflectionEnabledByDefault) + { + return; + } + + AIContent c = new() + { + AdditionalProperties = new() + { + ["point"] = new { X = 1.0, Y = 2.0 }, + ["metadata"] = new { Name = "Test", Id = 42 }, + ["config"] = new { Enabled = true, Timeout = 30 } + } + }; + + var contentBlock = c.ToContentBlock(); + + Assert.NotNull(contentBlock); + Assert.NotNull(contentBlock.Meta); + Assert.Equal(3, contentBlock.Meta.Count); + } + + [Fact] + public void ToContentBlock_WithNestedAnonymousTypes_DoesNotThrow() + { + if (!JsonSerializer.IsReflectionEnabledByDefault) + { + return; + } + + AIContent c = new() + { + AdditionalProperties = new() + { + ["outer"] = new + { + Inner = new { Value = "test" }, + Count = 5 + } + } + }; + + var contentBlock = c.ToContentBlock(); + + Assert.NotNull(contentBlock); + Assert.NotNull(contentBlock.Meta); + Assert.True(contentBlock.Meta.ContainsKey("outer")); + } + + [Fact] + public void ToContentBlock_WithMixedTypesInAdditionalProperties_DoesNotThrow() + { + if (!JsonSerializer.IsReflectionEnabledByDefault) + { + return; + } + + AIContent c = new() + { + AdditionalProperties = new() + { + ["anonymous"] = new { X = 1.0, Y = 2.0 }, + ["string"] = "test", + ["number"] = 42, + ["boolean"] = true, + ["array"] = new[] { 1, 2, 3 } + } + }; + + var contentBlock = c.ToContentBlock(); + + Assert.NotNull(contentBlock); + Assert.NotNull(contentBlock.Meta); + Assert.Equal(5, contentBlock.Meta.Count); + } + + [Fact] + public void TextContent_ToContentBlock_WithAnonymousTypeInAdditionalProperties_PreservesData() + { + if (!JsonSerializer.IsReflectionEnabledByDefault) + { + return; + } + + TextContent textContent = new("Hello, world!") + { + AdditionalProperties = new() + { + ["location"] = new { Lat = 40.7128, Lon = -74.0060 } + } + }; + + var contentBlock = textContent.ToContentBlock(); + var textBlock = Assert.IsType(contentBlock); + + Assert.Equal("Hello, world!", textBlock.Text); + Assert.NotNull(textBlock.Meta); + Assert.True(textBlock.Meta.ContainsKey("location")); + } + + [Fact] + public void DataContent_ToContentBlock_WithAnonymousTypeInAdditionalProperties_PreservesData() + { + if (!JsonSerializer.IsReflectionEnabledByDefault) + { + return; + } + + byte[] imageData = [1, 2, 3, 4, 5]; + DataContent dataContent = new(imageData, "image/png") + { + AdditionalProperties = new() + { + ["dimensions"] = new { Width = 100, Height = 200 } + } + }; + + var contentBlock = dataContent.ToContentBlock(); + var imageBlock = Assert.IsType(contentBlock); + + Assert.Equal(Convert.ToBase64String(imageData), imageBlock.Data); + Assert.Equal("image/png", imageBlock.MimeType); + Assert.NotNull(imageBlock.Meta); + Assert.True(imageBlock.Meta.ContainsKey("dimensions")); + } + + [Fact] + public void ToContentBlock_WithCustomSerializerOptions_UsesProvidedOptions() + { + if (!JsonSerializer.IsReflectionEnabledByDefault) + { + return; + } + + // Create custom options with specific settings + var customOptions = new JsonSerializerOptions(McpJsonUtilities.DefaultOptions) + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower + }; + + AIContent c = new() + { + AdditionalProperties = new() + { + ["TestData"] = new { MyProperty = "value" } + } + }; + + var contentBlock = c.ToContentBlock(customOptions); + + Assert.NotNull(contentBlock); + Assert.NotNull(contentBlock.Meta); + + // Verify that the custom naming policy was applied + var json = contentBlock.Meta.ToString(); + Assert.Contains("my_property", json.ToLowerInvariant()); + } + + [Fact] + public void ToContentBlock_WithNamedUserDefinedTypeInAdditionalProperties_Works() + { + // This test should work regardless of reflection being enabled/disabled + // because named types can be handled by source generators + + // Create options with source generation support for the test type + var options = new JsonSerializerOptions(McpJsonUtilities.DefaultOptions); + options.TypeInfoResolverChain.Add(NamedTypeTestJsonContext.Default); + + // Define a simple named type + var testData = new TestCoordinates { X = 1.0, Y = 2.0 }; + + AIContent c = new() + { + AdditionalProperties = new() + { + ["coordinates"] = testData + } + }; + + // Should not throw NotSupportedException + var contentBlock = c.ToContentBlock(options); + + Assert.NotNull(contentBlock); + Assert.NotNull(contentBlock.Meta); + Assert.True(contentBlock.Meta.ContainsKey("coordinates")); + + // Verify the data was serialized correctly + var coordinatesNode = contentBlock.Meta["coordinates"]; + Assert.NotNull(coordinatesNode); + + var json = coordinatesNode.ToString(); + Assert.Contains("1", json); + Assert.Contains("2", json); + } + + [Fact] + public void ToChatMessage_CallToolResult_WithAnonymousTypeInContent_Works() + { + if (!JsonSerializer.IsReflectionEnabledByDefault) + { + return; + } + + // Create a CallToolResult with anonymous type data in the content + var result = new CallToolResult + { + Content = new List + { + new TextContentBlock + { + Text = "Result with metadata", + Meta = JsonSerializer.SerializeToNode(new { Status = "success", Code = 200 }) as System.Text.Json.Nodes.JsonObject + } + } + }; + + // This should not throw NotSupportedException + var exception = Record.Exception(() => result.ToChatMessage("call_123")); + + Assert.Null(exception); + } +} + +// Test type for named user-defined type test +internal record TestCoordinates +{ + public double X { get; init; } + public double Y { get; init; } +} + +// Source generation context for the test type +[JsonSerializable(typeof(TestCoordinates))] +[JsonSerializable(typeof(IReadOnlyDictionary))] +internal partial class NamedTypeTestJsonContext : JsonSerializerContext; \ No newline at end of file diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs index c1b2bcf2..83d3aaec 100644 --- a/tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs @@ -165,6 +165,14 @@ public static TextContentBlock MetadataEchoTool(RequestContext()); } + + [Fact] + public async Task CallToolAsync_WithAnonymousTypeArguments_Works() + { + if (!JsonSerializer.IsReflectionEnabledByDefault) + { + return; + } + + await using McpClient client = await CreateMcpClientForServer(); + + // Call with dictionary containing anonymous type values + var arguments = new Dictionary + { + ["text"] = "test", + ["coordinates"] = new { X = 1.0, Y = 2.0 } // Anonymous type + }; + + // This should not throw NotSupportedException + var result = await client.CallToolAsync("argument_echo_tool", arguments, cancellationToken: TestContext.Current.CancellationToken); + + Assert.NotNull(result); + Assert.NotEmpty(result.Content); + + // Verify the anonymous type was serialized correctly + var textBlock = Assert.IsType(result.Content[0]); + Assert.Contains("coordinates", textBlock.Text); + } } diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs index d9f3f1b4..4a52d64f 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs @@ -809,7 +809,7 @@ public async Task AsSamplingChatClient_NoSamplingSupport_Throws() { await using var server = new TestServerForIChatClient(supportsSampling: false); - Assert.Throws(server.AsSamplingChatClient); + Assert.Throws(() => server.AsSamplingChatClient()); } [Fact]