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>
|
/// <summary>Adds <see cref="McpServerPrompt"/> instances to the service collection backing <paramref name="builder"/>.</summary>
|
||||||
/// <typeparam name="TPromptType">The prompt type.</typeparam>
|
/// <typeparam name="TPromptType">The prompt type.</typeparam>
|
||||||
/// <param name="builder">The builder instance.</param>
|
/// <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>
|
/// <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="builder"/> is <see langword="null"/>.</exception>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
|
|
@ -154,7 +155,8 @@ public static partial class McpServerBuilderExtensions
|
||||||
DynamicallyAccessedMemberTypes.PublicMethods |
|
DynamicallyAccessedMemberTypes.PublicMethods |
|
||||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
||||||
DynamicallyAccessedMemberTypes.PublicConstructors)] TPromptType>(
|
DynamicallyAccessedMemberTypes.PublicConstructors)] TPromptType>(
|
||||||
this IMcpServerBuilder builder)
|
this IMcpServerBuilder builder,
|
||||||
|
JsonSerializerOptions? serializerOptions = null)
|
||||||
{
|
{
|
||||||
Throw.IfNull(builder);
|
Throw.IfNull(builder);
|
||||||
|
|
||||||
|
|
@ -163,8 +165,8 @@ public static partial class McpServerBuilderExtensions
|
||||||
if (promptMethod.GetCustomAttribute<McpServerPromptAttribute>() is not null)
|
if (promptMethod.GetCustomAttribute<McpServerPromptAttribute>() is not null)
|
||||||
{
|
{
|
||||||
builder.Services.AddSingleton((Func<IServiceProvider, McpServerPrompt>)(promptMethod.IsStatic ?
|
builder.Services.AddSingleton((Func<IServiceProvider, McpServerPrompt>)(promptMethod.IsStatic ?
|
||||||
services => McpServerPrompt.Create(promptMethod, options: new() { Services = services }) :
|
services => McpServerPrompt.Create(promptMethod, options: new() { Services = services, SerializerOptions = serializerOptions }) :
|
||||||
services => McpServerPrompt.Create(promptMethod, typeof(TPromptType), new() { Services = services })));
|
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>
|
/// <summary>Adds <see cref="McpServerPrompt"/> instances to the service collection backing <paramref name="builder"/>.</summary>
|
||||||
/// <param name="builder">The builder instance.</param>
|
/// <param name="builder">The builder instance.</param>
|
||||||
/// <param name="promptTypes">Types with marked methods to add as prompts to the server.</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>
|
/// <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="builder"/> is <see langword="null"/>.</exception>
|
||||||
/// <exception cref="ArgumentNullException"><paramref name="promptTypes"/> 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.
|
/// instance for each. For instance methods, an instance will be constructed for each invocation of the prompt.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[RequiresUnreferencedCode(WithPromptsRequiresUnreferencedCodeMessage)]
|
[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(builder);
|
||||||
Throw.IfNull(promptTypes);
|
Throw.IfNull(promptTypes);
|
||||||
|
|
@ -197,8 +200,8 @@ public static partial class McpServerBuilderExtensions
|
||||||
if (promptMethod.GetCustomAttribute<McpServerPromptAttribute>() is not null)
|
if (promptMethod.GetCustomAttribute<McpServerPromptAttribute>() is not null)
|
||||||
{
|
{
|
||||||
builder.Services.AddSingleton((Func<IServiceProvider, McpServerPrompt>)(promptMethod.IsStatic ?
|
builder.Services.AddSingleton((Func<IServiceProvider, McpServerPrompt>)(promptMethod.IsStatic ?
|
||||||
services => McpServerPrompt.Create(promptMethod, options: new() { Services = services }) :
|
services => McpServerPrompt.Create(promptMethod, options: new() { Services = services, SerializerOptions = serializerOptions }) :
|
||||||
services => McpServerPrompt.Create(promptMethod, promptType, new() { Services = services })));
|
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.
|
/// Adds types marked with the <see cref="McpServerPromptTypeAttribute"/> attribute from the given assembly as prompts to the server.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="builder">The builder instance.</param>
|
/// <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>
|
/// <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>
|
/// <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="builder"/> is <see langword="null"/>.</exception>
|
||||||
|
|
@ -235,7 +239,7 @@ public static partial class McpServerBuilderExtensions
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[RequiresUnreferencedCode(WithPromptsRequiresUnreferencedCodeMessage)]
|
[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);
|
Throw.IfNull(builder);
|
||||||
|
|
||||||
|
|
@ -244,7 +248,8 @@ public static partial class McpServerBuilderExtensions
|
||||||
return builder.WithPrompts(
|
return builder.WithPrompts(
|
||||||
from t in promptAssembly.GetTypes()
|
from t in promptAssembly.GetTypes()
|
||||||
where t.GetCustomAttribute<McpServerPromptTypeAttribute>() is not null
|
where t.GetCustomAttribute<McpServerPromptTypeAttribute>() is not null
|
||||||
select t);
|
select t,
|
||||||
|
serializerOptions);
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Text.Json.Serialization;
|
using ModelContextProtocol.Utils.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace ModelContextProtocol.Protocol.Types;
|
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;
|
namespace ModelContextProtocol.Protocol.Types;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using ModelContextProtocol.Utils.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace ModelContextProtocol.Protocol.Types;
|
namespace ModelContextProtocol.Protocol.Types;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ using Microsoft.Extensions.AI;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using ModelContextProtocol.Protocol.Types;
|
using ModelContextProtocol.Protocol.Types;
|
||||||
using ModelContextProtocol.Utils;
|
using ModelContextProtocol.Utils;
|
||||||
|
using ModelContextProtocol.Utils.Json;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
@ -66,6 +67,7 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt
|
||||||
Name = options?.Name ?? method.GetCustomAttribute<McpServerPromptAttribute>()?.Name,
|
Name = options?.Name ?? method.GetCustomAttribute<McpServerPromptAttribute>()?.Name,
|
||||||
Description = options?.Description,
|
Description = options?.Description,
|
||||||
MarshalResult = static (result, _, cancellationToken) => new ValueTask<object?>(result),
|
MarshalResult = static (result, _, cancellationToken) => new ValueTask<object?>(result),
|
||||||
|
SerializerOptions = options?.SerializerOptions ?? McpJsonUtilities.DefaultOptions,
|
||||||
ConfigureParameterBinding = pi =>
|
ConfigureParameterBinding = pi =>
|
||||||
{
|
{
|
||||||
if (pi.ParameterType == typeof(RequestContext<GetPromptRequestParams>))
|
if (pi.ParameterType == typeof(RequestContext<GetPromptRequestParams>))
|
||||||
|
|
@ -136,6 +138,10 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt
|
||||||
Throw.IfNull(function);
|
Throw.IfNull(function);
|
||||||
|
|
||||||
List<PromptArgument> args = [];
|
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))
|
if (function.JsonSchema.TryGetProperty("properties", out JsonElement properties))
|
||||||
{
|
{
|
||||||
foreach (var param in properties.EnumerateObject())
|
foreach (var param in properties.EnumerateObject())
|
||||||
|
|
@ -144,7 +150,7 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt
|
||||||
{
|
{
|
||||||
Name = param.Name,
|
Name = param.Name,
|
||||||
Description = param.Value.TryGetProperty("description", out JsonElement description) ? description.GetString() : null,
|
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.ComponentModel;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace ModelContextProtocol.Server;
|
namespace ModelContextProtocol.Server;
|
||||||
|
|
||||||
|
|
@ -45,14 +47,23 @@ public sealed class McpServerPromptCreateOptions
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public string? Description { get; set; }
|
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>
|
/// <summary>
|
||||||
/// Creates a shallow clone of the current <see cref="McpServerPromptCreateOptions"/> instance.
|
/// Creates a shallow clone of the current <see cref="McpServerPromptCreateOptions"/> instance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal McpServerPromptCreateOptions Clone() =>
|
internal McpServerPromptCreateOptions Clone() =>
|
||||||
new McpServerPromptCreateOptions()
|
new McpServerPromptCreateOptions
|
||||||
{
|
{
|
||||||
Services = Services,
|
Services = Services,
|
||||||
Name = Name,
|
Name = Name,
|
||||||
Description = Description,
|
Description = Description,
|
||||||
|
SerializerOptions = SerializerOptions,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,19 @@
|
||||||
// Licensed to the .NET Foundation under one or more agreements.
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
|
|
||||||
|
using System.ComponentModel;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
// NOTE:
|
// 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
|
// 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>
|
/// <summary>
|
||||||
/// A JSON converter for enums that allows customizing the serialized string value of enum members
|
/// 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
|
/// 9.x support for custom enum member naming. It will be replaced by the built-in functionality
|
||||||
/// once .NET 9 is fully adopted.
|
/// once .NET 9 is fully adopted.
|
||||||
/// </remarks>
|
/// </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
|
JsonStringEnumConverter<TEnum> where TEnum : struct, Enum
|
||||||
{
|
{
|
||||||
#if !NET9_0_OR_GREATER
|
#if !NET9_0_OR_GREATER
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,14 @@ using ModelContextProtocol.Protocol.Messages;
|
||||||
using ModelContextProtocol.Protocol.Types;
|
using ModelContextProtocol.Protocol.Types;
|
||||||
using ModelContextProtocol.Server;
|
using ModelContextProtocol.Server;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading.Channels;
|
using System.Threading.Channels;
|
||||||
|
|
||||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||||
|
|
||||||
namespace ModelContextProtocol.Tests.Configuration;
|
namespace ModelContextProtocol.Tests.Configuration;
|
||||||
|
|
||||||
public class McpServerBuilderExtensionsPromptsTests : ClientServerTestBase
|
public partial class McpServerBuilderExtensionsPromptsTests : ClientServerTestBase
|
||||||
{
|
{
|
||||||
public McpServerBuilderExtensionsPromptsTests(ITestOutputHelper testOutputHelper)
|
public McpServerBuilderExtensionsPromptsTests(ITestOutputHelper testOutputHelper)
|
||||||
: base(testOutputHelper)
|
: base(testOutputHelper)
|
||||||
|
|
@ -237,7 +238,7 @@ public class McpServerBuilderExtensionsPromptsTests : ClientServerTestBase
|
||||||
ServiceCollection sc = new();
|
ServiceCollection sc = new();
|
||||||
sc.AddMcpServer()
|
sc.AddMcpServer()
|
||||||
.WithPrompts<SimplePrompts>()
|
.WithPrompts<SimplePrompts>()
|
||||||
.WithPrompts<MorePrompts>();
|
.WithPrompts<MorePrompts>(JsonContext4.Default.Options);
|
||||||
IServiceProvider services = sc.BuildServiceProvider();
|
IServiceProvider services = sc.BuildServiceProvider();
|
||||||
|
|
||||||
Assert.Contains(services.GetServices<McpServerPrompt>(), t => t.ProtocolPrompt.Name == nameof(SimplePrompts.ReturnsChatMessages));
|
Assert.Contains(services.GetServices<McpServerPrompt>(), t => t.ProtocolPrompt.Name == nameof(SimplePrompts.ReturnsChatMessages));
|
||||||
|
|
@ -270,7 +271,7 @@ public class McpServerBuilderExtensionsPromptsTests : ClientServerTestBase
|
||||||
public sealed class MorePrompts
|
public sealed class MorePrompts
|
||||||
{
|
{
|
||||||
[McpServerPrompt]
|
[McpServerPrompt]
|
||||||
public static PromptMessage AnotherPrompt() =>
|
public static PromptMessage AnotherPrompt(ObjectWithId id) =>
|
||||||
new PromptMessage
|
new PromptMessage
|
||||||
{
|
{
|
||||||
Role = Role.User,
|
Role = Role.User,
|
||||||
|
|
@ -282,4 +283,8 @@ public class McpServerBuilderExtensionsPromptsTests : ClientServerTestBase
|
||||||
{
|
{
|
||||||
public string Id { get; set; } = Guid.NewGuid().ToString("N");
|
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