Disable STJ reflection for testing. (#266)

* Disable STJ reflection for testing.

* Remove params method and redundant overload.

---------

Co-authored-by: Stephen Toub <stoub@microsoft.com>
This commit is contained in:
Eirik Tsarpalis 2025-04-10 16:02:00 +01:00 • committed by GitHub
commit e3aa54eb3c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 155 additions and 75 deletions

View file

@ -8,6 +8,7 @@ using ModelContextProtocol.Server;
using ModelContextProtocol.Utils;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Text.Json;
namespace Microsoft.Extensions.DependencyInjection;
@ -24,6 +25,7 @@ public static partial class McpServerBuilderExtensions
/// <summary>Adds <see cref="McpServerTool"/> instances to the service collection backing <paramref name="builder"/>.</summary>
/// <typeparam name="TToolType">The tool type.</typeparam>
/// <param name="builder">The builder instance.</param>
/// <param name="serializerOptions">The serializer options governing tool parameter marshalling.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
/// <remarks>
@ -35,7 +37,8 @@ public static partial class McpServerBuilderExtensions
DynamicallyAccessedMemberTypes.PublicMethods |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicConstructors)] TToolType>(
this IMcpServerBuilder builder)
this IMcpServerBuilder builder,
JsonSerializerOptions? serializerOptions = null)
{
Throw.IfNull(builder);
@ -44,8 +47,8 @@ public static partial class McpServerBuilderExtensions
if (toolMethod.GetCustomAttribute<McpServerToolAttribute>() is not null)
{
builder.Services.AddSingleton((Func<IServiceProvider, McpServerTool>)(toolMethod.IsStatic ?
services => McpServerTool.Create(toolMethod, options: new() { Services = services }) :
services => McpServerTool.Create(toolMethod, typeof(TToolType), new() { Services = services })));
services => McpServerTool.Create(toolMethod, options: new() { Services = services, SerializerOptions = serializerOptions }) :
services => McpServerTool.Create(toolMethod, typeof(TToolType), new() { Services = services, SerializerOptions = serializerOptions })));
}
}
@ -55,6 +58,7 @@ public static partial class McpServerBuilderExtensions
/// <summary>Adds <see cref="McpServerTool"/> instances to the service collection backing <paramref name="builder"/>.</summary>
/// <param name="builder">The builder instance.</param>
/// <param name="toolTypes">Types with marked methods to add as tools to the server.</param>
/// <param name="serializerOptions">The serializer options governing tool parameter marshalling.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="toolTypes"/> is <see langword="null"/>.</exception>
@ -64,7 +68,7 @@ public static partial class McpServerBuilderExtensions
/// instance for each. For instance methods, an instance will be constructed for each invocation of the tool.
/// </remarks>
[RequiresUnreferencedCode(WithToolsRequiresUnreferencedCodeMessage)]
public static IMcpServerBuilder WithTools(this IMcpServerBuilder builder, params IEnumerable<Type> toolTypes)
public static IMcpServerBuilder WithTools(this IMcpServerBuilder builder, IEnumerable<Type> toolTypes, JsonSerializerOptions? serializerOptions = null)
{
Throw.IfNull(builder);
Throw.IfNull(toolTypes);
@ -78,8 +82,8 @@ public static partial class McpServerBuilderExtensions
if (toolMethod.GetCustomAttribute<McpServerToolAttribute>() is not null)
{
builder.Services.AddSingleton((Func<IServiceProvider, McpServerTool>)(toolMethod.IsStatic ?
services => McpServerTool.Create(toolMethod, options: new() { Services = services }) :
services => McpServerTool.Create(toolMethod, toolType, new() { Services = services })));
services => McpServerTool.Create(toolMethod, options: new() { Services = services , SerializerOptions = serializerOptions }) :
services => McpServerTool.Create(toolMethod, toolType, new() { Services = services , SerializerOptions = serializerOptions })));
}
}
}
@ -92,6 +96,7 @@ public static partial class McpServerBuilderExtensions
/// Adds types marked with the <see cref="McpServerToolTypeAttribute"/> attribute from the given assembly as tools to the server.
/// </summary>
/// <param name="builder">The builder instance.</param>
/// <param name="serializerOptions">The serializer options governing tool parameter marshalling.</param>
/// <param name="toolAssembly">The assembly to load the types from. If <see langword="null"/>, the calling assembly will be used.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
@ -116,7 +121,7 @@ public static partial class McpServerBuilderExtensions
/// </para>
/// </remarks>
[RequiresUnreferencedCode(WithToolsRequiresUnreferencedCodeMessage)]
public static IMcpServerBuilder WithToolsFromAssembly(this IMcpServerBuilder builder, Assembly? toolAssembly = null)
public static IMcpServerBuilder WithToolsFromAssembly(this IMcpServerBuilder builder, Assembly? toolAssembly = null, JsonSerializerOptions? serializerOptions = null)
{
Throw.IfNull(builder);
@ -125,7 +130,8 @@ public static partial class McpServerBuilderExtensions
return builder.WithTools(
from t in toolAssembly.GetTypes()
where t.GetCustomAttribute<McpServerToolTypeAttribute>() is not null
select t);
select t,
serializerOptions);
}
#endregion

View file

@ -67,6 +67,7 @@ internal sealed class AIFunctionMcpServerTool : McpServerTool
Name = options?.Name ?? method.GetCustomAttribute<McpServerToolAttribute>()?.Name,
Description = options?.Description,
MarshalResult = static (result, _, cancellationToken) => new ValueTask<object?>(result),
SerializerOptions = options?.SerializerOptions ?? McpJsonUtilities.DefaultOptions,
ConfigureParameterBinding = pi =>
{
if (pi.ParameterType == typeof(RequestContext<CallToolRequestParams>))
@ -314,7 +315,7 @@ internal sealed class AIFunctionMcpServerTool : McpServerTool
{
Content = [new()
{
Text = JsonSerializer.Serialize(result, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object))),
Text = JsonSerializer.Serialize(result, AIFunction.JsonSerializerOptions.GetTypeInfo(typeof(object))),
Type = "text"
}]
},

View file

@ -1,4 +1,6 @@
using ModelContextProtocol.Utils.Json;
using System.ComponentModel;
using System.Text.Json;
namespace ModelContextProtocol.Server;
@ -122,11 +124,19 @@ public sealed class McpServerToolCreateOptions
/// </remarks>
public bool? ReadOnly { get; set; }
/// <summary>
/// Gets or sets the JSON serializer options to use when marshalling data to/from JSON.
/// </summary>
/// <remarks>
/// Defaults to <see cref="McpJsonUtilities.DefaultOptions"/> if left unspecified.
/// </remarks>
public JsonSerializerOptions? SerializerOptions { get; set; }
/// <summary>
/// Creates a shallow clone of the current <see cref="McpServerToolCreateOptions"/> instance.
/// </summary>
internal McpServerToolCreateOptions Clone() =>
new McpServerToolCreateOptions()
new McpServerToolCreateOptions
{
Services = Services,
Name = Name,
@ -135,6 +145,7 @@ public sealed class McpServerToolCreateOptions
Destructive = Destructive,
Idempotent = Idempotent,
OpenWorld = OpenWorld,
ReadOnly = ReadOnly
ReadOnly = ReadOnly,
SerializerOptions = SerializerOptions,
};
}

View file

@ -10,6 +10,11 @@
<RootNamespace>ModelContextProtocol.AspNetCore.Tests</RootNamespace>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'net9.0'">
<!-- For better test coverage, only disable reflection in one of the targets -->
<JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
</PropertyGroup>
<PropertyGroup>
<!-- Without this, tests are currently not showing results until all tests complete
https://xunit.net/docs/getting-started/v3/microsoft-testing-platform

View file

@ -10,11 +10,12 @@ using ModelContextProtocol.Protocol.Transport;
using ModelContextProtocol.Server;
using ModelContextProtocol.Tests.Utils;
using ModelContextProtocol.Utils.Json;
using System.Text.Json.Serialization;
using TestServerWithHosting.Tools;
namespace ModelContextProtocol.Tests;
public class SseIntegrationTests(ITestOutputHelper outputHelper) : KestrelInMemoryTest(outputHelper)
public partial class SseIntegrationTests(ITestOutputHelper outputHelper) : KestrelInMemoryTest(outputHelper)
{
private SseClientTransportOptions DefaultTransportOptions = new()
{
@ -41,7 +42,7 @@ public class SseIntegrationTests(ITestOutputHelper outputHelper) : KestrelInMemo
await using var mcpClient = await ConnectMcpClient(httpClient);
// Send a test message through POST endpoint
await mcpClient.SendNotificationAsync("test/message", new { message = "Hello, SSE!" }, cancellationToken: TestContext.Current.CancellationToken);
await mcpClient.SendNotificationAsync("test/message", new Envelope { Message = "Hello, SSE!" }, serializerOptions: JsonContext.Default.Options, cancellationToken: TestContext.Current.CancellationToken);
Assert.True(true);
}
@ -57,7 +58,7 @@ public class SseIntegrationTests(ITestOutputHelper outputHelper) : KestrelInMemo
await using var mcpClient = await ConnectMcpClient(httpClient);
// Send a test message through POST endpoint
await mcpClient.SendNotificationAsync("test/message", new { message = "Hello, SSE!" }, cancellationToken: TestContext.Current.CancellationToken);
await mcpClient.SendNotificationAsync("test/message", new Envelope { Message = "Hello, SSE!" }, serializerOptions: JsonContext.Default.Options, cancellationToken: TestContext.Current.CancellationToken);
Assert.True(true);
}
@ -73,7 +74,7 @@ public class SseIntegrationTests(ITestOutputHelper outputHelper) : KestrelInMemo
mcpServer.RegisterNotificationHandler("test/notification", async (notification, cancellationToken) =>
{
Assert.Equal("Hello from client!", notification.Params?["message"]?.GetValue<string>());
await mcpServer.SendNotificationAsync("test/notification", new { message = "Hello from server!" }, cancellationToken: cancellationToken);
await mcpServer.SendNotificationAsync("test/notification", new Envelope { Message = "Hello from server!" }, serializerOptions: JsonContext.Default.Options, cancellationToken: cancellationToken);
});
return mcpServer.RunAsync(cancellationToken);
});
@ -90,7 +91,7 @@ public class SseIntegrationTests(ITestOutputHelper outputHelper) : KestrelInMemo
});
// Send a test message through POST endpoint
await mcpClient.SendNotificationAsync("test/notification", new { message = "Hello from client!" }, cancellationToken: TestContext.Current.CancellationToken);
await mcpClient.SendNotificationAsync("test/notification", new Envelope { Message = "Hello from client!" }, serializerOptions: JsonContext.Default.Options, cancellationToken: TestContext.Current.CancellationToken);
var message = await receivedNotification.Task.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
Assert.Equal("Hello from server!", message);
@ -205,4 +206,13 @@ public class SseIntegrationTests(ITestOutputHelper outputHelper) : KestrelInMemo
await context.Response.WriteAsync("Accepted");
});
}
public class Envelope
{
public required string Message { get; set; }
}
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(Envelope))]
partial class JsonContext : JsonSerializerContext;
}

View file

@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Connections;
using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Server;
using ModelContextProtocol.Utils.Json;
using Serilog;
using System.Text;
using System.Text.Json;
@ -124,7 +125,7 @@ public class Program
},
"required": ["message"]
}
"""),
""", McpJsonUtilities.DefaultOptions),
},
new Tool()
{
@ -145,7 +146,7 @@ public class Program
},
"required": ["prompt", "maxTokens"]
}
"""),
""", McpJsonUtilities.DefaultOptions),
}
]
});

View file

@ -5,6 +5,7 @@ using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol.Messages;
using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Server;
using ModelContextProtocol.Utils.Json;
using Moq;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
@ -379,7 +380,7 @@ public class McpClientExtensionsTests : ClientServerTestBase
await using (client.RegisterNotificationHandler(NotificationMethods.LoggingMessageNotification,
(notification, cancellationToken) =>
{
Assert.True(channel.Writer.TryWrite(JsonSerializer.Deserialize<LoggingMessageNotificationParams>(notification.Params)));
Assert.True(channel.Writer.TryWrite(JsonSerializer.Deserialize<LoggingMessageNotificationParams>(notification.Params, McpJsonUtilities.DefaultOptions)));
return Task.CompletedTask;
}))
{
@ -398,7 +399,7 @@ public class McpClientExtensionsTests : ClientServerTestBase
Assert.Equal("TestLogger", m.Logger);
string ? s = JsonSerializer.Deserialize<string>(m.Data.Value);
string ? s = JsonSerializer.Deserialize<string>(m.Data.Value, McpJsonUtilities.DefaultOptions);
Assert.NotNull(s);
if (s.Contains("Information"))

View file

@ -3,6 +3,7 @@ using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol.Messages;
using ModelContextProtocol.Protocol.Transport;
using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Utils.Json;
using Moq;
using System.Text.Json;
using System.Threading.Channels;
@ -107,7 +108,7 @@ public class McpClientFactoryTests
Name = "NopTransport",
Version = "1.0.0"
},
}),
}, McpJsonUtilities.DefaultOptions),
});
break;
}

View file

@ -4,12 +4,14 @@ using ModelContextProtocol.Protocol.Messages;
using ModelContextProtocol.Protocol.Transport;
using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Tests.Utils;
using ModelContextProtocol.Utils.Json;
using OpenAI;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace ModelContextProtocol.Tests;
public class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIntegrationTestFixture>
public partial class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIntegrationTestFixture>
{
private static readonly string? s_openAIKey = Environment.GetEnvironmentVariable("AI:OpenAI:ApiKey");
@ -261,7 +263,7 @@ public class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIntegratio
[
new(NotificationMethods.ResourceUpdatedNotification, (notification, cancellationToken) =>
{
var notificationParams = JsonSerializer.Deserialize<ResourceUpdatedNotificationParams>(notification.Params);
var notificationParams = JsonSerializer.Deserialize<ResourceUpdatedNotificationParams>(notification.Params, McpJsonUtilities.DefaultOptions);
tcs.TrySetResult(true);
return Task.CompletedTask;
})
@ -291,7 +293,7 @@ public class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIntegratio
[
new(NotificationMethods.ResourceUpdatedNotification, (notification, cancellationToken) =>
{
var notificationParams = JsonSerializer.Deserialize<ResourceUpdatedNotificationParams>(notification.Params);
var notificationParams = JsonSerializer.Deserialize<ResourceUpdatedNotificationParams>(notification.Params, McpJsonUtilities.DefaultOptions);
receivedNotification.TrySetResult(true);
return Task.CompletedTask;
})
@ -442,13 +444,18 @@ public class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIntegratio
// Verify we can send notifications without errors
await client.SendNotificationAsync(NotificationMethods.RootsUpdatedNotification, cancellationToken: TestContext.Current.CancellationToken);
await client.SendNotificationAsync("test/notification", new { test = true }, cancellationToken: TestContext.Current.CancellationToken);
await client.SendNotificationAsync("test/notification", new TestNotification { Test = true }, cancellationToken: TestContext.Current.CancellationToken, serializerOptions: JsonContext3.Default.Options);
// assert
// no response to check, if no exception is thrown, it's a success
Assert.True(true);
}
class TestNotification
{
public required bool Test { get; set; }
}
[Fact]
public async Task CallTool_Stdio_MemoryServer()
{
@ -557,7 +564,7 @@ public class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIntegratio
[
new(NotificationMethods.LoggingMessageNotification, (notification, cancellationToken) =>
{
var loggingMessageNotificationParameters = JsonSerializer.Deserialize<LoggingMessageNotificationParams>(notification.Params);
var loggingMessageNotificationParameters = JsonSerializer.Deserialize<LoggingMessageNotificationParams>(notification.Params, McpJsonUtilities.DefaultOptions);
if (loggingMessageNotificationParameters is not null)
{
receivedNotification.TrySetResult(true);
@ -574,4 +581,7 @@ public class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIntegratio
// assert
await receivedNotification.Task;
}
[JsonSerializable(typeof(TestNotification))]
partial class JsonContext3 : JsonSerializerContext;
}

View file

@ -7,10 +7,12 @@ using ModelContextProtocol.Protocol.Messages;
using ModelContextProtocol.Protocol.Transport;
using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Server;
using ModelContextProtocol.Utils.Json;
using System.Collections.Concurrent;
using System.ComponentModel;
using System.IO.Pipelines;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using System.Threading.Channels;
@ -18,7 +20,7 @@ using System.Threading.Channels;
namespace ModelContextProtocol.Tests.Configuration;
public class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
{
public McpServerBuilderExtensionsToolsTests(ITestOutputHelper testOutputHelper)
: base(testOutputHelper)
@ -47,7 +49,7 @@ public class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
"properties": {},
"required": []
}
"""),
""", McpJsonUtilities.DefaultOptions),
}],
};
@ -65,7 +67,7 @@ public class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
"properties": {},
"required": []
}
"""),
""", McpJsonUtilities.DefaultOptions),
}],
};
@ -83,7 +85,7 @@ public class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
"properties": {},
"required": []
}
"""),
""", McpJsonUtilities.DefaultOptions),
}],
};
@ -107,7 +109,7 @@ public class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
throw new Exception($"Unknown tool '{request.Params?.Name}'");
}
})
.WithTools<EchoTool>();
.WithTools<EchoTool>(serializerOptions: JsonContext.Default.Options);
services.AddSingleton(new ObjectWithId());
}
@ -430,7 +432,7 @@ public class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
public void Register_Tools_From_Current_Assembly()
{
ServiceCollection sc = new();
sc.AddMcpServer().WithToolsFromAssembly();
sc.AddMcpServer().WithToolsFromAssembly(serializerOptions: JsonContext.Default.Options);
IServiceProvider services = sc.BuildServiceProvider();
Assert.Contains(services.GetServices<McpServerTool>(), t => t.ProtocolTool.Name == "Echo");
@ -446,17 +448,17 @@ public class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
{
sc.AddSingleton(new ComplexObject());
}
sc.AddMcpServer().WithTools(typeof(EchoTool));
sc.AddMcpServer().WithTools([typeof(EchoTool)], JsonContext.Default.Options);
IServiceProvider services = sc.BuildServiceProvider();
McpServerTool tool = services.GetServices<McpServerTool>().First(t => t.ProtocolTool.Name == "EchoComplex");
if (parameterInServices)
{
Assert.DoesNotContain("\"complex\"", JsonSerializer.Serialize(tool.ProtocolTool.InputSchema));
Assert.DoesNotContain("\"complex\"", JsonSerializer.Serialize(tool.ProtocolTool.InputSchema, AIJsonUtilities.DefaultOptions));
}
else
{
Assert.Contains("\"complex\"", JsonSerializer.Serialize(tool.ProtocolTool.InputSchema));
Assert.Contains("\"complex\"", JsonSerializer.Serialize(tool.ProtocolTool.InputSchema, AIJsonUtilities.DefaultOptions));
}
}
@ -483,17 +485,17 @@ public class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
break;
}
sc.AddMcpServer().WithToolsFromAssembly();
sc.AddMcpServer().WithToolsFromAssembly(serializerOptions: JsonContext.Default.Options);
IServiceProvider services = sc.BuildServiceProvider();
McpServerTool tool = services.GetServices<McpServerTool>().First(t => t.ProtocolTool.Name == "EchoComplex");
if (lifetime is not null)
{
Assert.DoesNotContain("\"complex\"", JsonSerializer.Serialize(tool.ProtocolTool.InputSchema));
Assert.DoesNotContain("\"complex\"", JsonSerializer.Serialize(tool.ProtocolTool.InputSchema, AIJsonUtilities.DefaultOptions));
}
else
{
Assert.Contains("\"complex\"", JsonSerializer.Serialize(tool.ProtocolTool.InputSchema));
Assert.Contains("\"complex\"", JsonSerializer.Serialize(tool.ProtocolTool.InputSchema, AIJsonUtilities.DefaultOptions));
}
}
@ -526,9 +528,9 @@ public class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
{
ServiceCollection sc = new();
sc.AddMcpServer()
.WithTools<EchoTool>()
.WithTools<AnotherToolType>()
.WithTools(typeof(ToolTypeWithNoAttribute));
.WithTools<EchoTool>(serializerOptions: JsonContext.Default.Options)
.WithTools<AnotherToolType>(serializerOptions: JsonContext.Default.Options)
.WithTools([typeof(ToolTypeWithNoAttribute)], JsonContext.Default.Options);
IServiceProvider services = sc.BuildServiceProvider();
Assert.Contains(services.GetServices<McpServerTool>(), t => t.ProtocolTool.Name == "double_echo");
@ -584,7 +586,7 @@ public class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
ConcurrentQueue<ProgressNotification> notifications = new();
await using (client.RegisterNotificationHandler(NotificationMethods.ProgressNotification, (notification, cancellationToken) =>
{
ProgressNotification pn = JsonSerializer.Deserialize<ProgressNotification>(notification.Params)!;
ProgressNotification pn = JsonSerializer.Deserialize<ProgressNotification>(notification.Params, McpJsonUtilities.DefaultOptions)!;
notifications.Enqueue(pn);
return Task.CompletedTask;
}))
@ -598,7 +600,7 @@ public class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
},
cancellationToken: TestContext.Current.CancellationToken);
Assert.Contains("done", JsonSerializer.Serialize(result));
Assert.Contains("done", JsonSerializer.Serialize(result, McpJsonUtilities.DefaultOptions));
SpinWait.SpinUntil(() => notifications.Count == 10, TimeSpan.FromSeconds(10));
}
@ -765,4 +767,16 @@ public class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
{
public string Id { get; set; } = Guid.NewGuid().ToString("N");
}
[JsonSerializable(typeof(bool))]
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(long))]
[JsonSerializable(typeof(double))]
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(DateTime))]
[JsonSerializable(typeof(DateTimeOffset))]
[JsonSerializable(typeof(ComplexObject))]
[JsonSerializable(typeof(string[]))]
[JsonSerializable(typeof(JsonElement))]
partial class JsonContext : JsonSerializerContext;
}

View file

@ -19,10 +19,8 @@ public static class McpJsonUtilitiesTests
public static void DefaultOptions_UseReflectionWhenEnabled()
{
var options = McpJsonUtilities.DefaultOptions;
bool isReflectionEnabled = JsonSerializer.IsReflectionEnabledByDefault;
Type anonType = new { Id = 42 }.GetType();
Assert.True(isReflectionEnabled); // To be disabled once https://github.com/dotnet/extensions/pull/6241 is incorporated
Assert.Equal(isReflectionEnabled, options.TryGetTypeInfo(anonType, out _));
Assert.Equal(JsonSerializer.IsReflectionEnabledByDefault, options.TryGetTypeInfo(anonType, out _));
}
}

View file

@ -12,6 +12,11 @@
<RootNamespace>ModelContextProtocol.Tests</RootNamespace>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'net9.0'">
<!-- For better test coverage, only disable reflection in one of the targets -->
<JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
</PropertyGroup>
<PropertyGroup>
<!-- Without this, tests are currently not showing results until all tests complete
https://xunit.net/docs/getting-started/v3/microsoft-testing-platform

View file

@ -1,4 +1,5 @@
using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Utils.Json;
using System.Text.Json;
namespace ModelContextProtocol.Tests.Protocol;
@ -57,7 +58,7 @@ public static class ProtocolTypeTests
[InlineData(Role.Assistant, "\"assistant\"")]
public static void SerializeRole_ShouldBeCamelCased(Role role, string expectedValue)
{
var actualValue = JsonSerializer.Serialize(role);
var actualValue = JsonSerializer.Serialize(role, McpJsonUtilities.DefaultOptions);
Assert.Equal(expectedValue, actualValue);
}
@ -73,7 +74,7 @@ public static class ProtocolTypeTests
[InlineData(LoggingLevel.Emergency, "\"emergency\"")]
public static void SerializeLoggingLevel_ShouldBeCamelCased(LoggingLevel level, string expectedValue)
{
var actualValue = JsonSerializer.Serialize(level);
var actualValue = JsonSerializer.Serialize(level, McpJsonUtilities.DefaultOptions);
Assert.Equal(expectedValue, actualValue);
}
@ -84,7 +85,7 @@ public static class ProtocolTypeTests
[InlineData(ContextInclusion.AllServers, "\"allServers\"")]
public static void ContextInclusion_ShouldBeCamelCased(ContextInclusion level, string expectedValue)
{
var actualValue = JsonSerializer.Serialize(level);
var actualValue = JsonSerializer.Serialize(level, McpJsonUtilities.DefaultOptions);
Assert.Equal(expectedValue, actualValue);
}

View file

@ -1,4 +1,5 @@
using ModelContextProtocol.Protocol.Messages;
using ModelContextProtocol.Utils.Json;
using System.Text.Json;
namespace ModelContextProtocol.Tests.Protocol;
@ -10,14 +11,14 @@ public class RequestIdTests
{
RequestId id = new("test-id");
Assert.Equal("test-id", id.ToString());
Assert.Equal("\"test-id\"", JsonSerializer.Serialize(id));
Assert.Equal("\"test-id\"", JsonSerializer.Serialize(id, McpJsonUtilities.DefaultOptions));
Assert.Same("test-id", id.Id);
Assert.True(id.Equals(new("test-id")));
Assert.False(id.Equals(new("tEst-id")));
Assert.Equal("test-id".GetHashCode(), id.GetHashCode());
Assert.Equal(id, JsonSerializer.Deserialize<RequestId>(JsonSerializer.Serialize(id)));
Assert.Equal(id, JsonSerializer.Deserialize<RequestId>(JsonSerializer.Serialize(id, McpJsonUtilities.DefaultOptions), McpJsonUtilities.DefaultOptions));
}
[Fact]
@ -25,7 +26,7 @@ public class RequestIdTests
{
RequestId id = new(42);
Assert.Equal("42", id.ToString());
Assert.Equal("42", JsonSerializer.Serialize(id));
Assert.Equal("42", JsonSerializer.Serialize(id, McpJsonUtilities.DefaultOptions));
Assert.Equal(42, Assert.IsType<long>(id.Id));
Assert.True(id.Equals(new(42)));
@ -33,6 +34,6 @@ public class RequestIdTests
Assert.False(id.Equals(new("42")));
Assert.Equal(42L.GetHashCode(), id.GetHashCode());
Assert.Equal(id, JsonSerializer.Deserialize<RequestId>(JsonSerializer.Serialize(id)));
Assert.Equal(id, JsonSerializer.Deserialize<RequestId>(JsonSerializer.Serialize(id, McpJsonUtilities.DefaultOptions), McpJsonUtilities.DefaultOptions));
}
}

View file

@ -3,6 +3,7 @@ using ModelContextProtocol.Protocol.Messages;
using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Server;
using ModelContextProtocol.Tests.Utils;
using ModelContextProtocol.Utils.Json;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Nodes;
@ -185,7 +186,7 @@ public class McpServerTests : LoggedTest
configureOptions: null,
assertResult: response =>
{
var result = JsonSerializer.Deserialize<InitializeResult>(response);
var result = JsonSerializer.Deserialize<InitializeResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result);
Assert.Equal("ModelContextProtocol.Tests", result.ServerInfo.Name);
Assert.Equal("1.0.0.0", result.ServerInfo.Version);
@ -217,7 +218,7 @@ public class McpServerTests : LoggedTest
configureOptions: null,
assertResult: response =>
{
var result = JsonSerializer.Deserialize<CompleteResult>(response);
var result = JsonSerializer.Deserialize<CompleteResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result?.Completion);
Assert.Equal(["test"], result.Completion.Values);
Assert.Equal(2, result.Completion.Total);
@ -254,7 +255,7 @@ public class McpServerTests : LoggedTest
configureOptions: null,
assertResult: response =>
{
var result = JsonSerializer.Deserialize<ListResourceTemplatesResult>(response);
var result = JsonSerializer.Deserialize<ListResourceTemplatesResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result?.ResourceTemplates);
Assert.NotEmpty(result.ResourceTemplates);
Assert.Equal("test", result.ResourceTemplates[0].UriTemplate);
@ -283,7 +284,7 @@ public class McpServerTests : LoggedTest
configureOptions: null,
assertResult: response =>
{
var result = JsonSerializer.Deserialize<ListResourcesResult>(response);
var result = JsonSerializer.Deserialize<ListResourcesResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result?.Resources);
Assert.NotEmpty(result.Resources);
Assert.Equal("test", result.Resources[0].Uri);
@ -318,7 +319,7 @@ public class McpServerTests : LoggedTest
configureOptions: null,
assertResult: response =>
{
var result = JsonSerializer.Deserialize<ReadResourceResult>(response);
var result = JsonSerializer.Deserialize<ReadResourceResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result?.Contents);
Assert.NotEmpty(result.Contents);
@ -355,7 +356,7 @@ public class McpServerTests : LoggedTest
configureOptions: null,
assertResult: response =>
{
var result = JsonSerializer.Deserialize<ListPromptsResult>(response);
var result = JsonSerializer.Deserialize<ListPromptsResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result?.Prompts);
Assert.NotEmpty(result.Prompts);
Assert.Equal("test", result.Prompts[0].Name);
@ -384,7 +385,7 @@ public class McpServerTests : LoggedTest
configureOptions: null,
assertResult: response =>
{
var result = JsonSerializer.Deserialize<GetPromptResult>(response);
var result = JsonSerializer.Deserialize<GetPromptResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result);
Assert.Equal("test", result.Description);
});
@ -418,7 +419,7 @@ public class McpServerTests : LoggedTest
configureOptions: null,
assertResult: response =>
{
var result = JsonSerializer.Deserialize<ListToolsResult>(response);
var result = JsonSerializer.Deserialize<ListToolsResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result);
Assert.NotEmpty(result.Tools);
Assert.Equal("test", result.Tools[0].Name);
@ -453,7 +454,7 @@ public class McpServerTests : LoggedTest
configureOptions: null,
assertResult: response =>
{
var result = JsonSerializer.Deserialize<CallToolResponse>(response);
var result = JsonSerializer.Deserialize<CallToolResponse>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result);
Assert.NotEmpty(result.Content);
Assert.Equal("test", result.Content[0].Text);
@ -581,7 +582,7 @@ public class McpServerTests : LoggedTest
public Task<JsonRpcResponse> SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken)
{
CreateMessageRequestParams? rp = JsonSerializer.Deserialize<CreateMessageRequestParams>(request.Params);
CreateMessageRequestParams? rp = JsonSerializer.Deserialize<CreateMessageRequestParams>(request.Params, McpJsonUtilities.DefaultOptions);
Assert.NotNull(rp);
Assert.Equal(0.75f, rp.Temperature);
@ -608,7 +609,7 @@ public class McpServerTests : LoggedTest
return Task.FromResult(new JsonRpcResponse
{
Id = new RequestId("0"),
Result = JsonSerializer.SerializeToNode(result),
Result = JsonSerializer.SerializeToNode(result, McpJsonUtilities.DefaultOptions),
});
}
@ -658,11 +659,11 @@ public class McpServerTests : LoggedTest
Total = 100,
Message = "Progress message",
},
}),
}, McpJsonUtilities.DefaultOptions),
}, TestContext.Current.CancellationToken);
var notification = await notificationReceived.Task;
var progress = JsonSerializer.Deserialize<ProgressNotification>(notification.Params);
var progress = JsonSerializer.Deserialize<ProgressNotification>(notification.Params, McpJsonUtilities.DefaultOptions);
Assert.NotNull(progress);
Assert.Equal("abc", progress.ProgressToken.ToString());
Assert.Equal(50, progress.Progress.Progress);

View file

@ -2,6 +2,7 @@
using Microsoft.Extensions.DependencyInjection;
using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Server;
using ModelContextProtocol.Utils.Json;
using Moq;
using System.Reflection;
using System.Text.Json;
@ -9,7 +10,7 @@ using System.Text.Json.Serialization;
namespace ModelContextProtocol.Tests.Server;
public class McpServerToolTests
public partial class McpServerToolTests
{
[Fact]
public void Create_InvalidArgs_Throws()
@ -36,7 +37,7 @@ public class McpServerToolTests
return "42";
});
Assert.DoesNotContain("server", JsonSerializer.Serialize(tool.ProtocolTool.InputSchema));
Assert.DoesNotContain("server", JsonSerializer.Serialize(tool.ProtocolTool.InputSchema, McpJsonUtilities.DefaultOptions));
var result = await tool.InvokeAsync(
new RequestContext<CallToolRequestParams>(mockServer.Object, null),
@ -86,7 +87,7 @@ public class McpServerToolTests
McpServerTool tool = services.GetRequiredService<McpServerTool>();
Assert.DoesNotContain("actualMyService", JsonSerializer.Serialize(tool.ProtocolTool.InputSchema));
Assert.DoesNotContain("actualMyService", JsonSerializer.Serialize(tool.ProtocolTool.InputSchema, McpJsonUtilities.DefaultOptions));
Mock<IMcpServer> mockServer = new();
@ -127,9 +128,11 @@ public class McpServerToolTests
[Fact]
public async Task SupportsDisposingInstantiatedDisposableTargets()
{
McpServerToolCreateOptions options = new() { SerializerOptions = JsonContext2.Default.Options };
McpServerTool tool1 = McpServerTool.Create(
typeof(DisposableToolType).GetMethod(nameof(DisposableToolType.InstanceMethod))!,
typeof(DisposableToolType));
typeof(DisposableToolType),
options);
var result = await tool1.InvokeAsync(
new RequestContext<CallToolRequestParams>(null!, null),
@ -140,9 +143,11 @@ public class McpServerToolTests
[Fact]
public async Task SupportsAsyncDisposingInstantiatedAsyncDisposableTargets()
{
McpServerToolCreateOptions options = new() { SerializerOptions = JsonContext2.Default.Options };
McpServerTool tool1 = McpServerTool.Create(
typeof(AsyncDisposableToolType).GetMethod(nameof(AsyncDisposableToolType.InstanceMethod))!,
typeof(AsyncDisposableToolType));
typeof(AsyncDisposableToolType),
options);
var result = await tool1.InvokeAsync(
new RequestContext<CallToolRequestParams>(null!, null),
@ -153,9 +158,11 @@ public class McpServerToolTests
[Fact]
public async Task SupportsAsyncDisposingInstantiatedAsyncDisposableAndDisposableTargets()
{
McpServerToolCreateOptions options = new() { SerializerOptions = JsonContext2.Default.Options };
McpServerTool tool1 = McpServerTool.Create(
typeof(AsyncDisposableAndDisposableToolType).GetMethod(nameof(AsyncDisposableAndDisposableToolType.InstanceMethod))!,
typeof(AsyncDisposableAndDisposableToolType));
typeof(AsyncDisposableAndDisposableToolType),
options);
var result = await tool1.InvokeAsync(
new RequestContext<CallToolRequestParams>(null!, null),
@ -432,4 +439,10 @@ public class McpServerToolTests
return this;
}
}
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(DisposableToolType))]
[JsonSerializable(typeof(AsyncDisposableToolType))]
[JsonSerializable(typeof(AsyncDisposableAndDisposableToolType))]
partial class JsonContext2 : JsonSerializerContext;
}

View file

@ -160,7 +160,7 @@ public class StdioServerTransportTests : LoggedTest
var chineseResult = Encoding.UTF8.GetString(output.ToArray()).Trim();
var expectedChinese = JsonSerializer.Serialize(chineseMessage, McpJsonUtilities.DefaultOptions);
Assert.Equal(expectedChinese, chineseResult);
Assert.Contains(JsonSerializer.Serialize(chineseText), chineseResult);
Assert.Contains(JsonSerializer.Serialize(chineseText, McpJsonUtilities.DefaultOptions), chineseResult);
// Test 2: Emoji (non-BMP Unicode using surrogate pairs)
var emojiText = "🔍 🚀 👍"; // Magnifying glass, rocket, thumbs up

View file

@ -1,6 +1,7 @@
using ModelContextProtocol.Protocol.Messages;
using ModelContextProtocol.Protocol.Transport;
using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Utils.Json;
using System.Text.Json;
using System.Threading.Channels;
@ -63,7 +64,7 @@ public class TestServerTransport : ITransport
Result = JsonSerializer.SerializeToNode(new ListRootsResult
{
Roots = []
}),
}, McpJsonUtilities.DefaultOptions),
}, cancellationToken);
}
@ -72,7 +73,7 @@ public class TestServerTransport : ITransport
await WriteMessageAsync(new JsonRpcResponse
{
Id = request.Id,
Result = JsonSerializer.SerializeToNode(new CreateMessageResult { Content = new(), Model = "model", Role = Role.User }),
Result = JsonSerializer.SerializeToNode(new CreateMessageResult { Content = new(), Model = "model", Role = Role.User }, McpJsonUtilities.DefaultOptions),
}, cancellationToken);
}