diff --git a/src/ModelContextProtocol.Core/Server/McpServerOptions.cs b/src/ModelContextProtocol.Core/Server/McpServerOptions.cs index 618e87d5..3f323bbe 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerOptions.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerOptions.cs @@ -1,4 +1,5 @@ using ModelContextProtocol.Protocol; +using System.Text.Json; namespace ModelContextProtocol.Server; @@ -60,6 +61,41 @@ public sealed class McpServerOptions /// public string? ServerInstructions { get; set; } + /// + /// Gets or sets the default JSON serializer options to use for tools, prompts, and resources. + /// + /// + /// + /// This property provides server-wide default serialization settings that will be used + /// by all tools, prompts, and resources unless they explicitly specify their own + /// during registration. + /// + /// + /// If not set, defaults to . + /// + /// + /// This is useful for configuring settings like JsonNumberHandling.AllowNamedFloatingPointLiterals + /// to handle special floating-point values like , , + /// and . + /// + /// + public JsonSerializerOptions? JsonSerializerOptions { get; set; } + + /// + /// Gets or sets the default JSON schema creation options to use for tools, prompts, and resources. + /// + /// + /// + /// This property provides server-wide default schema creation settings that will be used + /// by all tools, prompts, and resources unless they explicitly specify their own + /// during registration. + /// + /// + /// If not set, defaults to . + /// + /// + public Microsoft.Extensions.AI.AIJsonSchemaCreateOptions? SchemaCreateOptions { get; set; } + /// /// Gets or sets whether to create a new service provider scope for each handled request. /// diff --git a/src/ModelContextProtocol/McpServerBuilderExtensions.cs b/src/ModelContextProtocol/McpServerBuilderExtensions.cs index d4c33826..693f74f6 100644 --- a/src/ModelContextProtocol/McpServerBuilderExtensions.cs +++ b/src/ModelContextProtocol/McpServerBuilderExtensions.cs @@ -1,3 +1,4 @@ +using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -24,6 +25,7 @@ public static partial class McpServerBuilderExtensions /// The tool type. /// The builder instance. /// The serializer options governing tool parameter marshalling. + /// The JSON schema creation options governing tool schema generation. /// The builder provided in . /// is . /// @@ -36,7 +38,8 @@ public static partial class McpServerBuilderExtensions DynamicallyAccessedMemberTypes.NonPublicMethods | DynamicallyAccessedMemberTypes.PublicConstructors)] TToolType>( this IMcpServerBuilder builder, - JsonSerializerOptions? serializerOptions = null) + JsonSerializerOptions? serializerOptions = null, + AIJsonSchemaCreateOptions? schemaCreateOptions = null) { Throw.IfNull(builder); @@ -45,8 +48,36 @@ public static partial class McpServerBuilderExtensions if (toolMethod.GetCustomAttribute() is not null) { builder.Services.AddSingleton((Func)(toolMethod.IsStatic ? - services => McpServerTool.Create(toolMethod, options: new() { Services = services, SerializerOptions = serializerOptions }) : - services => McpServerTool.Create(toolMethod, static r => CreateTarget(r.Services, typeof(TToolType)), new() { Services = services, SerializerOptions = serializerOptions }))); + services => + { + var effectiveSerializerOptions = serializerOptions; + var effectiveSchemaCreateOptions = schemaCreateOptions; + + // Try to get server-wide defaults if not explicitly provided + if (effectiveSerializerOptions is null || effectiveSchemaCreateOptions is null) + { + var defaultOptions = services.GetService(); + effectiveSerializerOptions ??= defaultOptions?.JsonSerializerOptions; + effectiveSchemaCreateOptions ??= defaultOptions?.SchemaCreateOptions; + } + + return McpServerTool.Create(toolMethod, options: new() { Services = services, SerializerOptions = effectiveSerializerOptions, SchemaCreateOptions = effectiveSchemaCreateOptions }); + } : + services => + { + var effectiveSerializerOptions = serializerOptions; + var effectiveSchemaCreateOptions = schemaCreateOptions; + + // Try to get server-wide defaults if not explicitly provided + if (effectiveSerializerOptions is null || effectiveSchemaCreateOptions is null) + { + var defaultOptions = services.GetService(); + effectiveSerializerOptions ??= defaultOptions?.JsonSerializerOptions; + effectiveSchemaCreateOptions ??= defaultOptions?.SchemaCreateOptions; + } + + return McpServerTool.Create(toolMethod, static r => CreateTarget(r.Services, typeof(TToolType)), new() { Services = services, SerializerOptions = effectiveSerializerOptions, SchemaCreateOptions = effectiveSchemaCreateOptions }); + })); } } @@ -58,6 +89,7 @@ public static partial class McpServerBuilderExtensions /// The builder instance. /// The target instance from which the tools should be sourced. /// The serializer options governing tool parameter marshalling. + /// The JSON schema creation options governing tool schema generation. /// The builder provided in . /// is . /// @@ -76,7 +108,8 @@ public static partial class McpServerBuilderExtensions DynamicallyAccessedMemberTypes.NonPublicMethods)] TToolType>( this IMcpServerBuilder builder, TToolType target, - JsonSerializerOptions? serializerOptions = null) + JsonSerializerOptions? serializerOptions = null, + AIJsonSchemaCreateOptions? schemaCreateOptions = null) { Throw.IfNull(builder); Throw.IfNull(target); @@ -93,7 +126,7 @@ public static partial class McpServerBuilderExtensions builder.Services.AddSingleton(services => McpServerTool.Create( toolMethod, toolMethod.IsStatic ? null : target, - new() { Services = services, SerializerOptions = serializerOptions })); + new() { Services = services, SerializerOptions = serializerOptions ?? services.GetService>()?.Value.JsonSerializerOptions , SchemaCreateOptions = schemaCreateOptions ?? services.GetService>()?.Value.SchemaCreateOptions })); } } @@ -126,6 +159,7 @@ public static IMcpServerBuilder WithTools(this IMcpServerBuilder builder, IEnume /// The builder instance. /// Types with -attributed methods to add as tools to the server. /// The serializer options governing tool parameter marshalling. + /// The JSON schema creation options governing tool schema generation. /// The builder provided in . /// is . /// is . @@ -135,7 +169,8 @@ public static IMcpServerBuilder WithTools(this IMcpServerBuilder builder, IEnume /// instance for each. For instance methods, an instance will be constructed for each invocation of the tool. /// [RequiresUnreferencedCode(WithToolsRequiresUnreferencedCodeMessage)] - public static IMcpServerBuilder WithTools(this IMcpServerBuilder builder, IEnumerable toolTypes, JsonSerializerOptions? serializerOptions = null) + public static IMcpServerBuilder WithTools(this IMcpServerBuilder builder, IEnumerable toolTypes, JsonSerializerOptions? serializerOptions = null, + AIJsonSchemaCreateOptions? schemaCreateOptions = null) { Throw.IfNull(builder); Throw.IfNull(toolTypes); @@ -149,8 +184,8 @@ public static IMcpServerBuilder WithTools(this IMcpServerBuilder builder, IEnume if (toolMethod.GetCustomAttribute() is not null) { builder.Services.AddSingleton((Func)(toolMethod.IsStatic ? - services => McpServerTool.Create(toolMethod, options: new() { Services = services , SerializerOptions = serializerOptions }) : - services => McpServerTool.Create(toolMethod, r => CreateTarget(r.Services, toolType), new() { Services = services , SerializerOptions = serializerOptions }))); + services => McpServerTool.Create(toolMethod, options: new() { Services = services , SerializerOptions = serializerOptions ?? services.GetService>()?.Value.JsonSerializerOptions , SchemaCreateOptions = schemaCreateOptions ?? services.GetService>()?.Value.SchemaCreateOptions }) : + services => McpServerTool.Create(toolMethod, r => CreateTarget(r.Services, toolType), new() { Services = services , SerializerOptions = serializerOptions ?? services.GetService>()?.Value.JsonSerializerOptions , SchemaCreateOptions = schemaCreateOptions ?? services.GetService>()?.Value.SchemaCreateOptions }))); } } } @@ -164,6 +199,7 @@ public static IMcpServerBuilder WithTools(this IMcpServerBuilder builder, IEnume /// /// The builder instance. /// The serializer options governing tool parameter marshalling. + /// The JSON schema creation options governing tool schema generation. /// The assembly to load the types from. If , the calling assembly will be used. /// The builder provided in . /// is . @@ -188,7 +224,8 @@ public static IMcpServerBuilder WithTools(this IMcpServerBuilder builder, IEnume /// /// [RequiresUnreferencedCode(WithToolsRequiresUnreferencedCodeMessage)] - public static IMcpServerBuilder WithToolsFromAssembly(this IMcpServerBuilder builder, Assembly? toolAssembly = null, JsonSerializerOptions? serializerOptions = null) + public static IMcpServerBuilder WithToolsFromAssembly(this IMcpServerBuilder builder, Assembly? toolAssembly = null, JsonSerializerOptions? serializerOptions = null, + AIJsonSchemaCreateOptions? schemaCreateOptions = null) { Throw.IfNull(builder); @@ -198,7 +235,8 @@ public static IMcpServerBuilder WithToolsFromAssembly(this IMcpServerBuilder bui from t in toolAssembly.GetTypes() where t.GetCustomAttribute() is not null select t, - serializerOptions); + serializerOptions, + schemaCreateOptions); } #endregion @@ -211,6 +249,7 @@ where t.GetCustomAttribute() is not null /// The prompt type. /// The builder instance. /// The serializer options governing prompt parameter marshalling. + /// The JSON schema creation options governing prompt schema generation. /// The builder provided in . /// is . /// @@ -223,7 +262,8 @@ where t.GetCustomAttribute() is not null DynamicallyAccessedMemberTypes.NonPublicMethods | DynamicallyAccessedMemberTypes.PublicConstructors)] TPromptType>( this IMcpServerBuilder builder, - JsonSerializerOptions? serializerOptions = null) + JsonSerializerOptions? serializerOptions = null, + AIJsonSchemaCreateOptions? schemaCreateOptions = null) { Throw.IfNull(builder); @@ -232,8 +272,8 @@ where t.GetCustomAttribute() is not null if (promptMethod.GetCustomAttribute() is not null) { builder.Services.AddSingleton((Func)(promptMethod.IsStatic ? - services => McpServerPrompt.Create(promptMethod, options: new() { Services = services, SerializerOptions = serializerOptions }) : - services => McpServerPrompt.Create(promptMethod, static r => CreateTarget(r.Services, typeof(TPromptType)), new() { Services = services, SerializerOptions = serializerOptions }))); + services => McpServerPrompt.Create(promptMethod, options: new() { Services = services, SerializerOptions = serializerOptions ?? services.GetService>()?.Value.JsonSerializerOptions , SchemaCreateOptions = schemaCreateOptions ?? services.GetService>()?.Value.SchemaCreateOptions }) : + services => McpServerPrompt.Create(promptMethod, static r => CreateTarget(r.Services, typeof(TPromptType)), new() { Services = services, SerializerOptions = serializerOptions ?? services.GetService>()?.Value.JsonSerializerOptions , SchemaCreateOptions = schemaCreateOptions ?? services.GetService>()?.Value.SchemaCreateOptions }))); } } @@ -245,6 +285,7 @@ where t.GetCustomAttribute() is not null /// The builder instance. /// The target instance from which the prompts should be sourced. /// The serializer options governing prompt parameter marshalling. + /// The JSON schema creation options governing prompt schema generation. /// The builder provided in . /// is . /// @@ -263,7 +304,8 @@ where t.GetCustomAttribute() is not null DynamicallyAccessedMemberTypes.NonPublicMethods)] TPromptType>( this IMcpServerBuilder builder, TPromptType target, - JsonSerializerOptions? serializerOptions = null) + JsonSerializerOptions? serializerOptions = null, + AIJsonSchemaCreateOptions? schemaCreateOptions = null) { Throw.IfNull(builder); Throw.IfNull(target); @@ -277,7 +319,7 @@ where t.GetCustomAttribute() is not null { if (promptMethod.GetCustomAttribute() is not null) { - builder.Services.AddSingleton(services => McpServerPrompt.Create(promptMethod, target, new() { Services = services, SerializerOptions = serializerOptions })); + builder.Services.AddSingleton(services => McpServerPrompt.Create(promptMethod, target, new() { Services = services, SerializerOptions = serializerOptions ?? services.GetService>()?.Value.JsonSerializerOptions , SchemaCreateOptions = schemaCreateOptions ?? services.GetService>()?.Value.SchemaCreateOptions })); } } @@ -310,6 +352,7 @@ public static IMcpServerBuilder WithPrompts(this IMcpServerBuilder builder, IEnu /// The builder instance. /// Types with marked methods to add as prompts to the server. /// The serializer options governing prompt parameter marshalling. + /// The JSON schema creation options governing prompt schema generation. /// The builder provided in . /// is . /// is . @@ -319,7 +362,8 @@ public static IMcpServerBuilder WithPrompts(this IMcpServerBuilder builder, IEnu /// instance for each. For instance methods, an instance will be constructed for each invocation of the prompt. /// [RequiresUnreferencedCode(WithPromptsRequiresUnreferencedCodeMessage)] - public static IMcpServerBuilder WithPrompts(this IMcpServerBuilder builder, IEnumerable promptTypes, JsonSerializerOptions? serializerOptions = null) + public static IMcpServerBuilder WithPrompts(this IMcpServerBuilder builder, IEnumerable promptTypes, JsonSerializerOptions? serializerOptions = null, + AIJsonSchemaCreateOptions? schemaCreateOptions = null) { Throw.IfNull(builder); Throw.IfNull(promptTypes); @@ -333,8 +377,8 @@ public static IMcpServerBuilder WithPrompts(this IMcpServerBuilder builder, IEnu if (promptMethod.GetCustomAttribute() is not null) { builder.Services.AddSingleton((Func)(promptMethod.IsStatic ? - services => McpServerPrompt.Create(promptMethod, options: new() { Services = services, SerializerOptions = serializerOptions }) : - services => McpServerPrompt.Create(promptMethod, r => CreateTarget(r.Services, promptType), new() { Services = services, SerializerOptions = serializerOptions }))); + services => McpServerPrompt.Create(promptMethod, options: new() { Services = services, SerializerOptions = serializerOptions ?? services.GetService>()?.Value.JsonSerializerOptions , SchemaCreateOptions = schemaCreateOptions ?? services.GetService>()?.Value.SchemaCreateOptions }) : + services => McpServerPrompt.Create(promptMethod, r => CreateTarget(r.Services, promptType), new() { Services = services, SerializerOptions = serializerOptions ?? services.GetService>()?.Value.JsonSerializerOptions , SchemaCreateOptions = schemaCreateOptions ?? services.GetService>()?.Value.SchemaCreateOptions }))); } } } @@ -348,6 +392,7 @@ public static IMcpServerBuilder WithPrompts(this IMcpServerBuilder builder, IEnu /// /// The builder instance. /// The serializer options governing prompt parameter marshalling. + /// The JSON schema creation options governing prompt schema generation. /// The assembly to load the types from. If , the calling assembly will be used. /// The builder provided in . /// is . @@ -372,7 +417,8 @@ public static IMcpServerBuilder WithPrompts(this IMcpServerBuilder builder, IEnu /// /// [RequiresUnreferencedCode(WithPromptsRequiresUnreferencedCodeMessage)] - public static IMcpServerBuilder WithPromptsFromAssembly(this IMcpServerBuilder builder, Assembly? promptAssembly = null, JsonSerializerOptions? serializerOptions = null) + public static IMcpServerBuilder WithPromptsFromAssembly(this IMcpServerBuilder builder, Assembly? promptAssembly = null, JsonSerializerOptions? serializerOptions = null, + AIJsonSchemaCreateOptions? schemaCreateOptions = null) { Throw.IfNull(builder); @@ -382,7 +428,8 @@ public static IMcpServerBuilder WithPromptsFromAssembly(this IMcpServerBuilder b from t in promptAssembly.GetTypes() where t.GetCustomAttribute() is not null select t, - serializerOptions); + serializerOptions, + schemaCreateOptions); } #endregion @@ -394,6 +441,8 @@ where t.GetCustomAttribute() is not null /// Adds instances to the service collection backing . /// The resource type. /// The builder instance. + /// The serializer options governing resource parameter marshalling. + /// The JSON schema creation options governing resource schema generation. /// The builder provided in . /// is . /// @@ -405,7 +454,9 @@ where t.GetCustomAttribute() is not null DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods | DynamicallyAccessedMemberTypes.PublicConstructors)] TResourceType>( - this IMcpServerBuilder builder) + this IMcpServerBuilder builder, + JsonSerializerOptions? serializerOptions = null, + AIJsonSchemaCreateOptions? schemaCreateOptions = null) { Throw.IfNull(builder); @@ -414,8 +465,8 @@ where t.GetCustomAttribute() is not null if (resourceTemplateMethod.GetCustomAttribute() is not null) { builder.Services.AddSingleton((Func)(resourceTemplateMethod.IsStatic ? - services => McpServerResource.Create(resourceTemplateMethod, options: new() { Services = services }) : - services => McpServerResource.Create(resourceTemplateMethod, static r => CreateTarget(r.Services, typeof(TResourceType)), new() { Services = services }))); + services => McpServerResource.Create(resourceTemplateMethod, options: new() { Services = services, SerializerOptions = serializerOptions ?? services.GetService>()?.Value.JsonSerializerOptions , SchemaCreateOptions = schemaCreateOptions ?? services.GetService>()?.Value.SchemaCreateOptions }) : + services => McpServerResource.Create(resourceTemplateMethod, static r => CreateTarget(r.Services, typeof(TResourceType)), new() { Services = services, SerializerOptions = serializerOptions ?? services.GetService>()?.Value.JsonSerializerOptions , SchemaCreateOptions = schemaCreateOptions ?? services.GetService>()?.Value.SchemaCreateOptions }))); } } @@ -426,6 +477,8 @@ where t.GetCustomAttribute() is not null /// The resource type. /// The builder instance. /// The target instance from which the prompts should be sourced. + /// The serializer options governing resource parameter marshalling. + /// The JSON schema creation options governing resource schema generation. /// The builder provided in . /// is . /// @@ -443,7 +496,9 @@ where t.GetCustomAttribute() is not null DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] TResourceType>( this IMcpServerBuilder builder, - TResourceType target) + TResourceType target, + JsonSerializerOptions? serializerOptions = null, + AIJsonSchemaCreateOptions? schemaCreateOptions = null) { Throw.IfNull(builder); Throw.IfNull(target); @@ -457,7 +512,7 @@ where t.GetCustomAttribute() is not null { if (resourceTemplateMethod.GetCustomAttribute() is not null) { - builder.Services.AddSingleton(services => McpServerResource.Create(resourceTemplateMethod, target, new() { Services = services })); + builder.Services.AddSingleton(services => McpServerResource.Create(resourceTemplateMethod, target, new() { Services = services, SerializerOptions = serializerOptions ?? services.GetService>()?.Value.JsonSerializerOptions , SchemaCreateOptions = schemaCreateOptions ?? services.GetService>()?.Value.SchemaCreateOptions })); } } @@ -489,6 +544,8 @@ public static IMcpServerBuilder WithResources(this IMcpServerBuilder builder, IE /// Adds instances to the service collection backing . /// The builder instance. /// Types with marked methods to add as resources to the server. + /// The serializer options governing resource parameter marshalling. + /// The JSON schema creation options governing resource schema generation. /// The builder provided in . /// is . /// is . @@ -498,7 +555,8 @@ public static IMcpServerBuilder WithResources(this IMcpServerBuilder builder, IE /// instance for each. For instance methods, an instance will be constructed for each invocation of the resource. /// [RequiresUnreferencedCode(WithResourcesRequiresUnreferencedCodeMessage)] - public static IMcpServerBuilder WithResources(this IMcpServerBuilder builder, IEnumerable resourceTemplateTypes) + public static IMcpServerBuilder WithResources(this IMcpServerBuilder builder, IEnumerable resourceTemplateTypes, JsonSerializerOptions? serializerOptions = null, + AIJsonSchemaCreateOptions? schemaCreateOptions = null) { Throw.IfNull(builder); Throw.IfNull(resourceTemplateTypes); @@ -512,8 +570,8 @@ public static IMcpServerBuilder WithResources(this IMcpServerBuilder builder, IE if (resourceTemplateMethod.GetCustomAttribute() is not null) { builder.Services.AddSingleton((Func)(resourceTemplateMethod.IsStatic ? - services => McpServerResource.Create(resourceTemplateMethod, options: new() { Services = services }) : - services => McpServerResource.Create(resourceTemplateMethod, r => CreateTarget(r.Services, resourceTemplateType), new() { Services = services }))); + services => McpServerResource.Create(resourceTemplateMethod, options: new() { Services = services, SerializerOptions = serializerOptions ?? services.GetService>()?.Value.JsonSerializerOptions , SchemaCreateOptions = schemaCreateOptions ?? services.GetService>()?.Value.SchemaCreateOptions }) : + services => McpServerResource.Create(resourceTemplateMethod, r => CreateTarget(r.Services, resourceTemplateType), new() { Services = services, SerializerOptions = serializerOptions ?? services.GetService>()?.Value.JsonSerializerOptions , SchemaCreateOptions = schemaCreateOptions ?? services.GetService>()?.Value.SchemaCreateOptions }))); } } } @@ -526,6 +584,8 @@ public static IMcpServerBuilder WithResources(this IMcpServerBuilder builder, IE /// Adds types marked with the attribute from the given assembly as resources to the server. /// /// The builder instance. + /// The serializer options governing resource parameter marshalling. + /// The JSON schema creation options governing resource schema generation. /// The assembly to load the types from. If , the calling assembly will be used. /// The builder provided in . /// is . @@ -550,7 +610,8 @@ public static IMcpServerBuilder WithResources(this IMcpServerBuilder builder, IE /// /// [RequiresUnreferencedCode(WithResourcesRequiresUnreferencedCodeMessage)] - public static IMcpServerBuilder WithResourcesFromAssembly(this IMcpServerBuilder builder, Assembly? resourceAssembly = null) + public static IMcpServerBuilder WithResourcesFromAssembly(this IMcpServerBuilder builder, Assembly? resourceAssembly = null, JsonSerializerOptions? serializerOptions = null, + AIJsonSchemaCreateOptions? schemaCreateOptions = null) { Throw.IfNull(builder); @@ -559,7 +620,9 @@ public static IMcpServerBuilder WithResourcesFromAssembly(this IMcpServerBuilder return builder.WithResources( from t in resourceAssembly.GetTypes() where t.GetCustomAttribute() is not null - select t); + select t, + serializerOptions, + schemaCreateOptions); } #endregion diff --git a/src/ModelContextProtocol/McpServerDefaultOptions.cs b/src/ModelContextProtocol/McpServerDefaultOptions.cs new file mode 100644 index 00000000..e1d3a1e2 --- /dev/null +++ b/src/ModelContextProtocol/McpServerDefaultOptions.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.AI; +using System.Text.Json; + +namespace ModelContextProtocol.Server; + +/// +/// Holds default options for MCP server primitives (tools, prompts, resources). +/// This is separate from McpServerOptions to avoid circular dependencies during service resolution. +/// +internal sealed class McpServerDefaultOptions +{ + /// + /// Gets or sets the default JSON serializer options. + /// + public JsonSerializerOptions? JsonSerializerOptions { get; set; } + + /// + /// Gets or sets the default JSON schema creation options. + /// + public AIJsonSchemaCreateOptions? SchemaCreateOptions { get; set; } +} diff --git a/src/ModelContextProtocol/McpServerServiceCollectionExtensions.cs b/src/ModelContextProtocol/McpServerServiceCollectionExtensions.cs index d0072002..9910ce84 100644 --- a/src/ModelContextProtocol/McpServerServiceCollectionExtensions.cs +++ b/src/ModelContextProtocol/McpServerServiceCollectionExtensions.cs @@ -21,10 +21,23 @@ public static IMcpServerBuilder AddMcpServer(this IServiceCollection services, A { services.AddOptions(); services.TryAddEnumerable(ServiceDescriptor.Transient, McpServerOptionsSetup>()); + + // Capture default options from the configuration callback to avoid circular dependencies + // when resolving IOptions from within tool/prompt/resource factories + var defaultOptions = new McpServerDefaultOptions(); if (configureOptions is not null) { + var tempOptions = new McpServerOptions(); + configureOptions(tempOptions); + defaultOptions.JsonSerializerOptions = tempOptions.JsonSerializerOptions; + defaultOptions.SchemaCreateOptions = tempOptions.SchemaCreateOptions; + services.Configure(configureOptions); } + + // Register the default options as a singleton that can be safely resolved + // without circular dependencies + services.TryAddSingleton(defaultOptions); return new DefaultMcpServerBuilder(services); } diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerJsonSerializerOptionsTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerJsonSerializerOptionsTests.cs new file mode 100644 index 00000000..e2cd323e --- /dev/null +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerJsonSerializerOptionsTests.cs @@ -0,0 +1,158 @@ +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace ModelContextProtocol.Tests.Configuration; + +public class McpServerJsonSerializerOptionsTests +{ + [Fact] + public void McpServerOptions_JsonSerializerOptions_DefaultsToNull() + { + // Arrange & Act + var options = new McpServerOptions(); + + // Assert + Assert.Null(options.JsonSerializerOptions); + } + + [Fact] + public void McpServerOptions_JsonSerializerOptions_CanBeSet() + { + // Arrange + var customOptions = new JsonSerializerOptions + { + NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals + }; + var options = new McpServerOptions(); + + // Act + options.JsonSerializerOptions = customOptions; + + // Assert + Assert.NotNull(options.JsonSerializerOptions); + Assert.Equal(JsonNumberHandling.AllowNamedFloatingPointLiterals, options.JsonSerializerOptions.NumberHandling); + } + + [Fact] + public void WithTools_UsesServerWideOptions_WhenNoExplicitOptionsProvided() + { + // Arrange + var services = new ServiceCollection(); + var customOptions = new JsonSerializerOptions(McpJsonUtilities.DefaultOptions) + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower + }; + + var builder = services.AddMcpServer(options => + { + options.JsonSerializerOptions = customOptions; + }); + + // Act - WithTools should pick up the server-wide options with snake_case naming policy + builder.WithTools(); + var serviceProvider = services.BuildServiceProvider(); + + // Assert - Verify the tool schema uses snake_case property naming + var tools = serviceProvider.GetServices().ToList(); + Assert.Single(tools); + + var tool = tools[0]; + Assert.Equal("ToolWithParameters", tool.ProtocolTool.Name); + + // Check that the input schema uses snake_case for property names + var inputSchema = tool.ProtocolTool.InputSchema; + + // The schema should have a "properties" object with snake_case property names + var propertiesElement = inputSchema.GetProperty("properties"); + Assert.True(propertiesElement.TryGetProperty("my_parameter", out _), "Schema should have 'my_parameter' property (snake_case)"); + Assert.False(propertiesElement.TryGetProperty("MyParameter", out _), "Schema should not have 'MyParameter' property (PascalCase)"); + } + + [Fact] + public void WithPrompts_UsesServerWideOptions_WhenNoExplicitOptionsProvided() + { + // Arrange + var services = new ServiceCollection(); + var customOptions = new JsonSerializerOptions(McpJsonUtilities.DefaultOptions) + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower + }; + + var builder = services.AddMcpServer(options => + { + options.JsonSerializerOptions = customOptions; + }); + + // Act - WithPrompts should pick up the server-wide options with snake_case naming policy + builder.WithPrompts(); + var serviceProvider = services.BuildServiceProvider(); + + // Assert - Verify the prompt schema uses snake_case property naming + var prompts = serviceProvider.GetServices().ToList(); + Assert.Single(prompts); + + var prompt = prompts[0]; + Assert.Equal("PromptWithParameters", prompt.ProtocolPrompt.Name); + + // Check that the arguments schema uses snake_case for property names + var arguments = prompt.ProtocolPrompt.Arguments; + Assert.NotNull(arguments); + Assert.Single(arguments); + Assert.Equal("my_argument", arguments[0].Name); + } + + [Fact] + public void WithResources_UsesServerWideOptions_WhenNoExplicitOptionsProvided() + { + // Arrange + var services = new ServiceCollection(); + var customOptions = new JsonSerializerOptions(McpJsonUtilities.DefaultOptions) + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower + }; + + var builder = services.AddMcpServer(options => + { + options.JsonSerializerOptions = customOptions; + }); + + // Act - WithResources should pick up the server-wide options with snake_case naming policy + builder.WithResources(); + var serviceProvider = services.BuildServiceProvider(); + + // Assert - Verify the resource was registered (resources don't expose schema in the same way) + var resources = serviceProvider.GetServices().ToList(); + Assert.Single(resources); + + var resource = resources[0]; + Assert.Equal("resource://test/{myParameter}", resource.ProtocolResourceTemplate.UriTemplate); + } + + [McpServerToolType] + private class TestTools + { + [McpServerTool] + public static string ToolWithParameters(string myParameter) => myParameter; + } + + [McpServerPromptType] + private class TestPrompts + { + [McpServerPrompt] + public static ChatMessage PromptWithParameters(string myArgument) => + new(ChatRole.User, $"Prompt with: {myArgument}"); + } + + [McpServerResourceType] + private class TestResources + { + [McpServerResource(UriTemplate = "resource://test/{myParameter}")] + public static string ResourceWithParameters(string myParameter) => + $"Resource content: {myParameter}"; + } +}