Skip to content

Commit 2f84c3e

Browse files
Add JsonSerializerOptions property to McpServerOptions and update builder extensions
Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>
1 parent 958d1fd commit 2f84c3e

File tree

3 files changed

+142
-20
lines changed

3 files changed

+142
-20
lines changed

src/ModelContextProtocol.Core/Server/McpServerOptions.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using ModelContextProtocol.Protocol;
2+
using System.Text.Json;
23

34
namespace ModelContextProtocol.Server;
45

@@ -60,6 +61,26 @@ public sealed class McpServerOptions
6061
/// </remarks>
6162
public string? ServerInstructions { get; set; }
6263

64+
/// <summary>
65+
/// Gets or sets the default JSON serializer options to use for tools, prompts, and resources.
66+
/// </summary>
67+
/// <remarks>
68+
/// <para>
69+
/// This property provides server-wide default serialization settings that will be used
70+
/// by all tools, prompts, and resources unless they explicitly specify their own
71+
/// <see cref="JsonSerializerOptions"/> during registration.
72+
/// </para>
73+
/// <para>
74+
/// If not set, defaults to <see cref="McpJsonUtilities.DefaultOptions"/>.
75+
/// </para>
76+
/// <para>
77+
/// This is useful for configuring settings like <c>JsonNumberHandling.AllowNamedFloatingPointLiterals</c>
78+
/// to handle special floating-point values like <see cref="double.PositiveInfinity"/>, <see cref="double.NegativeInfinity"/>,
79+
/// and <see cref="double.NaN"/>.
80+
/// </para>
81+
/// </remarks>
82+
public JsonSerializerOptions? JsonSerializerOptions { get; set; }
83+
6384
/// <summary>
6485
/// Gets or sets whether to create a new service provider scope for each handled request.
6586
/// </summary>

src/ModelContextProtocol/McpServerBuilderExtensions.cs

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ public static partial class McpServerBuilderExtensions
4545
if (toolMethod.GetCustomAttribute<McpServerToolAttribute>() is not null)
4646
{
4747
builder.Services.AddSingleton((Func<IServiceProvider, McpServerTool>)(toolMethod.IsStatic ?
48-
services => McpServerTool.Create(toolMethod, options: new() { Services = services, SerializerOptions = serializerOptions }) :
49-
services => McpServerTool.Create(toolMethod, static r => CreateTarget(r.Services, typeof(TToolType)), new() { Services = services, SerializerOptions = serializerOptions })));
48+
services => McpServerTool.Create(toolMethod, options: new() { Services = services, SerializerOptions = serializerOptions ?? services.GetRequiredService<IOptions<McpServerOptions>>().Value.JsonSerializerOptions }) :
49+
services => McpServerTool.Create(toolMethod, static r => CreateTarget(r.Services, typeof(TToolType)), new() { Services = services, SerializerOptions = serializerOptions ?? services.GetRequiredService<IOptions<McpServerOptions>>().Value.JsonSerializerOptions })));
5050
}
5151
}
5252

@@ -93,7 +93,7 @@ public static partial class McpServerBuilderExtensions
9393
builder.Services.AddSingleton(services => McpServerTool.Create(
9494
toolMethod,
9595
toolMethod.IsStatic ? null : target,
96-
new() { Services = services, SerializerOptions = serializerOptions }));
96+
new() { Services = services, SerializerOptions = serializerOptions ?? services.GetRequiredService<IOptions<McpServerOptions>>().Value.JsonSerializerOptions }));
9797
}
9898
}
9999

@@ -149,8 +149,8 @@ public static IMcpServerBuilder WithTools(this IMcpServerBuilder builder, IEnume
149149
if (toolMethod.GetCustomAttribute<McpServerToolAttribute>() is not null)
150150
{
151151
builder.Services.AddSingleton((Func<IServiceProvider, McpServerTool>)(toolMethod.IsStatic ?
152-
services => McpServerTool.Create(toolMethod, options: new() { Services = services , SerializerOptions = serializerOptions }) :
153-
services => McpServerTool.Create(toolMethod, r => CreateTarget(r.Services, toolType), new() { Services = services , SerializerOptions = serializerOptions })));
152+
services => McpServerTool.Create(toolMethod, options: new() { Services = services , SerializerOptions = serializerOptions ?? services.GetRequiredService<IOptions<McpServerOptions>>().Value.JsonSerializerOptions }) :
153+
services => McpServerTool.Create(toolMethod, r => CreateTarget(r.Services, toolType), new() { Services = services , SerializerOptions = serializerOptions ?? services.GetRequiredService<IOptions<McpServerOptions>>().Value.JsonSerializerOptions })));
154154
}
155155
}
156156
}
@@ -232,8 +232,8 @@ where t.GetCustomAttribute<McpServerToolTypeAttribute>() is not null
232232
if (promptMethod.GetCustomAttribute<McpServerPromptAttribute>() is not null)
233233
{
234234
builder.Services.AddSingleton((Func<IServiceProvider, McpServerPrompt>)(promptMethod.IsStatic ?
235-
services => McpServerPrompt.Create(promptMethod, options: new() { Services = services, SerializerOptions = serializerOptions }) :
236-
services => McpServerPrompt.Create(promptMethod, static r => CreateTarget(r.Services, typeof(TPromptType)), new() { Services = services, SerializerOptions = serializerOptions })));
235+
services => McpServerPrompt.Create(promptMethod, options: new() { Services = services, SerializerOptions = serializerOptions ?? services.GetRequiredService<IOptions<McpServerOptions>>().Value.JsonSerializerOptions }) :
236+
services => McpServerPrompt.Create(promptMethod, static r => CreateTarget(r.Services, typeof(TPromptType)), new() { Services = services, SerializerOptions = serializerOptions ?? services.GetRequiredService<IOptions<McpServerOptions>>().Value.JsonSerializerOptions })));
237237
}
238238
}
239239

@@ -277,7 +277,7 @@ where t.GetCustomAttribute<McpServerToolTypeAttribute>() is not null
277277
{
278278
if (promptMethod.GetCustomAttribute<McpServerPromptAttribute>() is not null)
279279
{
280-
builder.Services.AddSingleton(services => McpServerPrompt.Create(promptMethod, target, new() { Services = services, SerializerOptions = serializerOptions }));
280+
builder.Services.AddSingleton(services => McpServerPrompt.Create(promptMethod, target, new() { Services = services, SerializerOptions = serializerOptions ?? services.GetRequiredService<IOptions<McpServerOptions>>().Value.JsonSerializerOptions }));
281281
}
282282
}
283283

@@ -333,8 +333,8 @@ public static IMcpServerBuilder WithPrompts(this IMcpServerBuilder builder, IEnu
333333
if (promptMethod.GetCustomAttribute<McpServerPromptAttribute>() is not null)
334334
{
335335
builder.Services.AddSingleton((Func<IServiceProvider, McpServerPrompt>)(promptMethod.IsStatic ?
336-
services => McpServerPrompt.Create(promptMethod, options: new() { Services = services, SerializerOptions = serializerOptions }) :
337-
services => McpServerPrompt.Create(promptMethod, r => CreateTarget(r.Services, promptType), new() { Services = services, SerializerOptions = serializerOptions })));
336+
services => McpServerPrompt.Create(promptMethod, options: new() { Services = services, SerializerOptions = serializerOptions ?? services.GetRequiredService<IOptions<McpServerOptions>>().Value.JsonSerializerOptions }) :
337+
services => McpServerPrompt.Create(promptMethod, r => CreateTarget(r.Services, promptType), new() { Services = services, SerializerOptions = serializerOptions ?? services.GetRequiredService<IOptions<McpServerOptions>>().Value.JsonSerializerOptions })));
338338
}
339339
}
340340
}
@@ -394,6 +394,7 @@ where t.GetCustomAttribute<McpServerPromptTypeAttribute>() is not null
394394
/// <summary>Adds <see cref="McpServerResource"/> instances to the service collection backing <paramref name="builder"/>.</summary>
395395
/// <typeparam name="TResourceType">The resource type.</typeparam>
396396
/// <param name="builder">The builder instance.</param>
397+
/// <param name="serializerOptions">The serializer options governing resource parameter marshalling.</param>
397398
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
398399
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
399400
/// <remarks>
@@ -405,7 +406,8 @@ where t.GetCustomAttribute<McpServerPromptTypeAttribute>() is not null
405406
DynamicallyAccessedMemberTypes.PublicMethods |
406407
DynamicallyAccessedMemberTypes.NonPublicMethods |
407408
DynamicallyAccessedMemberTypes.PublicConstructors)] TResourceType>(
408-
this IMcpServerBuilder builder)
409+
this IMcpServerBuilder builder,
410+
JsonSerializerOptions? serializerOptions = null)
409411
{
410412
Throw.IfNull(builder);
411413

@@ -414,8 +416,8 @@ where t.GetCustomAttribute<McpServerPromptTypeAttribute>() is not null
414416
if (resourceTemplateMethod.GetCustomAttribute<McpServerResourceAttribute>() is not null)
415417
{
416418
builder.Services.AddSingleton((Func<IServiceProvider, McpServerResource>)(resourceTemplateMethod.IsStatic ?
417-
services => McpServerResource.Create(resourceTemplateMethod, options: new() { Services = services }) :
418-
services => McpServerResource.Create(resourceTemplateMethod, static r => CreateTarget(r.Services, typeof(TResourceType)), new() { Services = services })));
419+
services => McpServerResource.Create(resourceTemplateMethod, options: new() { Services = services, SerializerOptions = serializerOptions ?? services.GetRequiredService<IOptions<McpServerOptions>>().Value.JsonSerializerOptions }) :
420+
services => McpServerResource.Create(resourceTemplateMethod, static r => CreateTarget(r.Services, typeof(TResourceType)), new() { Services = services, SerializerOptions = serializerOptions ?? services.GetRequiredService<IOptions<McpServerOptions>>().Value.JsonSerializerOptions })));
419421
}
420422
}
421423

@@ -426,6 +428,7 @@ where t.GetCustomAttribute<McpServerPromptTypeAttribute>() is not null
426428
/// <typeparam name="TResourceType">The resource type.</typeparam>
427429
/// <param name="builder">The builder instance.</param>
428430
/// <param name="target">The target instance from which the prompts should be sourced.</param>
431+
/// <param name="serializerOptions">The serializer options governing resource parameter marshalling.</param>
429432
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
430433
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
431434
/// <remarks>
@@ -443,7 +446,8 @@ where t.GetCustomAttribute<McpServerPromptTypeAttribute>() is not null
443446
DynamicallyAccessedMemberTypes.PublicMethods |
444447
DynamicallyAccessedMemberTypes.NonPublicMethods)] TResourceType>(
445448
this IMcpServerBuilder builder,
446-
TResourceType target)
449+
TResourceType target,
450+
JsonSerializerOptions? serializerOptions = null)
447451
{
448452
Throw.IfNull(builder);
449453
Throw.IfNull(target);
@@ -457,7 +461,7 @@ where t.GetCustomAttribute<McpServerPromptTypeAttribute>() is not null
457461
{
458462
if (resourceTemplateMethod.GetCustomAttribute<McpServerResourceAttribute>() is not null)
459463
{
460-
builder.Services.AddSingleton(services => McpServerResource.Create(resourceTemplateMethod, target, new() { Services = services }));
464+
builder.Services.AddSingleton(services => McpServerResource.Create(resourceTemplateMethod, target, new() { Services = services, SerializerOptions = serializerOptions ?? services.GetRequiredService<IOptions<McpServerOptions>>().Value.JsonSerializerOptions }));
461465
}
462466
}
463467

@@ -489,6 +493,7 @@ public static IMcpServerBuilder WithResources(this IMcpServerBuilder builder, IE
489493
/// <summary>Adds <see cref="McpServerResource"/> instances to the service collection backing <paramref name="builder"/>.</summary>
490494
/// <param name="builder">The builder instance.</param>
491495
/// <param name="resourceTemplateTypes">Types with marked methods to add as resources to the server.</param>
496+
/// <param name="serializerOptions">The serializer options governing resource parameter marshalling.</param>
492497
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
493498
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
494499
/// <exception cref="ArgumentNullException"><paramref name="resourceTemplateTypes"/> is <see langword="null"/>.</exception>
@@ -498,7 +503,7 @@ public static IMcpServerBuilder WithResources(this IMcpServerBuilder builder, IE
498503
/// instance for each. For instance methods, an instance will be constructed for each invocation of the resource.
499504
/// </remarks>
500505
[RequiresUnreferencedCode(WithResourcesRequiresUnreferencedCodeMessage)]
501-
public static IMcpServerBuilder WithResources(this IMcpServerBuilder builder, IEnumerable<Type> resourceTemplateTypes)
506+
public static IMcpServerBuilder WithResources(this IMcpServerBuilder builder, IEnumerable<Type> resourceTemplateTypes, JsonSerializerOptions? serializerOptions = null)
502507
{
503508
Throw.IfNull(builder);
504509
Throw.IfNull(resourceTemplateTypes);
@@ -512,8 +517,8 @@ public static IMcpServerBuilder WithResources(this IMcpServerBuilder builder, IE
512517
if (resourceTemplateMethod.GetCustomAttribute<McpServerResourceAttribute>() is not null)
513518
{
514519
builder.Services.AddSingleton((Func<IServiceProvider, McpServerResource>)(resourceTemplateMethod.IsStatic ?
515-
services => McpServerResource.Create(resourceTemplateMethod, options: new() { Services = services }) :
516-
services => McpServerResource.Create(resourceTemplateMethod, r => CreateTarget(r.Services, resourceTemplateType), new() { Services = services })));
520+
services => McpServerResource.Create(resourceTemplateMethod, options: new() { Services = services, SerializerOptions = serializerOptions ?? services.GetRequiredService<IOptions<McpServerOptions>>().Value.JsonSerializerOptions }) :
521+
services => McpServerResource.Create(resourceTemplateMethod, r => CreateTarget(r.Services, resourceTemplateType), new() { Services = services, SerializerOptions = serializerOptions ?? services.GetRequiredService<IOptions<McpServerOptions>>().Value.JsonSerializerOptions })));
517522
}
518523
}
519524
}
@@ -526,6 +531,7 @@ public static IMcpServerBuilder WithResources(this IMcpServerBuilder builder, IE
526531
/// Adds types marked with the <see cref="McpServerResourceTypeAttribute"/> attribute from the given assembly as resources to the server.
527532
/// </summary>
528533
/// <param name="builder">The builder instance.</param>
534+
/// <param name="serializerOptions">The serializer options governing resource parameter marshalling.</param>
529535
/// <param name="resourceAssembly">The assembly to load the types from. If <see langword="null"/>, the calling assembly will be used.</param>
530536
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
531537
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
@@ -550,7 +556,7 @@ public static IMcpServerBuilder WithResources(this IMcpServerBuilder builder, IE
550556
/// </para>
551557
/// </remarks>
552558
[RequiresUnreferencedCode(WithResourcesRequiresUnreferencedCodeMessage)]
553-
public static IMcpServerBuilder WithResourcesFromAssembly(this IMcpServerBuilder builder, Assembly? resourceAssembly = null)
559+
public static IMcpServerBuilder WithResourcesFromAssembly(this IMcpServerBuilder builder, Assembly? resourceAssembly = null, JsonSerializerOptions? serializerOptions = null)
554560
{
555561
Throw.IfNull(builder);
556562

@@ -559,7 +565,8 @@ public static IMcpServerBuilder WithResourcesFromAssembly(this IMcpServerBuilder
559565
return builder.WithResources(
560566
from t in resourceAssembly.GetTypes()
561567
where t.GetCustomAttribute<McpServerResourceTypeAttribute>() is not null
562-
select t);
568+
select t,
569+
serializerOptions);
563570
}
564571
#endregion
565572

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using Microsoft.Extensions.Options;
3+
using ModelContextProtocol.Client;
4+
using ModelContextProtocol.Protocol;
5+
using ModelContextProtocol.Server;
6+
using ModelContextProtocol.Tests.Utils;
7+
using System.ComponentModel;
8+
using System.Text.Json;
9+
using System.Text.Json.Serialization;
10+
11+
namespace ModelContextProtocol.Tests.Configuration;
12+
13+
public class McpServerJsonSerializerOptionsTests : ClientServerTestBase
14+
{
15+
public McpServerJsonSerializerOptionsTests(ITestOutputHelper testOutputHelper)
16+
: base(testOutputHelper)
17+
{
18+
}
19+
20+
private class SpecialNumbers
21+
{
22+
public double PositiveInfinity { get; set; }
23+
public double NegativeInfinity { get; set; }
24+
public double NotANumber { get; set; }
25+
}
26+
27+
protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder)
28+
{
29+
// Configure server-wide JsonSerializerOptions to allow named floating point literals
30+
var customOptions = new JsonSerializerOptions(McpJsonUtilities.DefaultOptions)
31+
{
32+
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals
33+
};
34+
35+
services.Configure<McpServerOptions>(options =>
36+
{
37+
options.JsonSerializerOptions = customOptions;
38+
});
39+
40+
// Register a tool that will use the server-wide JsonSerializerOptions
41+
// The null serializerOptions parameter should cause it to use McpServerOptions.JsonSerializerOptions
42+
services.AddSingleton(sp =>
43+
{
44+
var serverOptions = sp.GetRequiredService<IOptions<McpServerOptions>>().Value;
45+
return McpServerTool.Create(
46+
() => new SpecialNumbers
47+
{
48+
PositiveInfinity = double.PositiveInfinity,
49+
NegativeInfinity = double.NegativeInfinity,
50+
NotANumber = double.NaN
51+
},
52+
new McpServerToolCreateOptions
53+
{
54+
Name = "GetSpecialNumbers",
55+
Description = "Returns special floating point values",
56+
UseStructuredContent = true,
57+
SerializerOptions = serverOptions.JsonSerializerOptions,
58+
Services = sp
59+
});
60+
});
61+
}
62+
63+
[Fact]
64+
public async Task ServerWide_JsonSerializerOptions_Applied_To_Tools()
65+
{
66+
// Arrange
67+
McpClient client = await CreateMcpClientForServer();
68+
69+
// Act
70+
IList<McpClientTool> tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
71+
CallToolResult result = await client.CallToolAsync("GetSpecialNumbers", cancellationToken: TestContext.Current.CancellationToken);
72+
73+
// Assert
74+
Assert.NotNull(tools);
75+
Assert.Single(tools);
76+
Assert.Equal("GetSpecialNumbers", tools[0].Name);
77+
78+
// Verify the result contains structured content with special numbers
79+
Assert.NotNull(result);
80+
Assert.NotNull(result.StructuredContent);
81+
82+
var structuredContent = JsonSerializer.Deserialize<SpecialNumbers>(
83+
result.StructuredContent.ToString(),
84+
new JsonSerializerOptions(McpJsonUtilities.DefaultOptions)
85+
{
86+
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals
87+
});
88+
89+
Assert.NotNull(structuredContent);
90+
Assert.True(double.IsPositiveInfinity(structuredContent.PositiveInfinity));
91+
Assert.True(double.IsNegativeInfinity(structuredContent.NegativeInfinity));
92+
Assert.True(double.IsNaN(structuredContent.NotANumber));
93+
}
94+
}

0 commit comments

Comments
 (0)