diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs index 82b6ceb9d..2caa461be 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs @@ -74,7 +74,7 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions( { Name = options?.Name ?? method.GetCustomAttribute()?.Name ?? DeriveName(method), Description = options?.Description, - MarshalResult = static (result, _, cancellationToken) => new ValueTask(result), + MarshalResult = options?.MarshalResult ?? (static (result, _, cancellationToken) => new ValueTask(result)), SerializerOptions = options?.SerializerOptions ?? McpJsonUtilities.DefaultOptions, JsonSchemaCreateOptions = options?.SchemaCreateOptions, ConfigureParameterBinding = pi => diff --git a/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs b/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs index b0b6b3de7..b51ff1afa 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs @@ -157,6 +157,16 @@ public sealed class McpServerToolCreateOptions /// public JsonSerializerOptions? SerializerOptions { get; set; } + /// + /// Gets or sets a delegate used to determine the object returned from invoking the tool, + /// based on the result of the underlying method. + /// + /// + /// The default is , which means the underlying method's result is + /// returned as-is. + /// + public Func>? MarshalResult { get; set; } + /// /// Gets or sets the JSON schema options when creating an from a method. /// @@ -214,6 +224,7 @@ internal McpServerToolCreateOptions Clone() => UseStructuredContent = UseStructuredContent, OutputSchema = OutputSchema, SerializerOptions = SerializerOptions, + MarshalResult = MarshalResult, SchemaCreateOptions = SchemaCreateOptions, Metadata = Metadata, Icons = Icons, diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs index 8fd1d9954..f17173afe 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs @@ -457,6 +457,45 @@ public async Task SupportsSchemaCreateOptions() ); } + [Fact] + public async Task SupportsCustomResultMarshaling() + { + Type? seenDeclaredResultType = null; + Mock mockServer = new(); + McpServerTool tool = McpServerTool.Create(() => 21 * 2, new() + { + Name = "marshaller", + MarshalResult = (result, declaredResultType, cancellationToken) => + { + seenDeclaredResultType = declaredResultType; + Assert.False(cancellationToken.IsCancellationRequested); + return new ValueTask($"doubled:{result}"); + }, + }); + + var result = await tool.InvokeAsync( + new RequestContext(mockServer.Object, CreateTestJsonRpcRequest(), new() { Name = "marshaller" }), + TestContext.Current.CancellationToken); + + Assert.Equal(typeof(int), seenDeclaredResultType); + var text = Assert.IsType(Assert.Single(result.Content)); + Assert.Equal("doubled:42", text.Text); + } + + [Fact] + public async Task MarshalResult_Unset_ReturnsUnderlyingResultAsIs() + { + Mock mockServer = new(); + McpServerTool tool = McpServerTool.Create(() => "42", new() { Name = "passthrough" }); + + var result = await tool.InvokeAsync( + new RequestContext(mockServer.Object, CreateTestJsonRpcRequest(), new() { Name = "passthrough" }), + TestContext.Current.CancellationToken); + + var text = Assert.IsType(Assert.Single(result.Content)); + Assert.Equal("42", text.Text); + } + [Theory] [MemberData(nameof(StructuredOutput_ReturnsExpectedSchema_Inputs))] public async Task StructuredOutput_Enabled_ReturnsExpectedSchema(T value)