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

View file

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

View file

@ -1,4 +1,6 @@
using ModelContextProtocol.Utils.Json;
using System.ComponentModel; using System.ComponentModel;
using System.Text.Json;
namespace ModelContextProtocol.Server; namespace ModelContextProtocol.Server;
@ -122,11 +124,19 @@ public sealed class McpServerToolCreateOptions
/// </remarks> /// </remarks>
public bool? ReadOnly { get; set; } 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> /// <summary>
/// Creates a shallow clone of the current <see cref="McpServerToolCreateOptions"/> instance. /// Creates a shallow clone of the current <see cref="McpServerToolCreateOptions"/> instance.
/// </summary> /// </summary>
internal McpServerToolCreateOptions Clone() => internal McpServerToolCreateOptions Clone() =>
new McpServerToolCreateOptions() new McpServerToolCreateOptions
{ {
Services = Services, Services = Services,
Name = Name, Name = Name,
@ -135,6 +145,7 @@ public sealed class McpServerToolCreateOptions
Destructive = Destructive, Destructive = Destructive,
Idempotent = Idempotent, Idempotent = Idempotent,
OpenWorld = OpenWorld, OpenWorld = OpenWorld,
ReadOnly = ReadOnly ReadOnly = ReadOnly,
SerializerOptions = SerializerOptions,
}; };
} }

View file

@ -10,6 +10,11 @@
<RootNamespace>ModelContextProtocol.AspNetCore.Tests</RootNamespace> <RootNamespace>ModelContextProtocol.AspNetCore.Tests</RootNamespace>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'net9.0'">
<!-- For better test coverage, only disable reflection in one of the targets -->
<JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
</PropertyGroup>
<PropertyGroup> <PropertyGroup>
<!-- Without this, tests are currently not showing results until all tests complete <!-- Without this, tests are currently not showing results until all tests complete
https://xunit.net/docs/getting-started/v3/microsoft-testing-platform 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.Server;
using ModelContextProtocol.Tests.Utils; using ModelContextProtocol.Tests.Utils;
using ModelContextProtocol.Utils.Json; using ModelContextProtocol.Utils.Json;
using System.Text.Json.Serialization;
using TestServerWithHosting.Tools; using TestServerWithHosting.Tools;
namespace ModelContextProtocol.Tests; namespace ModelContextProtocol.Tests;
public class SseIntegrationTests(ITestOutputHelper outputHelper) : KestrelInMemoryTest(outputHelper) public partial class SseIntegrationTests(ITestOutputHelper outputHelper) : KestrelInMemoryTest(outputHelper)
{ {
private SseClientTransportOptions DefaultTransportOptions = new() private SseClientTransportOptions DefaultTransportOptions = new()
{ {
@ -41,7 +42,7 @@ public class SseIntegrationTests(ITestOutputHelper outputHelper) : KestrelInMemo
await using var mcpClient = await ConnectMcpClient(httpClient); await using var mcpClient = await ConnectMcpClient(httpClient);
// Send a test message through POST endpoint // 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); Assert.True(true);
} }
@ -57,7 +58,7 @@ public class SseIntegrationTests(ITestOutputHelper outputHelper) : KestrelInMemo
await using var mcpClient = await ConnectMcpClient(httpClient); await using var mcpClient = await ConnectMcpClient(httpClient);
// Send a test message through POST endpoint // 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); Assert.True(true);
} }
@ -73,7 +74,7 @@ public class SseIntegrationTests(ITestOutputHelper outputHelper) : KestrelInMemo
mcpServer.RegisterNotificationHandler("test/notification", async (notification, cancellationToken) => mcpServer.RegisterNotificationHandler("test/notification", async (notification, cancellationToken) =>
{ {
Assert.Equal("Hello from client!", notification.Params?["message"]?.GetValue<string>()); 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); return mcpServer.RunAsync(cancellationToken);
}); });
@ -90,7 +91,7 @@ public class SseIntegrationTests(ITestOutputHelper outputHelper) : KestrelInMemo
}); });
// Send a test message through POST endpoint // 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); var message = await receivedNotification.Task.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
Assert.Equal("Hello from server!", message); Assert.Equal("Hello from server!", message);
@ -205,4 +206,13 @@ public class SseIntegrationTests(ITestOutputHelper outputHelper) : KestrelInMemo
await context.Response.WriteAsync("Accepted"); 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 Microsoft.AspNetCore.Connections;
using ModelContextProtocol.Protocol.Types; using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Server; using ModelContextProtocol.Server;
using ModelContextProtocol.Utils.Json;
using Serilog; using Serilog;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
@ -124,7 +125,7 @@ public class Program
}, },
"required": ["message"] "required": ["message"]
} }
"""), """, McpJsonUtilities.DefaultOptions),
}, },
new Tool() new Tool()
{ {
@ -145,7 +146,7 @@ public class Program
}, },
"required": ["prompt", "maxTokens"] "required": ["prompt", "maxTokens"]
} }
"""), """, McpJsonUtilities.DefaultOptions),
} }
] ]
}); });

View file

@ -5,6 +5,7 @@ using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol.Messages; using ModelContextProtocol.Protocol.Messages;
using ModelContextProtocol.Protocol.Types; using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Server; using ModelContextProtocol.Server;
using ModelContextProtocol.Utils.Json;
using Moq; using Moq;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization.Metadata; using System.Text.Json.Serialization.Metadata;
@ -379,7 +380,7 @@ public class McpClientExtensionsTests : ClientServerTestBase
await using (client.RegisterNotificationHandler(NotificationMethods.LoggingMessageNotification, await using (client.RegisterNotificationHandler(NotificationMethods.LoggingMessageNotification,
(notification, cancellationToken) => (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; return Task.CompletedTask;
})) }))
{ {
@ -398,7 +399,7 @@ public class McpClientExtensionsTests : ClientServerTestBase
Assert.Equal("TestLogger", m.Logger); 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); Assert.NotNull(s);
if (s.Contains("Information")) if (s.Contains("Information"))

View file

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

View file

@ -4,12 +4,14 @@ using ModelContextProtocol.Protocol.Messages;
using ModelContextProtocol.Protocol.Transport; using ModelContextProtocol.Protocol.Transport;
using ModelContextProtocol.Protocol.Types; using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Tests.Utils; using ModelContextProtocol.Tests.Utils;
using ModelContextProtocol.Utils.Json;
using OpenAI; using OpenAI;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization;
namespace ModelContextProtocol.Tests; 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"); 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) => new(NotificationMethods.ResourceUpdatedNotification, (notification, cancellationToken) =>
{ {
var notificationParams = JsonSerializer.Deserialize<ResourceUpdatedNotificationParams>(notification.Params); var notificationParams = JsonSerializer.Deserialize<ResourceUpdatedNotificationParams>(notification.Params, McpJsonUtilities.DefaultOptions);
tcs.TrySetResult(true); tcs.TrySetResult(true);
return Task.CompletedTask; return Task.CompletedTask;
}) })
@ -291,7 +293,7 @@ public class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIntegratio
[ [
new(NotificationMethods.ResourceUpdatedNotification, (notification, cancellationToken) => new(NotificationMethods.ResourceUpdatedNotification, (notification, cancellationToken) =>
{ {
var notificationParams = JsonSerializer.Deserialize<ResourceUpdatedNotificationParams>(notification.Params); var notificationParams = JsonSerializer.Deserialize<ResourceUpdatedNotificationParams>(notification.Params, McpJsonUtilities.DefaultOptions);
receivedNotification.TrySetResult(true); receivedNotification.TrySetResult(true);
return Task.CompletedTask; return Task.CompletedTask;
}) })
@ -442,13 +444,18 @@ public class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIntegratio
// Verify we can send notifications without errors // Verify we can send notifications without errors
await client.SendNotificationAsync(NotificationMethods.RootsUpdatedNotification, cancellationToken: TestContext.Current.CancellationToken); 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 // assert
// no response to check, if no exception is thrown, it's a success // no response to check, if no exception is thrown, it's a success
Assert.True(true); Assert.True(true);
} }
class TestNotification
{
public required bool Test { get; set; }
}
[Fact] [Fact]
public async Task CallTool_Stdio_MemoryServer() public async Task CallTool_Stdio_MemoryServer()
{ {
@ -557,7 +564,7 @@ public class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIntegratio
[ [
new(NotificationMethods.LoggingMessageNotification, (notification, cancellationToken) => 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) if (loggingMessageNotificationParameters is not null)
{ {
receivedNotification.TrySetResult(true); receivedNotification.TrySetResult(true);
@ -574,4 +581,7 @@ public class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIntegratio
// assert // assert
await receivedNotification.Task; 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.Transport;
using ModelContextProtocol.Protocol.Types; using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Server; using ModelContextProtocol.Server;
using ModelContextProtocol.Utils.Json;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.ComponentModel; using System.ComponentModel;
using System.IO.Pipelines; using System.IO.Pipelines;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Channels; using System.Threading.Channels;
@ -18,7 +20,7 @@ using System.Threading.Channels;
namespace ModelContextProtocol.Tests.Configuration; namespace ModelContextProtocol.Tests.Configuration;
public class McpServerBuilderExtensionsToolsTests : ClientServerTestBase public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
{ {
public McpServerBuilderExtensionsToolsTests(ITestOutputHelper testOutputHelper) public McpServerBuilderExtensionsToolsTests(ITestOutputHelper testOutputHelper)
: base(testOutputHelper) : base(testOutputHelper)
@ -47,7 +49,7 @@ public class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
"properties": {}, "properties": {},
"required": [] "required": []
} }
"""), """, McpJsonUtilities.DefaultOptions),
}], }],
}; };
@ -65,7 +67,7 @@ public class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
"properties": {}, "properties": {},
"required": [] "required": []
} }
"""), """, McpJsonUtilities.DefaultOptions),
}], }],
}; };
@ -83,7 +85,7 @@ public class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
"properties": {}, "properties": {},
"required": [] "required": []
} }
"""), """, McpJsonUtilities.DefaultOptions),
}], }],
}; };
@ -107,7 +109,7 @@ public class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
throw new Exception($"Unknown tool '{request.Params?.Name}'"); throw new Exception($"Unknown tool '{request.Params?.Name}'");
} }
}) })
.WithTools<EchoTool>(); .WithTools<EchoTool>(serializerOptions: JsonContext.Default.Options);
services.AddSingleton(new ObjectWithId()); services.AddSingleton(new ObjectWithId());
} }
@ -430,7 +432,7 @@ public class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
public void Register_Tools_From_Current_Assembly() public void Register_Tools_From_Current_Assembly()
{ {
ServiceCollection sc = new(); ServiceCollection sc = new();
sc.AddMcpServer().WithToolsFromAssembly(); sc.AddMcpServer().WithToolsFromAssembly(serializerOptions: JsonContext.Default.Options);
IServiceProvider services = sc.BuildServiceProvider(); IServiceProvider services = sc.BuildServiceProvider();
Assert.Contains(services.GetServices<McpServerTool>(), t => t.ProtocolTool.Name == "Echo"); Assert.Contains(services.GetServices<McpServerTool>(), t => t.ProtocolTool.Name == "Echo");
@ -446,17 +448,17 @@ public class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
{ {
sc.AddSingleton(new ComplexObject()); sc.AddSingleton(new ComplexObject());
} }
sc.AddMcpServer().WithTools(typeof(EchoTool)); sc.AddMcpServer().WithTools([typeof(EchoTool)], JsonContext.Default.Options);
IServiceProvider services = sc.BuildServiceProvider(); IServiceProvider services = sc.BuildServiceProvider();
McpServerTool tool = services.GetServices<McpServerTool>().First(t => t.ProtocolTool.Name == "EchoComplex"); McpServerTool tool = services.GetServices<McpServerTool>().First(t => t.ProtocolTool.Name == "EchoComplex");
if (parameterInServices) if (parameterInServices)
{ {
Assert.DoesNotContain("\"complex\"", JsonSerializer.Serialize(tool.ProtocolTool.InputSchema)); Assert.DoesNotContain("\"complex\"", JsonSerializer.Serialize(tool.ProtocolTool.InputSchema, AIJsonUtilities.DefaultOptions));
} }
else 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; break;
} }
sc.AddMcpServer().WithToolsFromAssembly(); sc.AddMcpServer().WithToolsFromAssembly(serializerOptions: JsonContext.Default.Options);
IServiceProvider services = sc.BuildServiceProvider(); IServiceProvider services = sc.BuildServiceProvider();
McpServerTool tool = services.GetServices<McpServerTool>().First(t => t.ProtocolTool.Name == "EchoComplex"); McpServerTool tool = services.GetServices<McpServerTool>().First(t => t.ProtocolTool.Name == "EchoComplex");
if (lifetime is not null) if (lifetime is not null)
{ {
Assert.DoesNotContain("\"complex\"", JsonSerializer.Serialize(tool.ProtocolTool.InputSchema)); Assert.DoesNotContain("\"complex\"", JsonSerializer.Serialize(tool.ProtocolTool.InputSchema, AIJsonUtilities.DefaultOptions));
} }
else 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(); ServiceCollection sc = new();
sc.AddMcpServer() sc.AddMcpServer()
.WithTools<EchoTool>() .WithTools<EchoTool>(serializerOptions: JsonContext.Default.Options)
.WithTools<AnotherToolType>() .WithTools<AnotherToolType>(serializerOptions: JsonContext.Default.Options)
.WithTools(typeof(ToolTypeWithNoAttribute)); .WithTools([typeof(ToolTypeWithNoAttribute)], JsonContext.Default.Options);
IServiceProvider services = sc.BuildServiceProvider(); IServiceProvider services = sc.BuildServiceProvider();
Assert.Contains(services.GetServices<McpServerTool>(), t => t.ProtocolTool.Name == "double_echo"); Assert.Contains(services.GetServices<McpServerTool>(), t => t.ProtocolTool.Name == "double_echo");
@ -584,7 +586,7 @@ public class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
ConcurrentQueue<ProgressNotification> notifications = new(); ConcurrentQueue<ProgressNotification> notifications = new();
await using (client.RegisterNotificationHandler(NotificationMethods.ProgressNotification, (notification, cancellationToken) => 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); notifications.Enqueue(pn);
return Task.CompletedTask; return Task.CompletedTask;
})) }))
@ -598,7 +600,7 @@ public class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
}, },
cancellationToken: TestContext.Current.CancellationToken); 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)); 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"); 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() public static void DefaultOptions_UseReflectionWhenEnabled()
{ {
var options = McpJsonUtilities.DefaultOptions; var options = McpJsonUtilities.DefaultOptions;
bool isReflectionEnabled = JsonSerializer.IsReflectionEnabledByDefault;
Type anonType = new { Id = 42 }.GetType(); Type anonType = new { Id = 42 }.GetType();
Assert.True(isReflectionEnabled); // To be disabled once https://github.com/dotnet/extensions/pull/6241 is incorporated Assert.Equal(JsonSerializer.IsReflectionEnabledByDefault, options.TryGetTypeInfo(anonType, out _));
Assert.Equal(isReflectionEnabled, options.TryGetTypeInfo(anonType, out _));
} }
} }

View file

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

View file

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

View file

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

View file

@ -2,6 +2,7 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using ModelContextProtocol.Protocol.Types; using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Server; using ModelContextProtocol.Server;
using ModelContextProtocol.Utils.Json;
using Moq; using Moq;
using System.Reflection; using System.Reflection;
using System.Text.Json; using System.Text.Json;
@ -9,7 +10,7 @@ using System.Text.Json.Serialization;
namespace ModelContextProtocol.Tests.Server; namespace ModelContextProtocol.Tests.Server;
public class McpServerToolTests public partial class McpServerToolTests
{ {
[Fact] [Fact]
public void Create_InvalidArgs_Throws() public void Create_InvalidArgs_Throws()
@ -36,7 +37,7 @@ public class McpServerToolTests
return "42"; 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( var result = await tool.InvokeAsync(
new RequestContext<CallToolRequestParams>(mockServer.Object, null), new RequestContext<CallToolRequestParams>(mockServer.Object, null),
@ -86,7 +87,7 @@ public class McpServerToolTests
McpServerTool tool = services.GetRequiredService<McpServerTool>(); 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(); Mock<IMcpServer> mockServer = new();
@ -127,9 +128,11 @@ public class McpServerToolTests
[Fact] [Fact]
public async Task SupportsDisposingInstantiatedDisposableTargets() public async Task SupportsDisposingInstantiatedDisposableTargets()
{ {
McpServerToolCreateOptions options = new() { SerializerOptions = JsonContext2.Default.Options };
McpServerTool tool1 = McpServerTool.Create( McpServerTool tool1 = McpServerTool.Create(
typeof(DisposableToolType).GetMethod(nameof(DisposableToolType.InstanceMethod))!, typeof(DisposableToolType).GetMethod(nameof(DisposableToolType.InstanceMethod))!,
typeof(DisposableToolType)); typeof(DisposableToolType),
options);
var result = await tool1.InvokeAsync( var result = await tool1.InvokeAsync(
new RequestContext<CallToolRequestParams>(null!, null), new RequestContext<CallToolRequestParams>(null!, null),
@ -140,9 +143,11 @@ public class McpServerToolTests
[Fact] [Fact]
public async Task SupportsAsyncDisposingInstantiatedAsyncDisposableTargets() public async Task SupportsAsyncDisposingInstantiatedAsyncDisposableTargets()
{ {
McpServerToolCreateOptions options = new() { SerializerOptions = JsonContext2.Default.Options };
McpServerTool tool1 = McpServerTool.Create( McpServerTool tool1 = McpServerTool.Create(
typeof(AsyncDisposableToolType).GetMethod(nameof(AsyncDisposableToolType.InstanceMethod))!, typeof(AsyncDisposableToolType).GetMethod(nameof(AsyncDisposableToolType.InstanceMethod))!,
typeof(AsyncDisposableToolType)); typeof(AsyncDisposableToolType),
options);
var result = await tool1.InvokeAsync( var result = await tool1.InvokeAsync(
new RequestContext<CallToolRequestParams>(null!, null), new RequestContext<CallToolRequestParams>(null!, null),
@ -153,9 +158,11 @@ public class McpServerToolTests
[Fact] [Fact]
public async Task SupportsAsyncDisposingInstantiatedAsyncDisposableAndDisposableTargets() public async Task SupportsAsyncDisposingInstantiatedAsyncDisposableAndDisposableTargets()
{ {
McpServerToolCreateOptions options = new() { SerializerOptions = JsonContext2.Default.Options };
McpServerTool tool1 = McpServerTool.Create( McpServerTool tool1 = McpServerTool.Create(
typeof(AsyncDisposableAndDisposableToolType).GetMethod(nameof(AsyncDisposableAndDisposableToolType.InstanceMethod))!, typeof(AsyncDisposableAndDisposableToolType).GetMethod(nameof(AsyncDisposableAndDisposableToolType.InstanceMethod))!,
typeof(AsyncDisposableAndDisposableToolType)); typeof(AsyncDisposableAndDisposableToolType),
options);
var result = await tool1.InvokeAsync( var result = await tool1.InvokeAsync(
new RequestContext<CallToolRequestParams>(null!, null), new RequestContext<CallToolRequestParams>(null!, null),
@ -432,4 +439,10 @@ public class McpServerToolTests
return this; 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 chineseResult = Encoding.UTF8.GetString(output.ToArray()).Trim();
var expectedChinese = JsonSerializer.Serialize(chineseMessage, McpJsonUtilities.DefaultOptions); var expectedChinese = JsonSerializer.Serialize(chineseMessage, McpJsonUtilities.DefaultOptions);
Assert.Equal(expectedChinese, chineseResult); 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) // Test 2: Emoji (non-BMP Unicode using surrogate pairs)
var emojiText = "🔍 🚀 👍"; // Magnifying glass, rocket, thumbs up var emojiText = "🔍 🚀 👍"; // Magnifying glass, rocket, thumbs up

View file

@ -1,6 +1,7 @@
using ModelContextProtocol.Protocol.Messages; using ModelContextProtocol.Protocol.Messages;
using ModelContextProtocol.Protocol.Transport; using ModelContextProtocol.Protocol.Transport;
using ModelContextProtocol.Protocol.Types; using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Utils.Json;
using System.Text.Json; using System.Text.Json;
using System.Threading.Channels; using System.Threading.Channels;
@ -63,7 +64,7 @@ public class TestServerTransport : ITransport
Result = JsonSerializer.SerializeToNode(new ListRootsResult Result = JsonSerializer.SerializeToNode(new ListRootsResult
{ {
Roots = [] Roots = []
}), }, McpJsonUtilities.DefaultOptions),
}, cancellationToken); }, cancellationToken);
} }
@ -72,7 +73,7 @@ public class TestServerTransport : ITransport
await WriteMessageAsync(new JsonRpcResponse await WriteMessageAsync(new JsonRpcResponse
{ {
Id = request.Id, 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); }, cancellationToken);
} }