Expose a JsonSerializerOptions setting in the prompts APIs (#279)
* Expose a JsonSerializerOptions setting in the prompts APIs * Change namespace for workaround converter.
This commit is contained in:
parent
a896fa8055
commit
ce4e6586d2
8 changed files with 54 additions and 19 deletions
|
|
@ -143,6 +143,7 @@ public static partial class McpServerBuilderExtensions
|
|||
/// <summary>Adds <see cref="McpServerPrompt"/> instances to the service collection backing <paramref name="builder"/>.</summary>
|
||||
/// <typeparam name="TPromptType">The prompt type.</typeparam>
|
||||
/// <param name="builder">The builder instance.</param>
|
||||
/// <param name="serializerOptions">The serializer options governing prompt parameter marshalling.</param>
|
||||
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
|
||||
/// <remarks>
|
||||
|
|
@ -154,7 +155,8 @@ public static partial class McpServerBuilderExtensions
|
|||
DynamicallyAccessedMemberTypes.PublicMethods |
|
||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
||||
DynamicallyAccessedMemberTypes.PublicConstructors)] TPromptType>(
|
||||
this IMcpServerBuilder builder)
|
||||
this IMcpServerBuilder builder,
|
||||
JsonSerializerOptions? serializerOptions = null)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
|
||||
|
|
@ -163,8 +165,8 @@ public static partial class McpServerBuilderExtensions
|
|||
if (promptMethod.GetCustomAttribute<McpServerPromptAttribute>() is not null)
|
||||
{
|
||||
builder.Services.AddSingleton((Func<IServiceProvider, McpServerPrompt>)(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 @@ public static partial class McpServerBuilderExtensions
|
|||
/// <summary>Adds <see cref="McpServerPrompt"/> instances to the service collection backing <paramref name="builder"/>.</summary>
|
||||
/// <param name="builder">The builder instance.</param>
|
||||
/// <param name="promptTypes">Types with marked methods to add as prompts to the server.</param>
|
||||
/// <param name="serializerOptions">The serializer options governing prompt parameter marshalling.</param>
|
||||
/// <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="promptTypes"/> is <see langword="null"/>.</exception>
|
||||
|
|
@ -183,7 +186,7 @@ public static partial class McpServerBuilderExtensions
|
|||
/// instance for each. For instance methods, an instance will be constructed for each invocation of the prompt.
|
||||
/// </remarks>
|
||||
[RequiresUnreferencedCode(WithPromptsRequiresUnreferencedCodeMessage)]
|
||||
public static IMcpServerBuilder WithPrompts(this IMcpServerBuilder builder, params IEnumerable<Type> promptTypes)
|
||||
public static IMcpServerBuilder WithPrompts(this IMcpServerBuilder builder, IEnumerable<Type> promptTypes, JsonSerializerOptions? serializerOptions = null)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
Throw.IfNull(promptTypes);
|
||||
|
|
@ -197,8 +200,8 @@ public static partial class McpServerBuilderExtensions
|
|||
if (promptMethod.GetCustomAttribute<McpServerPromptAttribute>() is not null)
|
||||
{
|
||||
builder.Services.AddSingleton((Func<IServiceProvider, McpServerPrompt>)(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 partial class McpServerBuilderExtensions
|
|||
/// Adds types marked with the <see cref="McpServerPromptTypeAttribute"/> attribute from the given assembly as prompts to the server.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder instance.</param>
|
||||
/// <param name="serializerOptions">The serializer options governing prompt parameter marshalling.</param>
|
||||
/// <param name="promptAssembly">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>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
|
||||
|
|
@ -235,7 +239,7 @@ public static partial class McpServerBuilderExtensions
|
|||
/// </para>
|
||||
/// </remarks>
|
||||
[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 partial class McpServerBuilderExtensions
|
|||
return builder.WithPrompts(
|
||||
from t in promptAssembly.GetTypes()
|
||||
where t.GetCustomAttribute<McpServerPromptTypeAttribute>() is not null
|
||||
select t);
|
||||
select t,
|
||||
serializerOptions);
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Text.Json.Serialization;
|
||||
using ModelContextProtocol.Utils.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ModelContextProtocol.Protocol.Types;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Text.Json.Serialization;
|
||||
using ModelContextProtocol.Utils.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ModelContextProtocol.Protocol.Types;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using ModelContextProtocol.Utils.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ModelContextProtocol.Protocol.Types;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using Microsoft.Extensions.AI;
|
|||
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 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt
|
|||
Name = options?.Name ?? method.GetCustomAttribute<McpServerPromptAttribute>()?.Name,
|
||||
Description = options?.Description,
|
||||
MarshalResult = static (result, _, cancellationToken) => new ValueTask<object?>(result),
|
||||
SerializerOptions = options?.SerializerOptions ?? McpJsonUtilities.DefaultOptions,
|
||||
ConfigureParameterBinding = pi =>
|
||||
{
|
||||
if (pi.ParameterType == typeof(RequestContext<GetPromptRequestParams>))
|
||||
|
|
@ -136,6 +138,10 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt
|
|||
Throw.IfNull(function);
|
||||
|
||||
List<PromptArgument> args = [];
|
||||
HashSet<string>? 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 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt
|
|||
{
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|||
/// </remarks>
|
||||
public string? Description { 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>
|
||||
/// Creates a shallow clone of the current <see cref="McpServerPromptCreateOptions"/> instance.
|
||||
/// </summary>
|
||||
internal McpServerPromptCreateOptions Clone() =>
|
||||
new McpServerPromptCreateOptions()
|
||||
new McpServerPromptCreateOptions
|
||||
{
|
||||
Services = Services,
|
||||
Name = Name,
|
||||
Description = Description,
|
||||
SerializerOptions = SerializerOptions,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<T>
|
||||
// This is a workaround for lack of System.Text.Json's JsonStringEnumConverter<T>
|
||||
// 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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
internal sealed class CustomizableJsonStringEnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] TEnum> :
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public sealed class CustomizableJsonStringEnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] TEnum> :
|
||||
JsonStringEnumConverter<TEnum> where TEnum : struct, Enum
|
||||
{
|
||||
#if !NET9_0_OR_GREATER
|
||||
|
|
|
|||
|
|
@ -6,13 +6,14 @@ using ModelContextProtocol.Protocol.Messages;
|
|||
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 class McpServerBuilderExtensionsPromptsTests : ClientServerTestBase
|
|||
ServiceCollection sc = new();
|
||||
sc.AddMcpServer()
|
||||
.WithPrompts<SimplePrompts>()
|
||||
.WithPrompts<MorePrompts>();
|
||||
.WithPrompts<MorePrompts>(JsonContext4.Default.Options);
|
||||
IServiceProvider services = sc.BuildServiceProvider();
|
||||
|
||||
Assert.Contains(services.GetServices<McpServerPrompt>(), t => t.ProtocolPrompt.Name == nameof(SimplePrompts.ReturnsChatMessages));
|
||||
|
|
@ -270,7 +271,7 @@ public class McpServerBuilderExtensionsPromptsTests : ClientServerTestBase
|
|||
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 McpServerBuilderExtensionsPromptsTests : ClientServerTestBase
|
|||
{
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString("N");
|
||||
}
|
||||
|
||||
[JsonSerializable(typeof(ObjectWithId))]
|
||||
[JsonSerializable(typeof(PromptMessage))]
|
||||
partial class JsonContext4 : JsonSerializerContext;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue