From 28d65470f4450224079755470db7ce225952a914 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Fri, 13 Jun 2025 05:50:14 -0700 Subject: [PATCH] Update Microsoft.Extensions.AI version & expose JSON configuration on `McpServerResourceCreateOptions` (#511) * Update Microsoft.Extensions.AI version * Update Microsoft.Extensions.AI.OpenAI * Fix failing tests; add JSON configuration options to McpServerResourceCreateOptions. * Replace temporary code with `AIFunction.ReturnJsonSchema` --------- Co-authored-by: Eirik Tsarpalis --- Directory.Packages.props | 4 +-- .../McpJsonUtilities.cs | 25 +------------------ .../Server/AIFunctionMcpServerPrompt.cs | 2 +- .../Server/AIFunctionMcpServerResource.cs | 3 ++- .../Server/AIFunctionMcpServerTool.cs | 4 +-- .../Server/McpServerResourceCreateOptions.cs | 20 +++++++++++++++ .../Server/McpServerPromptTests.cs | 7 ++++++ .../Server/McpServerResourceTests.cs | 9 ++++--- .../Server/McpServerToolTests.cs | 6 +++-- 9 files changed, 45 insertions(+), 35 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index b1af09c..78133ed 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,7 +3,7 @@ true 9.0.5 10.0.0-preview.4.25258.110 - 9.5.0 + 9.6.0 @@ -50,7 +50,7 @@ all - + diff --git a/src/ModelContextProtocol.Core/McpJsonUtilities.cs b/src/ModelContextProtocol.Core/McpJsonUtilities.cs index b075e12..1f0a308 100644 --- a/src/ModelContextProtocol.Core/McpJsonUtilities.cs +++ b/src/ModelContextProtocol.Core/McpJsonUtilities.cs @@ -85,30 +85,6 @@ public static partial class McpJsonUtilities return false; // No type keyword found. } - internal static JsonElement? GetReturnSchema(this AIFunction function, AIJsonSchemaCreateOptions? schemaCreateOptions) - { - // TODO replace with https://github.com/dotnet/extensions/pull/6447 once merged. - if (function.UnderlyingMethod?.ReturnType is not Type returnType) - { - return null; - } - - if (returnType == typeof(void) || returnType == typeof(Task) || returnType == typeof(ValueTask)) - { - // Do not report an output schema for void or Task methods. - return null; - } - - if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() is Type genericTypeDef && - (genericTypeDef == typeof(Task<>) || genericTypeDef == typeof(ValueTask<>))) - { - // Extract the real type from Task or ValueTask if applicable. - returnType = returnType.GetGenericArguments()[0]; - } - - return AIJsonUtilities.CreateJsonSchema(returnType, serializerOptions: function.JsonSerializerOptions, inferenceOptions: schemaCreateOptions); - } - // Keep in sync with CreateDefaultOptions above. [JsonSourceGenerationOptions(JsonSerializerDefaults.Web, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, @@ -157,6 +133,7 @@ public static partial class McpJsonUtilities [JsonSerializable(typeof(SubscribeRequestParams))] [JsonSerializable(typeof(UnsubscribeRequestParams))] [JsonSerializable(typeof(IReadOnlyDictionary))] + [JsonSerializable(typeof(PromptMessage[]))] // Primitive types for use in consuming AIFunctions [JsonSerializable(typeof(string))] diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs index 9e1cdbc..479a000 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs @@ -71,6 +71,7 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt Description = options?.Description, MarshalResult = static (result, _, cancellationToken) => new ValueTask(result), SerializerOptions = options?.SerializerOptions ?? McpJsonUtilities.DefaultOptions, + JsonSchemaCreateOptions = options?.SchemaCreateOptions, ConfigureParameterBinding = pi => { if (pi.ParameterType == typeof(RequestContext)) @@ -151,7 +152,6 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt return null; } }, - JsonSchemaCreateOptions = options?.SchemaCreateOptions, }; /// Creates an that wraps the specified . diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs index a8c478b..7677796 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs @@ -77,7 +77,8 @@ internal sealed class AIFunctionMcpServerResource : McpServerResource Name = options?.Name ?? method.GetCustomAttribute()?.Name, Description = options?.Description, MarshalResult = static (result, _, cancellationToken) => new ValueTask(result), - SerializerOptions = McpJsonUtilities.DefaultOptions, + SerializerOptions = options?.SerializerOptions ?? McpJsonUtilities.DefaultOptions, + JsonSchemaCreateOptions = options?.SchemaCreateOptions, ConfigureParameterBinding = pi => { if (pi.ParameterType == typeof(RequestContext)) diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs index 73862dd..cbb8d97 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs @@ -86,6 +86,7 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool Description = options?.Description, MarshalResult = static (result, _, cancellationToken) => new ValueTask(result), SerializerOptions = options?.SerializerOptions ?? McpJsonUtilities.DefaultOptions, + JsonSchemaCreateOptions = options?.SchemaCreateOptions, ConfigureParameterBinding = pi => { if (pi.ParameterType == typeof(RequestContext)) @@ -166,7 +167,6 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool return null; } }, - JsonSchemaCreateOptions = options?.SchemaCreateOptions, }; /// Creates an that wraps the specified . @@ -366,7 +366,7 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool return null; } - if (function.GetReturnSchema(toolCreateOptions?.SchemaCreateOptions) is not JsonElement outputSchema) + if (function.ReturnJsonSchema is not JsonElement outputSchema) { return null; } diff --git a/src/ModelContextProtocol.Core/Server/McpServerResourceCreateOptions.cs b/src/ModelContextProtocol.Core/Server/McpServerResourceCreateOptions.cs index 6ed0d0f..191ec97 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerResourceCreateOptions.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerResourceCreateOptions.cs @@ -1,4 +1,6 @@ +using Microsoft.Extensions.AI; using System.ComponentModel; +using System.Text.Json; namespace ModelContextProtocol.Server; @@ -60,6 +62,22 @@ public sealed class McpServerResourceCreateOptions /// public string? MimeType { 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; } + + /// + /// Gets or sets the JSON schema options when creating from a method. + /// + /// + /// Defaults to if left unspecified. + /// + public AIJsonSchemaCreateOptions? SchemaCreateOptions { get; set; } + /// /// Creates a shallow clone of the current instance. /// @@ -71,5 +89,7 @@ public sealed class McpServerResourceCreateOptions Name = Name, Description = Description, MimeType = MimeType, + SerializerOptions = SerializerOptions, + SchemaCreateOptions = SchemaCreateOptions, }; } diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerPromptTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerPromptTests.cs index 4428cb1..1cd9b4e 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerPromptTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerPromptTests.cs @@ -5,6 +5,8 @@ using ModelContextProtocol.Server; using Moq; using System.ComponentModel; using System.Reflection; +using System.Text.Json; +using System.Text.Json.Nodes; namespace ModelContextProtocol.Tests.Server; @@ -324,6 +326,11 @@ public class McpServerPromptTests { TransformSchemaNode = (context, node) => { + if (node.GetValueKind() is not JsonValueKind.Object) + { + node = new JsonObject(); + } + node["description"] = "1234"; return node; } diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerResourceTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerResourceTests.cs index 8682a0e..6e43c29 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerResourceTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerResourceTests.cs @@ -451,7 +451,7 @@ public partial class McpServerResourceTests { Assert.Same(mockServer.Object, server); return new TextResourceContents() { Text = "hello" }; - }, new() { Name = "Test" }); + }, new() { Name = "Test", SerializerOptions = JsonContext6.Default.Options }); var result = await resource.ReadAsync( new RequestContext(mockServer.Object) { Params = new() { Uri = "resource://Test" } }, TestContext.Current.CancellationToken); @@ -507,7 +507,7 @@ public partial class McpServerResourceTests { Assert.Same(mockServer.Object, server); return new List() { "42", "43" }; - }, new() { Name = "Test" }); + }, new() { Name = "Test", SerializerOptions = JsonContext6.Default.Options }); var result = await resource.ReadAsync( new RequestContext(mockServer.Object) { Params = new() { Uri = "resource://Test" } }, TestContext.Current.CancellationToken); @@ -547,7 +547,7 @@ public partial class McpServerResourceTests new TextContent("hello!"), new DataContent(new byte[] { 4, 5, 6 }, "application/json"), }; - }, new() { Name = "Test" }); + }, new() { Name = "Test", SerializerOptions = JsonContext6.Default.Options }); var result = await resource.ReadAsync( new RequestContext(mockServer.Object) { Params = new() { Uri = "resource://Test" } }, TestContext.Current.CancellationToken); @@ -575,5 +575,8 @@ public partial class McpServerResourceTests [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] [JsonSerializable(typeof(DisposableResourceType))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(TextResourceContents))] partial class JsonContext6 : JsonSerializerContext; } diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs index 0cd9f61..5a390cf 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs @@ -190,7 +190,7 @@ public partial class McpServerToolTests new DataContent("data:image/png;base64,1234"), new DataContent("data:audio/wav;base64,1234") }; - }); + }, new() { SerializerOptions = JsonContext2.Default.Options }); var result = await tool.InvokeAsync( new RequestContext(mockServer.Object), @@ -288,7 +288,7 @@ public partial class McpServerToolTests { Assert.Same(mockServer.Object, server); return new List() { "42", "43" }; - }); + }, new() { SerializerOptions = JsonContext2.Default.Options }); var result = await tool.InvokeAsync( new RequestContext(mockServer.Object), TestContext.Current.CancellationToken); @@ -632,5 +632,7 @@ public partial class McpServerToolTests [JsonSerializable(typeof(AsyncDisposableToolType))] [JsonSerializable(typeof(AsyncDisposableAndDisposableToolType))] [JsonSerializable(typeof(JsonSchema))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(List))] partial class JsonContext2 : JsonSerializerContext; }