using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using ModelContextProtocol.Protocol;
namespace ModelContextProtocol.Tests;
///
/// Validates that the internal property pattern used for experimental properties
/// produces the expected serialization behavior for SDK consumers using source generators.
///
///
///
/// Experimental properties (e.g. , )
/// use an internal *Core property for serialization. A consumer's source-generated
/// cannot see internal members, so experimental data is
/// silently dropped unless the consumer chains the SDK's resolver into their options.
///
///
/// These tests depend on and
/// being experimental. When those APIs stabilize, update these tests to reference whatever
/// experimental properties exist at that time, or remove them entirely if no experimental
/// APIs remain.
///
///
public class ExperimentalPropertySerializationTests
{
[Fact]
public void ExperimentalProperties_Dropped_WithConsumerContextOnly()
{
var options = new JsonSerializerOptions
{
TypeInfoResolverChain = { ConsumerJsonContext.Default }
};
var capabilities = new ServerCapabilities
{
Tools = new ToolsCapability(),
Extensions = new Dictionary { ["io.test"] = new JsonObject { ["enabled"] = true } }
};
string json = JsonSerializer.Serialize(capabilities, options);
Assert.DoesNotContain("\"extensions\"", json);
Assert.Contains("\"tools\"", json);
}
[Fact]
public void ExperimentalProperties_IgnoredOnDeserialize_WithConsumerContextOnly()
{
string json = JsonSerializer.Serialize(
new ServerCapabilities
{
Tools = new ToolsCapability(),
Extensions = new Dictionary { ["io.test"] = new JsonObject { ["enabled"] = true } }
},
McpJsonUtilities.DefaultOptions);
Assert.Contains("\"extensions\"", json);
var options = new JsonSerializerOptions
{
TypeInfoResolverChain = { ConsumerJsonContext.Default }
};
var deserialized = JsonSerializer.Deserialize(json, options)!;
Assert.NotNull(deserialized.Tools);
Assert.Null(deserialized.Extensions);
}
[Fact]
public void ExperimentalProperties_RoundTrip_WhenSdkResolverIsChained()
{
var options = new JsonSerializerOptions
{
TypeInfoResolverChain =
{
McpJsonUtilities.DefaultOptions.TypeInfoResolver!,
ConsumerJsonContext.Default,
}
};
var capabilities = new ServerCapabilities
{
Tools = new ToolsCapability(),
Extensions = new Dictionary { ["io.test"] = new JsonObject { ["enabled"] = true } }
};
string json = JsonSerializer.Serialize(capabilities, options);
Assert.Contains("\"extensions\"", json);
Assert.Contains("\"tools\"", json);
var deserialized = JsonSerializer.Deserialize(json, options)!;
Assert.NotNull(deserialized.Tools);
Assert.NotNull(deserialized.Extensions);
Assert.True(deserialized.Extensions.ContainsKey("io.test"));
}
[Fact]
public void ExperimentalProperties_RoundTrip_WithDefaultOptions()
{
var capabilities = new ClientCapabilities
{
Extensions = new Dictionary { ["io.test"] = new JsonObject { ["enabled"] = true } }
};
string json = JsonSerializer.Serialize(capabilities, McpJsonUtilities.DefaultOptions);
Assert.Contains("\"extensions\"", json);
var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions)!;
Assert.NotNull(deserialized.Extensions);
Assert.True(deserialized.Extensions.ContainsKey("io.test"));
}
}
[JsonSerializable(typeof(Tool))]
[JsonSerializable(typeof(ServerCapabilities))]
[JsonSerializable(typeof(ClientCapabilities))]
[JsonSerializable(typeof(CallToolResult))]
[JsonSerializable(typeof(CallToolRequestParams))]
[JsonSerializable(typeof(CreateMessageRequestParams))]
[JsonSerializable(typeof(ElicitRequestParams))]
internal partial class ConsumerJsonContext : JsonSerializerContext;