From 044ce9342f746b5fa30d3356193055f6bdfd6ab5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 04:35:24 +0000 Subject: [PATCH 1/3] Initial plan From d53360de747093b675bca8e9d63b78a1b868e77a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 05:05:48 +0000 Subject: [PATCH 2/3] Fix McpTask double-wrapping: add McpTask case in InvokeAsync switch and bypass ExecuteToolAsTaskAsync for McpTask-returning tools Agent-Logs-Url: https://github.com/modelcontextprotocol/csharp-sdk/sessions/0b57604e-d75a-4879-a7ed-139c8e03ec6b Co-authored-by: jeffhandley <1031940+jeffhandley@users.noreply.github.com> --- .../Server/AIFunctionMcpServerTool.cs | 24 +++++ .../Server/DelegatingMcpServerTool.cs | 3 + .../Server/McpServerImpl.cs | 7 ++ .../Server/McpServerTool.cs | 4 + .../Server/ToolTaskSupportTests.cs | 101 +++++++++++++++++- 5 files changed, 138 insertions(+), 1 deletion(-) diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs index 700d9d26d..a6e1719f2 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs @@ -244,11 +244,30 @@ private AIFunctionMcpServerTool(AIFunction function, Tool tool, IServiceProvider _structuredOutputRequiresWrapping = structuredOutputRequiresWrapping; _metadata = metadata; + + // Detect if the tool's underlying method returns McpTask (directly or wrapped in Task<>/ValueTask<>). + if (function.UnderlyingMethod is { } method) + { + Type returnType = method.ReturnType; + if (returnType.IsGenericType) + { + Type gt = returnType.GetGenericTypeDefinition(); + if (gt == typeof(Task<>) || gt == typeof(ValueTask<>)) + { + returnType = returnType.GetGenericArguments()[0]; + } + } + + ReturnsMcpTask = returnType == typeof(McpTask); + } } /// public override Tool ProtocolTool { get; } + /// + internal override bool ReturnsMcpTask { get; } + /// public override IReadOnlyList Metadata => _metadata; @@ -311,6 +330,11 @@ public override async ValueTask InvokeAsync( CallToolResult callToolResponse => callToolResponse, + McpTask mcpTask => new() + { + Task = mcpTask, + }, + _ => new() { Content = [new TextContentBlock { Text = JsonSerializer.Serialize(result, AIFunction.JsonSerializerOptions.GetTypeInfo(typeof(object))) }], diff --git a/src/ModelContextProtocol.Core/Server/DelegatingMcpServerTool.cs b/src/ModelContextProtocol.Core/Server/DelegatingMcpServerTool.cs index 775930090..b7b99de43 100644 --- a/src/ModelContextProtocol.Core/Server/DelegatingMcpServerTool.cs +++ b/src/ModelContextProtocol.Core/Server/DelegatingMcpServerTool.cs @@ -23,6 +23,9 @@ protected DelegatingMcpServerTool(McpServerTool innerTool) /// public override Tool ProtocolTool => _innerTool.ProtocolTool; + /// + internal override bool ReturnsMcpTask => _innerTool.ReturnsMcpTask; + /// public override IReadOnlyList Metadata => _innerTool.Metadata; diff --git a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs index 50c988cde..5830ce7c7 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs @@ -707,6 +707,13 @@ await originalListToolsHandler(request, cancellationToken).ConfigureAwait(false) McpErrorCode.InvalidParams); } + // If the tool manages its own task lifecycle (returns McpTask), + // invoke it directly and return its result without SDK task wrapping. + if (tool.ReturnsMcpTask) + { + return await tool.InvokeAsync(request, cancellationToken).ConfigureAwait(false); + } + // Task augmentation requested - return CreateTaskResult return await ExecuteToolAsTaskAsync(tool, request, taskMetadata, taskStore, sendNotifications, cancellationToken).ConfigureAwait(false); } diff --git a/src/ModelContextProtocol.Core/Server/McpServerTool.cs b/src/ModelContextProtocol.Core/Server/McpServerTool.cs index e2a9a34e0..ebaf052e0 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerTool.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerTool.cs @@ -157,6 +157,10 @@ protected McpServerTool() /// Gets the protocol type for this instance. public abstract Tool ProtocolTool { get; } + /// Gets whether the tool's underlying method returns , indicating + /// it manages its own task lifecycle and should not be wrapped by the SDK. + internal virtual bool ReturnsMcpTask => false; + /// /// Gets the metadata for this tool instance. /// diff --git a/tests/ModelContextProtocol.Tests/Server/ToolTaskSupportTests.cs b/tests/ModelContextProtocol.Tests/Server/ToolTaskSupportTests.cs index 25db2b330..3eb70aa15 100644 --- a/tests/ModelContextProtocol.Tests/Server/ToolTaskSupportTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/ToolTaskSupportTests.cs @@ -657,7 +657,106 @@ public async Task TaskPath_Logs_Error_When_Tool_Throws() #endregion - /// + #region Tool Returning McpTask Tests + +#pragma warning disable MCPEXP001 // Tasks feature is experimental + [Fact] + public async Task Tool_ReturningMcpTask_BypassesSdkTaskWrapping() + { + // Arrange - Server with task store and a tool that creates and returns its own McpTask + var taskStore = new InMemoryMcpTaskStore(); + + await using var fixture = new ClientServerFixture( + LoggerFactory, + configureServer: builder => + { + builder.WithTools([McpServerTool.Create( + async (IMcpTaskStore store) => + { + // Tool creates its own task explicitly + var task = await store.CreateTaskAsync( + new McpTaskMetadata(), + new RequestId("tool-req"), + new JsonRpcRequest { Method = "tools/call" }); + return task; + }, + new McpServerToolCreateOptions + { + Name = "self-managing-tool", + Description = "A tool that creates and returns its own McpTask", + Execution = new ToolExecution { TaskSupport = ToolTaskSupport.Optional } + })]); + }, + configureServices: services => + { + services.AddSingleton(taskStore); + services.Configure(options => options.TaskStore = taskStore); + }); + + // Act - Call the tool with task metadata (task-augmented request) + var callResult = await fixture.Client.CallToolAsync( + new CallToolRequestParams + { + Name = "self-managing-tool", + Task = new McpTaskMetadata() + }, + cancellationToken: TestContext.Current.CancellationToken); + + // Assert - Only 1 task should exist (the tool-created one), not 2 + Assert.NotNull(callResult.Task); + + var allTasks = await fixture.Client.ListTasksAsync(cancellationToken: TestContext.Current.CancellationToken); + Assert.Single(allTasks); + Assert.Equal(callResult.Task.TaskId, allTasks[0].TaskId); + } + + [Fact] + public async Task Tool_ReturningMcpTask_WithoutTaskMetadata_ReturnsTaskDirectly() + { + // Arrange - Server with task store and a tool that returns McpTask + var taskStore = new InMemoryMcpTaskStore(); + + await using var fixture = new ClientServerFixture( + LoggerFactory, + configureServer: builder => + { + builder.WithTools([McpServerTool.Create( + async (IMcpTaskStore store) => + { + var task = await store.CreateTaskAsync( + new McpTaskMetadata(), + new RequestId("tool-req"), + new JsonRpcRequest { Method = "tools/call" }); + return task; + }, + new McpServerToolCreateOptions + { + Name = "self-managing-tool", + Description = "A tool that creates and returns its own McpTask", + Execution = new ToolExecution { TaskSupport = ToolTaskSupport.Optional } + })]); + }, + configureServices: services => + { + services.AddSingleton(taskStore); + services.Configure(options => options.TaskStore = taskStore); + }); + + // Act - Call without task metadata (normal invocation) + var callResult = await fixture.Client.CallToolAsync( + "self-managing-tool", + cancellationToken: TestContext.Current.CancellationToken); + + // Assert - Tool's McpTask should be on the result + Assert.NotNull(callResult.Task); + + var allTasks = await fixture.Client.ListTasksAsync(cancellationToken: TestContext.Current.CancellationToken); + Assert.Single(allTasks); + Assert.Equal(callResult.Task.TaskId, allTasks[0].TaskId); + } +#pragma warning restore MCPEXP001 + + #endregion /// A fixture that creates a connected MCP client-server pair for testing. /// private sealed class ClientServerFixture : IAsyncDisposable From 4b43455f3ab79fef95e9f131a97c62cb8b360a80 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 05:27:56 +0000 Subject: [PATCH 3/3] Fix tests for McpTask double-wrapping: provide DI services at tool creation time Agent-Logs-Url: https://github.com/modelcontextprotocol/csharp-sdk/sessions/0b57604e-d75a-4879-a7ed-139c8e03ec6b Co-authored-by: jeffhandley <1031940+jeffhandley@users.noreply.github.com> --- .../Server/ToolTaskSupportTests.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/ModelContextProtocol.Tests/Server/ToolTaskSupportTests.cs b/tests/ModelContextProtocol.Tests/Server/ToolTaskSupportTests.cs index 3eb70aa15..5743de631 100644 --- a/tests/ModelContextProtocol.Tests/Server/ToolTaskSupportTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/ToolTaskSupportTests.cs @@ -666,6 +666,11 @@ public async Task Tool_ReturningMcpTask_BypassesSdkTaskWrapping() // Arrange - Server with task store and a tool that creates and returns its own McpTask var taskStore = new InMemoryMcpTaskStore(); + // Build a service provider so the tool can resolve IMcpTaskStore at creation time + var toolServices = new ServiceCollection(); + toolServices.AddSingleton(taskStore); + var toolServiceProvider = toolServices.BuildServiceProvider(); + await using var fixture = new ClientServerFixture( LoggerFactory, configureServer: builder => @@ -684,7 +689,8 @@ public async Task Tool_ReturningMcpTask_BypassesSdkTaskWrapping() { Name = "self-managing-tool", Description = "A tool that creates and returns its own McpTask", - Execution = new ToolExecution { TaskSupport = ToolTaskSupport.Optional } + Execution = new ToolExecution { TaskSupport = ToolTaskSupport.Optional }, + Services = toolServiceProvider })]); }, configureServices: services => @@ -716,6 +722,11 @@ public async Task Tool_ReturningMcpTask_WithoutTaskMetadata_ReturnsTaskDirectly( // Arrange - Server with task store and a tool that returns McpTask var taskStore = new InMemoryMcpTaskStore(); + // Build a service provider so the tool can resolve IMcpTaskStore at creation time + var toolServices = new ServiceCollection(); + toolServices.AddSingleton(taskStore); + var toolServiceProvider = toolServices.BuildServiceProvider(); + await using var fixture = new ClientServerFixture( LoggerFactory, configureServer: builder => @@ -733,7 +744,8 @@ public async Task Tool_ReturningMcpTask_WithoutTaskMetadata_ReturnsTaskDirectly( { Name = "self-managing-tool", Description = "A tool that creates and returns its own McpTask", - Execution = new ToolExecution { TaskSupport = ToolTaskSupport.Optional } + Execution = new ToolExecution { TaskSupport = ToolTaskSupport.Optional }, + Services = toolServiceProvider })]); }, configureServices: services =>