Skip to content

Commit e3aa54e

Browse files
Disable STJ reflection for testing. (#266)
* Disable STJ reflection for testing. * Remove params method and redundant overload. --------- Co-authored-by: Stephen Toub <[email protected]>
1 parent 45dcfe0 commit e3aa54e

18 files changed

+155
-75
lines changed

src/ModelContextProtocol/Configuration/McpServerBuilderExtensions.cs

+14-8
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using ModelContextProtocol.Utils;
99
using System.Diagnostics.CodeAnalysis;
1010
using System.Reflection;
11+
using System.Text.Json;
1112

1213
namespace Microsoft.Extensions.DependencyInjection;
1314

@@ -24,6 +25,7 @@ public static partial class McpServerBuilderExtensions
2425
/// <summary>Adds <see cref="McpServerTool"/> instances to the service collection backing <paramref name="builder"/>.</summary>
2526
/// <typeparam name="TToolType">The tool type.</typeparam>
2627
/// <param name="builder">The builder instance.</param>
28+
/// <param name="serializerOptions">The serializer options governing tool parameter marshalling.</param>
2729
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
2830
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
2931
/// <remarks>
@@ -35,7 +37,8 @@ public static partial class McpServerBuilderExtensions
3537
DynamicallyAccessedMemberTypes.PublicMethods |
3638
DynamicallyAccessedMemberTypes.NonPublicMethods |
3739
DynamicallyAccessedMemberTypes.PublicConstructors)] TToolType>(
38-
this IMcpServerBuilder builder)
40+
this IMcpServerBuilder builder,
41+
JsonSerializerOptions? serializerOptions = null)
3942
{
4043
Throw.IfNull(builder);
4144

@@ -44,8 +47,8 @@ public static partial class McpServerBuilderExtensions
4447
if (toolMethod.GetCustomAttribute<McpServerToolAttribute>() is not null)
4548
{
4649
builder.Services.AddSingleton((Func<IServiceProvider, McpServerTool>)(toolMethod.IsStatic ?
47-
services => McpServerTool.Create(toolMethod, options: new() { Services = services }) :
48-
services => McpServerTool.Create(toolMethod, typeof(TToolType), new() { Services = services })));
50+
services => McpServerTool.Create(toolMethod, options: new() { Services = services, SerializerOptions = serializerOptions }) :
51+
services => McpServerTool.Create(toolMethod, typeof(TToolType), new() { Services = services, SerializerOptions = serializerOptions })));
4952
}
5053
}
5154

@@ -55,6 +58,7 @@ public static partial class McpServerBuilderExtensions
5558
/// <summary>Adds <see cref="McpServerTool"/> instances to the service collection backing <paramref name="builder"/>.</summary>
5659
/// <param name="builder">The builder instance.</param>
5760
/// <param name="toolTypes">Types with marked methods to add as tools to the server.</param>
61+
/// <param name="serializerOptions">The serializer options governing tool parameter marshalling.</param>
5862
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
5963
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
6064
/// <exception cref="ArgumentNullException"><paramref name="toolTypes"/> is <see langword="null"/>.</exception>
@@ -64,7 +68,7 @@ public static partial class McpServerBuilderExtensions
6468
/// instance for each. For instance methods, an instance will be constructed for each invocation of the tool.
6569
/// </remarks>
6670
[RequiresUnreferencedCode(WithToolsRequiresUnreferencedCodeMessage)]
67-
public static IMcpServerBuilder WithTools(this IMcpServerBuilder builder, params IEnumerable<Type> toolTypes)
71+
public static IMcpServerBuilder WithTools(this IMcpServerBuilder builder, IEnumerable<Type> toolTypes, JsonSerializerOptions? serializerOptions = null)
6872
{
6973
Throw.IfNull(builder);
7074
Throw.IfNull(toolTypes);
@@ -78,8 +82,8 @@ public static IMcpServerBuilder WithTools(this IMcpServerBuilder builder, params
7882
if (toolMethod.GetCustomAttribute<McpServerToolAttribute>() is not null)
7983
{
8084
builder.Services.AddSingleton((Func<IServiceProvider, McpServerTool>)(toolMethod.IsStatic ?
81-
services => McpServerTool.Create(toolMethod, options: new() { Services = services }) :
82-
services => McpServerTool.Create(toolMethod, toolType, new() { Services = services })));
85+
services => McpServerTool.Create(toolMethod, options: new() { Services = services , SerializerOptions = serializerOptions }) :
86+
services => McpServerTool.Create(toolMethod, toolType, new() { Services = services , SerializerOptions = serializerOptions })));
8387
}
8488
}
8589
}
@@ -92,6 +96,7 @@ public static IMcpServerBuilder WithTools(this IMcpServerBuilder builder, params
9296
/// Adds types marked with the <see cref="McpServerToolTypeAttribute"/> attribute from the given assembly as tools to the server.
9397
/// </summary>
9498
/// <param name="builder">The builder instance.</param>
99+
/// <param name="serializerOptions">The serializer options governing tool parameter marshalling.</param>
95100
/// <param name="toolAssembly">The assembly to load the types from. If <see langword="null"/>, the calling assembly will be used.</param>
96101
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
97102
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
@@ -116,7 +121,7 @@ public static IMcpServerBuilder WithTools(this IMcpServerBuilder builder, params
116121
/// </para>
117122
/// </remarks>
118123
[RequiresUnreferencedCode(WithToolsRequiresUnreferencedCodeMessage)]
119-
public static IMcpServerBuilder WithToolsFromAssembly(this IMcpServerBuilder builder, Assembly? toolAssembly = null)
124+
public static IMcpServerBuilder WithToolsFromAssembly(this IMcpServerBuilder builder, Assembly? toolAssembly = null, JsonSerializerOptions? serializerOptions = null)
120125
{
121126
Throw.IfNull(builder);
122127

@@ -125,7 +130,8 @@ public static IMcpServerBuilder WithToolsFromAssembly(this IMcpServerBuilder bui
125130
return builder.WithTools(
126131
from t in toolAssembly.GetTypes()
127132
where t.GetCustomAttribute<McpServerToolTypeAttribute>() is not null
128-
select t);
133+
select t,
134+
serializerOptions);
129135
}
130136
#endregion
131137

src/ModelContextProtocol/Server/AIFunctionMcpServerTool.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
6767
Name = options?.Name ?? method.GetCustomAttribute<McpServerToolAttribute>()?.Name,
6868
Description = options?.Description,
6969
MarshalResult = static (result, _, cancellationToken) => new ValueTask<object?>(result),
70+
SerializerOptions = options?.SerializerOptions ?? McpJsonUtilities.DefaultOptions,
7071
ConfigureParameterBinding = pi =>
7172
{
7273
if (pi.ParameterType == typeof(RequestContext<CallToolRequestParams>))
@@ -314,7 +315,7 @@ public override async Task<CallToolResponse> InvokeAsync(
314315
{
315316
Content = [new()
316317
{
317-
Text = JsonSerializer.Serialize(result, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object))),
318+
Text = JsonSerializer.Serialize(result, AIFunction.JsonSerializerOptions.GetTypeInfo(typeof(object))),
318319
Type = "text"
319320
}]
320321
},

src/ModelContextProtocol/Server/McpServerToolCreateOptions.cs

+13-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
using ModelContextProtocol.Utils.Json;
12
using System.ComponentModel;
3+
using System.Text.Json;
24

35
namespace ModelContextProtocol.Server;
46

@@ -122,11 +124,19 @@ public sealed class McpServerToolCreateOptions
122124
/// </remarks>
123125
public bool? ReadOnly { get; set; }
124126

127+
/// <summary>
128+
/// Gets or sets the JSON serializer options to use when marshalling data to/from JSON.
129+
/// </summary>
130+
/// <remarks>
131+
/// Defaults to <see cref="McpJsonUtilities.DefaultOptions"/> if left unspecified.
132+
/// </remarks>
133+
public JsonSerializerOptions? SerializerOptions { get; set; }
134+
125135
/// <summary>
126136
/// Creates a shallow clone of the current <see cref="McpServerToolCreateOptions"/> instance.
127137
/// </summary>
128138
internal McpServerToolCreateOptions Clone() =>
129-
new McpServerToolCreateOptions()
139+
new McpServerToolCreateOptions
130140
{
131141
Services = Services,
132142
Name = Name,
@@ -135,6 +145,7 @@ internal McpServerToolCreateOptions Clone() =>
135145
Destructive = Destructive,
136146
Idempotent = Idempotent,
137147
OpenWorld = OpenWorld,
138-
ReadOnly = ReadOnly
148+
ReadOnly = ReadOnly,
149+
SerializerOptions = SerializerOptions,
139150
};
140151
}

tests/ModelContextProtocol.AspNetCore.Tests/ModelContextProtocol.AspNetCore.Tests.csproj

+5
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010
<RootNamespace>ModelContextProtocol.AspNetCore.Tests</RootNamespace>
1111
</PropertyGroup>
1212

13+
<PropertyGroup Condition="'$(TargetFramework)' == 'net9.0'">
14+
<!-- For better test coverage, only disable reflection in one of the targets -->
15+
<JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
16+
</PropertyGroup>
17+
1318
<PropertyGroup>
1419
<!-- Without this, tests are currently not showing results until all tests complete
1520
https://xunit.net/docs/getting-started/v3/microsoft-testing-platform

tests/ModelContextProtocol.AspNetCore.Tests/SseIntegrationTests.cs

+15-5
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@
1010
using ModelContextProtocol.Server;
1111
using ModelContextProtocol.Tests.Utils;
1212
using ModelContextProtocol.Utils.Json;
13+
using System.Text.Json.Serialization;
1314
using TestServerWithHosting.Tools;
1415

1516
namespace ModelContextProtocol.Tests;
1617

17-
public class SseIntegrationTests(ITestOutputHelper outputHelper) : KestrelInMemoryTest(outputHelper)
18+
public partial class SseIntegrationTests(ITestOutputHelper outputHelper) : KestrelInMemoryTest(outputHelper)
1819
{
1920
private SseClientTransportOptions DefaultTransportOptions = new()
2021
{
@@ -41,7 +42,7 @@ public async Task ConnectAndReceiveMessage_InMemoryServer()
4142
await using var mcpClient = await ConnectMcpClient(httpClient);
4243

4344
// Send a test message through POST endpoint
44-
await mcpClient.SendNotificationAsync("test/message", new { message = "Hello, SSE!" }, cancellationToken: TestContext.Current.CancellationToken);
45+
await mcpClient.SendNotificationAsync("test/message", new Envelope { Message = "Hello, SSE!" }, serializerOptions: JsonContext.Default.Options, cancellationToken: TestContext.Current.CancellationToken);
4546

4647
Assert.True(true);
4748
}
@@ -57,7 +58,7 @@ public async Task ConnectAndReceiveMessage_InMemoryServer_WithFullEndpointEventU
5758
await using var mcpClient = await ConnectMcpClient(httpClient);
5859

5960
// Send a test message through POST endpoint
60-
await mcpClient.SendNotificationAsync("test/message", new { message = "Hello, SSE!" }, cancellationToken: TestContext.Current.CancellationToken);
61+
await mcpClient.SendNotificationAsync("test/message", new Envelope { Message = "Hello, SSE!" }, serializerOptions: JsonContext.Default.Options, cancellationToken: TestContext.Current.CancellationToken);
6162

6263
Assert.True(true);
6364
}
@@ -73,7 +74,7 @@ public async Task ConnectAndReceiveNotification_InMemoryServer()
7374
mcpServer.RegisterNotificationHandler("test/notification", async (notification, cancellationToken) =>
7475
{
7576
Assert.Equal("Hello from client!", notification.Params?["message"]?.GetValue<string>());
76-
await mcpServer.SendNotificationAsync("test/notification", new { message = "Hello from server!" }, cancellationToken: cancellationToken);
77+
await mcpServer.SendNotificationAsync("test/notification", new Envelope { Message = "Hello from server!" }, serializerOptions: JsonContext.Default.Options, cancellationToken: cancellationToken);
7778
});
7879
return mcpServer.RunAsync(cancellationToken);
7980
});
@@ -90,7 +91,7 @@ public async Task ConnectAndReceiveNotification_InMemoryServer()
9091
});
9192

9293
// Send a test message through POST endpoint
93-
await mcpClient.SendNotificationAsync("test/notification", new { message = "Hello from client!" }, cancellationToken: TestContext.Current.CancellationToken);
94+
await mcpClient.SendNotificationAsync("test/notification", new Envelope { Message = "Hello from client!" }, serializerOptions: JsonContext.Default.Options, cancellationToken: TestContext.Current.CancellationToken);
9495

9596
var message = await receivedNotification.Task.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
9697
Assert.Equal("Hello from server!", message);
@@ -205,4 +206,13 @@ private static void MapAbsoluteEndpointUriMcp(IEndpointRouteBuilder endpoints)
205206
await context.Response.WriteAsync("Accepted");
206207
});
207208
}
209+
210+
public class Envelope
211+
{
212+
public required string Message { get; set; }
213+
}
214+
215+
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
216+
[JsonSerializable(typeof(Envelope))]
217+
partial class JsonContext : JsonSerializerContext;
208218
}

tests/ModelContextProtocol.TestSseServer/Program.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Microsoft.AspNetCore.Connections;
22
using ModelContextProtocol.Protocol.Types;
33
using ModelContextProtocol.Server;
4+
using ModelContextProtocol.Utils.Json;
45
using Serilog;
56
using System.Text;
67
using System.Text.Json;
@@ -124,7 +125,7 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st
124125
},
125126
"required": ["message"]
126127
}
127-
"""),
128+
""", McpJsonUtilities.DefaultOptions),
128129
},
129130
new Tool()
130131
{
@@ -145,7 +146,7 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st
145146
},
146147
"required": ["prompt", "maxTokens"]
147148
}
148-
"""),
149+
""", McpJsonUtilities.DefaultOptions),
149150
}
150151
]
151152
});

tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using ModelContextProtocol.Protocol.Messages;
66
using ModelContextProtocol.Protocol.Types;
77
using ModelContextProtocol.Server;
8+
using ModelContextProtocol.Utils.Json;
89
using Moq;
910
using System.Text.Json;
1011
using System.Text.Json.Serialization.Metadata;
@@ -379,7 +380,7 @@ public async Task AsClientLoggerProvider_MessagesSentToClient()
379380
await using (client.RegisterNotificationHandler(NotificationMethods.LoggingMessageNotification,
380381
(notification, cancellationToken) =>
381382
{
382-
Assert.True(channel.Writer.TryWrite(JsonSerializer.Deserialize<LoggingMessageNotificationParams>(notification.Params)));
383+
Assert.True(channel.Writer.TryWrite(JsonSerializer.Deserialize<LoggingMessageNotificationParams>(notification.Params, McpJsonUtilities.DefaultOptions)));
383384
return Task.CompletedTask;
384385
}))
385386
{
@@ -398,7 +399,7 @@ public async Task AsClientLoggerProvider_MessagesSentToClient()
398399

399400
Assert.Equal("TestLogger", m.Logger);
400401

401-
string ? s = JsonSerializer.Deserialize<string>(m.Data.Value);
402+
string ? s = JsonSerializer.Deserialize<string>(m.Data.Value, McpJsonUtilities.DefaultOptions);
402403
Assert.NotNull(s);
403404

404405
if (s.Contains("Information"))

tests/ModelContextProtocol.Tests/Client/McpClientFactoryTests.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using ModelContextProtocol.Protocol.Messages;
44
using ModelContextProtocol.Protocol.Transport;
55
using ModelContextProtocol.Protocol.Types;
6+
using ModelContextProtocol.Utils.Json;
67
using Moq;
78
using System.Text.Json;
89
using System.Threading.Channels;
@@ -107,7 +108,7 @@ public virtual Task SendMessageAsync(IJsonRpcMessage message, CancellationToken
107108
Name = "NopTransport",
108109
Version = "1.0.0"
109110
},
110-
}),
111+
}, McpJsonUtilities.DefaultOptions),
111112
});
112113
break;
113114
}

tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs

+15-5
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
using ModelContextProtocol.Protocol.Transport;
55
using ModelContextProtocol.Protocol.Types;
66
using ModelContextProtocol.Tests.Utils;
7+
using ModelContextProtocol.Utils.Json;
78
using OpenAI;
89
using System.Text.Json;
10+
using System.Text.Json.Serialization;
911

1012
namespace ModelContextProtocol.Tests;
1113

12-
public class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIntegrationTestFixture>
14+
public partial class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIntegrationTestFixture>
1315
{
1416
private static readonly string? s_openAIKey = Environment.GetEnvironmentVariable("AI:OpenAI:ApiKey");
1517

@@ -261,7 +263,7 @@ public async Task SubscribeResource_Stdio()
261263
[
262264
new(NotificationMethods.ResourceUpdatedNotification, (notification, cancellationToken) =>
263265
{
264-
var notificationParams = JsonSerializer.Deserialize<ResourceUpdatedNotificationParams>(notification.Params);
266+
var notificationParams = JsonSerializer.Deserialize<ResourceUpdatedNotificationParams>(notification.Params, McpJsonUtilities.DefaultOptions);
265267
tcs.TrySetResult(true);
266268
return Task.CompletedTask;
267269
})
@@ -291,7 +293,7 @@ public async Task UnsubscribeResource_Stdio()
291293
[
292294
new(NotificationMethods.ResourceUpdatedNotification, (notification, cancellationToken) =>
293295
{
294-
var notificationParams = JsonSerializer.Deserialize<ResourceUpdatedNotificationParams>(notification.Params);
296+
var notificationParams = JsonSerializer.Deserialize<ResourceUpdatedNotificationParams>(notification.Params, McpJsonUtilities.DefaultOptions);
295297
receivedNotification.TrySetResult(true);
296298
return Task.CompletedTask;
297299
})
@@ -442,13 +444,18 @@ public async Task Notifications_Stdio(string clientId)
442444

443445
// Verify we can send notifications without errors
444446
await client.SendNotificationAsync(NotificationMethods.RootsUpdatedNotification, cancellationToken: TestContext.Current.CancellationToken);
445-
await client.SendNotificationAsync("test/notification", new { test = true }, cancellationToken: TestContext.Current.CancellationToken);
447+
await client.SendNotificationAsync("test/notification", new TestNotification { Test = true }, cancellationToken: TestContext.Current.CancellationToken, serializerOptions: JsonContext3.Default.Options);
446448

447449
// assert
448450
// no response to check, if no exception is thrown, it's a success
449451
Assert.True(true);
450452
}
451453

454+
class TestNotification
455+
{
456+
public required bool Test { get; set; }
457+
}
458+
452459
[Fact]
453460
public async Task CallTool_Stdio_MemoryServer()
454461
{
@@ -557,7 +564,7 @@ public async Task SetLoggingLevel_ReceivesLoggingMessages(string clientId)
557564
[
558565
new(NotificationMethods.LoggingMessageNotification, (notification, cancellationToken) =>
559566
{
560-
var loggingMessageNotificationParameters = JsonSerializer.Deserialize<LoggingMessageNotificationParams>(notification.Params);
567+
var loggingMessageNotificationParameters = JsonSerializer.Deserialize<LoggingMessageNotificationParams>(notification.Params, McpJsonUtilities.DefaultOptions);
561568
if (loggingMessageNotificationParameters is not null)
562569
{
563570
receivedNotification.TrySetResult(true);
@@ -574,4 +581,7 @@ public async Task SetLoggingLevel_ReceivesLoggingMessages(string clientId)
574581
// assert
575582
await receivedNotification.Task;
576583
}
584+
585+
[JsonSerializable(typeof(TestNotification))]
586+
partial class JsonContext3 : JsonSerializerContext;
577587
}

0 commit comments

Comments
 (0)