diff --git a/Directory.Packages.props b/Directory.Packages.props index b1af09c22..78133ed99 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 b075e1263..1f0a30898 100644 --- a/src/ModelContextProtocol.Core/McpJsonUtilities.cs +++ b/src/ModelContextProtocol.Core/McpJsonUtilities.cs @@ -85,30 +85,6 @@ internal static bool IsValidMcpToolSchema(JsonElement element) 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 @@ internal static bool IsValidMcpToolSchema(JsonElement element) [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 9e1cdbcea..479a00042 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs @@ -71,6 +71,7 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions( 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 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions( 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 a8c478bc9..767779630 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs @@ -77,7 +77,8 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions( 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 73862ddc7..cbb8d973d 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs @@ -86,6 +86,7 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions( 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 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions( return null; } }, - JsonSchemaCreateOptions = options?.SchemaCreateOptions, }; /// Creates an that wraps the specified . @@ -366,7 +366,7 @@ public override async ValueTask InvokeAsync( 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 6ed0d0f2f..191ec9784 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 @@ internal McpServerResourceCreateOptions Clone() => 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 4428cb1fd..1cd9b4ede 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerPromptTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerPromptTests.cs @@ -5,6 +5,8 @@ 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 async Task SupportsSchemaCreateOptions() { 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 8682a0ee0..6e43c293e 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerResourceTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerResourceTests.cs @@ -451,7 +451,7 @@ public async Task CanReturnResourceContents() { 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 async Task CanReturnCollectionOfStrings() { 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 async Task CanReturnCollectionOfAIContent() 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 @@ private class DisposableResourceType : IDisposable [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 0cd9f6167..5a390cfe0 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs @@ -190,7 +190,7 @@ public async Task CanReturnCollectionOfAIContent() 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 async Task CanReturnCollectionOfStrings() { 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 @@ record Person(string Name, int Age); [JsonSerializable(typeof(AsyncDisposableToolType))] [JsonSerializable(typeof(AsyncDisposableAndDisposableToolType))] [JsonSerializable(typeof(JsonSchema))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(List))] partial class JsonContext2 : JsonSerializerContext; }