diff --git a/src/ModelContextProtocol/Configuration/McpServerBuilderExtensions.cs b/src/ModelContextProtocol/Configuration/McpServerBuilderExtensions.cs index d685cc474..521c4a191 100644 --- a/src/ModelContextProtocol/Configuration/McpServerBuilderExtensions.cs +++ b/src/ModelContextProtocol/Configuration/McpServerBuilderExtensions.cs @@ -143,6 +143,7 @@ where t.GetCustomAttribute() is not null /// Adds instances to the service collection backing . /// The prompt type. /// The builder instance. + /// The serializer options governing prompt parameter marshalling. /// The builder provided in . /// is . /// @@ -154,7 +155,8 @@ where t.GetCustomAttribute() is not null DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods | DynamicallyAccessedMemberTypes.PublicConstructors)] TPromptType>( - this IMcpServerBuilder builder) + this IMcpServerBuilder builder, + JsonSerializerOptions? serializerOptions = null) { Throw.IfNull(builder); @@ -163,8 +165,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 }) : - services => McpServerPrompt.Create(promptMethod, typeof(TPromptType), new() { Services = services }))); + services => McpServerPrompt.Create(promptMethod, options: new() { Services = services, SerializerOptions = serializerOptions }) : + services => McpServerPrompt.Create(promptMethod, typeof(TPromptType), new() { Services = services, SerializerOptions = serializerOptions }))); } } @@ -174,6 +176,7 @@ where t.GetCustomAttribute() is not null /// Adds instances to the service collection backing . /// The builder instance. /// Types with marked methods to add as prompts to the server. + /// The serializer options governing prompt parameter marshalling. /// The builder provided in . /// is . /// is . @@ -183,7 +186,7 @@ where t.GetCustomAttribute() is not null /// 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, params IEnumerable promptTypes) + public static IMcpServerBuilder WithPrompts(this IMcpServerBuilder builder, IEnumerable promptTypes, JsonSerializerOptions? serializerOptions = null) { Throw.IfNull(builder); Throw.IfNull(promptTypes); @@ -197,8 +200,8 @@ public static IMcpServerBuilder WithPrompts(this IMcpServerBuilder builder, para if (promptMethod.GetCustomAttribute() is not null) { builder.Services.AddSingleton((Func)(promptMethod.IsStatic ? - services => McpServerPrompt.Create(promptMethod, options: new() { Services = services }) : - services => McpServerPrompt.Create(promptMethod, promptType, new() { Services = services }))); + services => McpServerPrompt.Create(promptMethod, options: new() { Services = services, SerializerOptions = serializerOptions }) : + services => McpServerPrompt.Create(promptMethod, promptType, new() { Services = services, SerializerOptions = serializerOptions }))); } } } @@ -211,6 +214,7 @@ public static IMcpServerBuilder WithPrompts(this IMcpServerBuilder builder, para /// Adds types marked with the attribute from the given assembly as prompts to the server. /// /// The builder instance. + /// The serializer options governing prompt parameter marshalling. /// The assembly to load the types from. If , the calling assembly will be used. /// The builder provided in . /// is . @@ -235,7 +239,7 @@ public static IMcpServerBuilder WithPrompts(this IMcpServerBuilder builder, para /// /// [RequiresUnreferencedCode(WithPromptsRequiresUnreferencedCodeMessage)] - public static IMcpServerBuilder WithPromptsFromAssembly(this IMcpServerBuilder builder, Assembly? promptAssembly = null) + public static IMcpServerBuilder WithPromptsFromAssembly(this IMcpServerBuilder builder, Assembly? promptAssembly = null, JsonSerializerOptions? serializerOptions = null) { Throw.IfNull(builder); @@ -244,7 +248,8 @@ public static IMcpServerBuilder WithPromptsFromAssembly(this IMcpServerBuilder b return builder.WithPrompts( from t in promptAssembly.GetTypes() where t.GetCustomAttribute() is not null - select t); + select t, + serializerOptions); } #endregion diff --git a/src/ModelContextProtocol/Protocol/Types/ContextInclusion.cs b/src/ModelContextProtocol/Protocol/Types/ContextInclusion.cs index 161b28416..e10791ecf 100644 --- a/src/ModelContextProtocol/Protocol/Types/ContextInclusion.cs +++ b/src/ModelContextProtocol/Protocol/Types/ContextInclusion.cs @@ -1,4 +1,5 @@ -using System.Text.Json.Serialization; +using ModelContextProtocol.Utils.Json; +using System.Text.Json.Serialization; namespace ModelContextProtocol.Protocol.Types; diff --git a/src/ModelContextProtocol/Protocol/Types/LoggingLevel.cs b/src/ModelContextProtocol/Protocol/Types/LoggingLevel.cs index c57c4c47f..7bc43559e 100644 --- a/src/ModelContextProtocol/Protocol/Types/LoggingLevel.cs +++ b/src/ModelContextProtocol/Protocol/Types/LoggingLevel.cs @@ -1,4 +1,5 @@ -using System.Text.Json.Serialization; +using ModelContextProtocol.Utils.Json; +using System.Text.Json.Serialization; namespace ModelContextProtocol.Protocol.Types; diff --git a/src/ModelContextProtocol/Protocol/Types/Role.cs b/src/ModelContextProtocol/Protocol/Types/Role.cs index de2eaae9f..5b6ee9c06 100644 --- a/src/ModelContextProtocol/Protocol/Types/Role.cs +++ b/src/ModelContextProtocol/Protocol/Types/Role.cs @@ -1,3 +1,4 @@ +using ModelContextProtocol.Utils.Json; using System.Text.Json.Serialization; namespace ModelContextProtocol.Protocol.Types; diff --git a/src/ModelContextProtocol/Server/AIFunctionMcpServerPrompt.cs b/src/ModelContextProtocol/Server/AIFunctionMcpServerPrompt.cs index 78b20ce98..7684d6253 100644 --- a/src/ModelContextProtocol/Server/AIFunctionMcpServerPrompt.cs +++ b/src/ModelContextProtocol/Server/AIFunctionMcpServerPrompt.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.DependencyInjection; using ModelContextProtocol.Protocol.Types; using ModelContextProtocol.Utils; +using ModelContextProtocol.Utils.Json; using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text.Json; @@ -66,6 +67,7 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions( Name = options?.Name ?? method.GetCustomAttribute()?.Name, Description = options?.Description, MarshalResult = static (result, _, cancellationToken) => new ValueTask(result), + SerializerOptions = options?.SerializerOptions ?? McpJsonUtilities.DefaultOptions, ConfigureParameterBinding = pi => { if (pi.ParameterType == typeof(RequestContext)) @@ -136,6 +138,10 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions( Throw.IfNull(function); List args = []; + HashSet? requiredProps = function.JsonSchema.TryGetProperty("required", out JsonElement required) + ? new(required.EnumerateArray().Select(p => p.GetString()!), StringComparer.Ordinal) + : null; + if (function.JsonSchema.TryGetProperty("properties", out JsonElement properties)) { foreach (var param in properties.EnumerateObject()) @@ -144,7 +150,7 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions( { Name = param.Name, Description = param.Value.TryGetProperty("description", out JsonElement description) ? description.GetString() : null, - Required = param.Value.TryGetProperty("required", out JsonElement required) && required.GetBoolean(), + Required = requiredProps?.Contains(param.Name) ?? false, }); } } diff --git a/src/ModelContextProtocol/Server/McpServerPromptCreateOptions.cs b/src/ModelContextProtocol/Server/McpServerPromptCreateOptions.cs index e45dd2910..40ef66211 100644 --- a/src/ModelContextProtocol/Server/McpServerPromptCreateOptions.cs +++ b/src/ModelContextProtocol/Server/McpServerPromptCreateOptions.cs @@ -1,4 +1,6 @@ +using ModelContextProtocol.Utils.Json; using System.ComponentModel; +using System.Text.Json; namespace ModelContextProtocol.Server; @@ -45,14 +47,23 @@ public sealed class McpServerPromptCreateOptions /// public string? Description { get; set; } + /// + /// Gets or sets the JSON serializer options to use when marshalling data to/from JSON. + /// + /// + /// Defaults to if left unspecified. + /// + public JsonSerializerOptions? SerializerOptions { get; set; } + /// /// Creates a shallow clone of the current instance. /// internal McpServerPromptCreateOptions Clone() => - new McpServerPromptCreateOptions() + new McpServerPromptCreateOptions { Services = Services, Name = Name, Description = Description, + SerializerOptions = SerializerOptions, }; } diff --git a/src/ModelContextProtocol/Utils/Json/CustomizableJsonStringEnumConverter.cs b/src/ModelContextProtocol/Utils/Json/CustomizableJsonStringEnumConverter.cs index 5be8d80bd..37888c267 100644 --- a/src/ModelContextProtocol/Utils/Json/CustomizableJsonStringEnumConverter.cs +++ b/src/ModelContextProtocol/Utils/Json/CustomizableJsonStringEnumConverter.cs @@ -1,15 +1,19 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; // NOTE: -// This is a temporary workaround for lack of System.Text.Json's JsonStringEnumConverter +// This is a workaround for lack of System.Text.Json's JsonStringEnumConverter // 9.x support for JsonStringEnumMemberNameAttribute. Once all builds use the System.Text.Json 9.x -// version, this whole file can be removed. +// version, this whole file can be removed. Note that the type is public so that external source +// generators can use it, so removing it is a potential breaking change. -namespace System.Text.Json.Serialization; +namespace ModelContextProtocol.Utils.Json; /// /// A JSON converter for enums that allows customizing the serialized string value of enum members @@ -21,7 +25,8 @@ namespace System.Text.Json.Serialization; /// 9.x support for custom enum member naming. It will be replaced by the built-in functionality /// once .NET 9 is fully adopted. /// -internal sealed class CustomizableJsonStringEnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] TEnum> : +[EditorBrowsable(EditorBrowsableState.Never)] +public sealed class CustomizableJsonStringEnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] TEnum> : JsonStringEnumConverter where TEnum : struct, Enum { #if !NET9_0_OR_GREATER diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs index aeef9d87b..b6a980f90 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs @@ -6,13 +6,14 @@ using ModelContextProtocol.Protocol.Types; using ModelContextProtocol.Server; using System.ComponentModel; +using System.Text.Json.Serialization; using System.Threading.Channels; #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously namespace ModelContextProtocol.Tests.Configuration; -public class McpServerBuilderExtensionsPromptsTests : ClientServerTestBase +public partial class McpServerBuilderExtensionsPromptsTests : ClientServerTestBase { public McpServerBuilderExtensionsPromptsTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) @@ -237,7 +238,7 @@ public void Register_Prompts_From_Multiple_Sources() ServiceCollection sc = new(); sc.AddMcpServer() .WithPrompts() - .WithPrompts(); + .WithPrompts(JsonContext4.Default.Options); IServiceProvider services = sc.BuildServiceProvider(); Assert.Contains(services.GetServices(), t => t.ProtocolPrompt.Name == nameof(SimplePrompts.ReturnsChatMessages)); @@ -270,7 +271,7 @@ public string ReturnsString([Description("The first parameter")] string message) public sealed class MorePrompts { [McpServerPrompt] - public static PromptMessage AnotherPrompt() => + public static PromptMessage AnotherPrompt(ObjectWithId id) => new PromptMessage { Role = Role.User, @@ -282,4 +283,8 @@ public class ObjectWithId { public string Id { get; set; } = Guid.NewGuid().ToString("N"); } + + [JsonSerializable(typeof(ObjectWithId))] + [JsonSerializable(typeof(PromptMessage))] + partial class JsonContext4 : JsonSerializerContext; }