Skip to content

Commit ce4e658

Browse files
Expose a JsonSerializerOptions setting in the prompts APIs (#279)
* Expose a JsonSerializerOptions setting in the prompts APIs * Change namespace for workaround converter.
1 parent a896fa8 commit ce4e658

File tree

8 files changed

+54
-19
lines changed

8 files changed

+54
-19
lines changed

src/ModelContextProtocol/Configuration/McpServerBuilderExtensions.cs

+13-8
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ where t.GetCustomAttribute<McpServerToolTypeAttribute>() is not null
143143
/// <summary>Adds <see cref="McpServerPrompt"/> instances to the service collection backing <paramref name="builder"/>.</summary>
144144
/// <typeparam name="TPromptType">The prompt type.</typeparam>
145145
/// <param name="builder">The builder instance.</param>
146+
/// <param name="serializerOptions">The serializer options governing prompt parameter marshalling.</param>
146147
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
147148
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
148149
/// <remarks>
@@ -154,7 +155,8 @@ where t.GetCustomAttribute<McpServerToolTypeAttribute>() is not null
154155
DynamicallyAccessedMemberTypes.PublicMethods |
155156
DynamicallyAccessedMemberTypes.NonPublicMethods |
156157
DynamicallyAccessedMemberTypes.PublicConstructors)] TPromptType>(
157-
this IMcpServerBuilder builder)
158+
this IMcpServerBuilder builder,
159+
JsonSerializerOptions? serializerOptions = null)
158160
{
159161
Throw.IfNull(builder);
160162

@@ -163,8 +165,8 @@ where t.GetCustomAttribute<McpServerToolTypeAttribute>() is not null
163165
if (promptMethod.GetCustomAttribute<McpServerPromptAttribute>() is not null)
164166
{
165167
builder.Services.AddSingleton((Func<IServiceProvider, McpServerPrompt>)(promptMethod.IsStatic ?
166-
services => McpServerPrompt.Create(promptMethod, options: new() { Services = services }) :
167-
services => McpServerPrompt.Create(promptMethod, typeof(TPromptType), new() { Services = services })));
168+
services => McpServerPrompt.Create(promptMethod, options: new() { Services = services, SerializerOptions = serializerOptions }) :
169+
services => McpServerPrompt.Create(promptMethod, typeof(TPromptType), new() { Services = services, SerializerOptions = serializerOptions })));
168170
}
169171
}
170172

@@ -174,6 +176,7 @@ where t.GetCustomAttribute<McpServerToolTypeAttribute>() is not null
174176
/// <summary>Adds <see cref="McpServerPrompt"/> instances to the service collection backing <paramref name="builder"/>.</summary>
175177
/// <param name="builder">The builder instance.</param>
176178
/// <param name="promptTypes">Types with marked methods to add as prompts to the server.</param>
179+
/// <param name="serializerOptions">The serializer options governing prompt parameter marshalling.</param>
177180
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
178181
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
179182
/// <exception cref="ArgumentNullException"><paramref name="promptTypes"/> is <see langword="null"/>.</exception>
@@ -183,7 +186,7 @@ where t.GetCustomAttribute<McpServerToolTypeAttribute>() is not null
183186
/// instance for each. For instance methods, an instance will be constructed for each invocation of the prompt.
184187
/// </remarks>
185188
[RequiresUnreferencedCode(WithPromptsRequiresUnreferencedCodeMessage)]
186-
public static IMcpServerBuilder WithPrompts(this IMcpServerBuilder builder, params IEnumerable<Type> promptTypes)
189+
public static IMcpServerBuilder WithPrompts(this IMcpServerBuilder builder, IEnumerable<Type> promptTypes, JsonSerializerOptions? serializerOptions = null)
187190
{
188191
Throw.IfNull(builder);
189192
Throw.IfNull(promptTypes);
@@ -197,8 +200,8 @@ public static IMcpServerBuilder WithPrompts(this IMcpServerBuilder builder, para
197200
if (promptMethod.GetCustomAttribute<McpServerPromptAttribute>() is not null)
198201
{
199202
builder.Services.AddSingleton((Func<IServiceProvider, McpServerPrompt>)(promptMethod.IsStatic ?
200-
services => McpServerPrompt.Create(promptMethod, options: new() { Services = services }) :
201-
services => McpServerPrompt.Create(promptMethod, promptType, new() { Services = services })));
203+
services => McpServerPrompt.Create(promptMethod, options: new() { Services = services, SerializerOptions = serializerOptions }) :
204+
services => McpServerPrompt.Create(promptMethod, promptType, new() { Services = services, SerializerOptions = serializerOptions })));
202205
}
203206
}
204207
}
@@ -211,6 +214,7 @@ public static IMcpServerBuilder WithPrompts(this IMcpServerBuilder builder, para
211214
/// Adds types marked with the <see cref="McpServerPromptTypeAttribute"/> attribute from the given assembly as prompts to the server.
212215
/// </summary>
213216
/// <param name="builder">The builder instance.</param>
217+
/// <param name="serializerOptions">The serializer options governing prompt parameter marshalling.</param>
214218
/// <param name="promptAssembly">The assembly to load the types from. If <see langword="null"/>, the calling assembly will be used.</param>
215219
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
216220
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
@@ -235,7 +239,7 @@ public static IMcpServerBuilder WithPrompts(this IMcpServerBuilder builder, para
235239
/// </para>
236240
/// </remarks>
237241
[RequiresUnreferencedCode(WithPromptsRequiresUnreferencedCodeMessage)]
238-
public static IMcpServerBuilder WithPromptsFromAssembly(this IMcpServerBuilder builder, Assembly? promptAssembly = null)
242+
public static IMcpServerBuilder WithPromptsFromAssembly(this IMcpServerBuilder builder, Assembly? promptAssembly = null, JsonSerializerOptions? serializerOptions = null)
239243
{
240244
Throw.IfNull(builder);
241245

@@ -244,7 +248,8 @@ public static IMcpServerBuilder WithPromptsFromAssembly(this IMcpServerBuilder b
244248
return builder.WithPrompts(
245249
from t in promptAssembly.GetTypes()
246250
where t.GetCustomAttribute<McpServerPromptTypeAttribute>() is not null
247-
select t);
251+
select t,
252+
serializerOptions);
248253
}
249254
#endregion
250255

src/ModelContextProtocol/Protocol/Types/ContextInclusion.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Text.Json.Serialization;
1+
using ModelContextProtocol.Utils.Json;
2+
using System.Text.Json.Serialization;
23

34
namespace ModelContextProtocol.Protocol.Types;
45

src/ModelContextProtocol/Protocol/Types/LoggingLevel.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Text.Json.Serialization;
1+
using ModelContextProtocol.Utils.Json;
2+
using System.Text.Json.Serialization;
23

34
namespace ModelContextProtocol.Protocol.Types;
45

src/ModelContextProtocol/Protocol/Types/Role.cs

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using ModelContextProtocol.Utils.Json;
12
using System.Text.Json.Serialization;
23

34
namespace ModelContextProtocol.Protocol.Types;

src/ModelContextProtocol/Server/AIFunctionMcpServerPrompt.cs

+7-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using Microsoft.Extensions.DependencyInjection;
33
using ModelContextProtocol.Protocol.Types;
44
using ModelContextProtocol.Utils;
5+
using ModelContextProtocol.Utils.Json;
56
using System.Diagnostics.CodeAnalysis;
67
using System.Reflection;
78
using System.Text.Json;
@@ -66,6 +67,7 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
6667
Name = options?.Name ?? method.GetCustomAttribute<McpServerPromptAttribute>()?.Name,
6768
Description = options?.Description,
6869
MarshalResult = static (result, _, cancellationToken) => new ValueTask<object?>(result),
70+
SerializerOptions = options?.SerializerOptions ?? McpJsonUtilities.DefaultOptions,
6971
ConfigureParameterBinding = pi =>
7072
{
7173
if (pi.ParameterType == typeof(RequestContext<GetPromptRequestParams>))
@@ -136,6 +138,10 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
136138
Throw.IfNull(function);
137139

138140
List<PromptArgument> args = [];
141+
HashSet<string>? requiredProps = function.JsonSchema.TryGetProperty("required", out JsonElement required)
142+
? new(required.EnumerateArray().Select(p => p.GetString()!), StringComparer.Ordinal)
143+
: null;
144+
139145
if (function.JsonSchema.TryGetProperty("properties", out JsonElement properties))
140146
{
141147
foreach (var param in properties.EnumerateObject())
@@ -144,7 +150,7 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
144150
{
145151
Name = param.Name,
146152
Description = param.Value.TryGetProperty("description", out JsonElement description) ? description.GetString() : null,
147-
Required = param.Value.TryGetProperty("required", out JsonElement required) && required.GetBoolean(),
153+
Required = requiredProps?.Contains(param.Name) ?? false,
148154
});
149155
}
150156
}

src/ModelContextProtocol/Server/McpServerPromptCreateOptions.cs

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
using ModelContextProtocol.Utils.Json;
12
using System.ComponentModel;
3+
using System.Text.Json;
24

35
namespace ModelContextProtocol.Server;
46

@@ -45,14 +47,23 @@ public sealed class McpServerPromptCreateOptions
4547
/// </remarks>
4648
public string? Description { get; set; }
4749

50+
/// <summary>
51+
/// Gets or sets the JSON serializer options to use when marshalling data to/from JSON.
52+
/// </summary>
53+
/// <remarks>
54+
/// Defaults to <see cref="McpJsonUtilities.DefaultOptions"/> if left unspecified.
55+
/// </remarks>
56+
public JsonSerializerOptions? SerializerOptions { get; set; }
57+
4858
/// <summary>
4959
/// Creates a shallow clone of the current <see cref="McpServerPromptCreateOptions"/> instance.
5060
/// </summary>
5161
internal McpServerPromptCreateOptions Clone() =>
52-
new McpServerPromptCreateOptions()
62+
new McpServerPromptCreateOptions
5363
{
5464
Services = Services,
5565
Name = Name,
5666
Description = Description,
67+
SerializerOptions = SerializerOptions,
5768
};
5869
}

src/ModelContextProtocol/Utils/Json/CustomizableJsonStringEnumConverter.cs

+9-4
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.ComponentModel;
45
using System.Diagnostics.CodeAnalysis;
56
using System.Reflection;
7+
using System.Text.Json;
8+
using System.Text.Json.Serialization;
69

710
// NOTE:
8-
// This is a temporary workaround for lack of System.Text.Json's JsonStringEnumConverter<T>
11+
// This is a workaround for lack of System.Text.Json's JsonStringEnumConverter<T>
912
// 9.x support for JsonStringEnumMemberNameAttribute. Once all builds use the System.Text.Json 9.x
10-
// version, this whole file can be removed.
13+
// version, this whole file can be removed. Note that the type is public so that external source
14+
// generators can use it, so removing it is a potential breaking change.
1115

12-
namespace System.Text.Json.Serialization;
16+
namespace ModelContextProtocol.Utils.Json;
1317

1418
/// <summary>
1519
/// A JSON converter for enums that allows customizing the serialized string value of enum members
@@ -21,7 +25,8 @@ namespace System.Text.Json.Serialization;
2125
/// 9.x support for custom enum member naming. It will be replaced by the built-in functionality
2226
/// once .NET 9 is fully adopted.
2327
/// </remarks>
24-
internal sealed class CustomizableJsonStringEnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] TEnum> :
28+
[EditorBrowsable(EditorBrowsableState.Never)]
29+
public sealed class CustomizableJsonStringEnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] TEnum> :
2530
JsonStringEnumConverter<TEnum> where TEnum : struct, Enum
2631
{
2732
#if !NET9_0_OR_GREATER

tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs

+8-3
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66
using ModelContextProtocol.Protocol.Types;
77
using ModelContextProtocol.Server;
88
using System.ComponentModel;
9+
using System.Text.Json.Serialization;
910
using System.Threading.Channels;
1011

1112
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
1213

1314
namespace ModelContextProtocol.Tests.Configuration;
1415

15-
public class McpServerBuilderExtensionsPromptsTests : ClientServerTestBase
16+
public partial class McpServerBuilderExtensionsPromptsTests : ClientServerTestBase
1617
{
1718
public McpServerBuilderExtensionsPromptsTests(ITestOutputHelper testOutputHelper)
1819
: base(testOutputHelper)
@@ -237,7 +238,7 @@ public void Register_Prompts_From_Multiple_Sources()
237238
ServiceCollection sc = new();
238239
sc.AddMcpServer()
239240
.WithPrompts<SimplePrompts>()
240-
.WithPrompts<MorePrompts>();
241+
.WithPrompts<MorePrompts>(JsonContext4.Default.Options);
241242
IServiceProvider services = sc.BuildServiceProvider();
242243

243244
Assert.Contains(services.GetServices<McpServerPrompt>(), t => t.ProtocolPrompt.Name == nameof(SimplePrompts.ReturnsChatMessages));
@@ -270,7 +271,7 @@ public string ReturnsString([Description("The first parameter")] string message)
270271
public sealed class MorePrompts
271272
{
272273
[McpServerPrompt]
273-
public static PromptMessage AnotherPrompt() =>
274+
public static PromptMessage AnotherPrompt(ObjectWithId id) =>
274275
new PromptMessage
275276
{
276277
Role = Role.User,
@@ -282,4 +283,8 @@ public class ObjectWithId
282283
{
283284
public string Id { get; set; } = Guid.NewGuid().ToString("N");
284285
}
286+
287+
[JsonSerializable(typeof(ObjectWithId))]
288+
[JsonSerializable(typeof(PromptMessage))]
289+
partial class JsonContext4 : JsonSerializerContext;
285290
}

0 commit comments

Comments
 (0)