Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/ModelContextProtocol.Core/AIContentExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ public static class AIContentExtensions

/// <summary>Converts the specified dictionary to a <see cref="JsonObject"/>.</summary>
internal static JsonObject? ToJsonObject(this IReadOnlyDictionary<string, object?> properties) =>
JsonSerializer.SerializeToNode(properties, McpJsonUtilities.JsonContext.Default.IReadOnlyDictionaryStringObject) as JsonObject;
JsonSerializer.SerializeToNode(properties, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IReadOnlyDictionary<string, object?>))) as JsonObject;
Comment thread
stephentoub marked this conversation as resolved.
Outdated

internal static AdditionalPropertiesDictionary ToAdditionalProperties(this JsonObject obj)
{
Expand Down Expand Up @@ -271,7 +271,7 @@ public static IList<PromptMessage> 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)),
static json => JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions.GetTypeInfo<IDictionary<string, object?>>())),
Comment thread
stephentoub marked this conversation as resolved.
Outdated

ToolResultContentBlock toolResult => new FunctionResultContent(
toolResult.ToolUseId,
Expand Down
157 changes: 157 additions & 0 deletions tests/ModelContextProtocol.Tests/AIContentExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,161 @@ public void ToAIContent_ToolResultToFunctionResultRoundTrip()
Assert.False(functionResult.Exception != null);
Assert.NotNull(functionResult.Result);
}

// 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()
Comment thread
stephentoub marked this conversation as resolved.
{
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<TextContentBlock>(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<ImageContentBlock>(contentBlock);

Assert.Equal(Convert.ToBase64String(imageData), imageBlock.Data);
Assert.Equal("image/png", imageBlock.MimeType);
Assert.NotNull(imageBlock.Meta);
Assert.True(imageBlock.Meta.ContainsKey("dimensions"));
}
}