diff --git a/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_LongRunningTask_Client/Program.cs b/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_LongRunningTask_Client/Program.cs index 2aba8fcc16..83b4393c75 100644 --- a/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_LongRunningTask_Client/Program.cs +++ b/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_LongRunningTask_Client/Program.cs @@ -54,8 +54,9 @@ await using var mcpClient = await McpClient.CreateAsync(new StdioClientTransport })); // Wrap each MCP tool with task-aware behavior. The wrapper inspects the server's -// execution.taskSupport on each tool and, if Optional/Required, drives the task lifecycle -// transparently within the agent's tool loop. +// execution.taskSupport on each tool and, when it is Required, drives the task lifecycle +// transparently within the agent's tool loop. Tools that don't require task semantics are +// returned as-is and invoked inline. var taskOptions = new McpTaskOptions { DefaultTimeToLive = TimeSpan.FromMinutes(5), @@ -131,7 +132,7 @@ static async Task RunMcpServerAsync() internal sealed class DatasetAnalysisTools #pragma warning restore CA1812 { - [McpServerTool(Name = "AnalyzeDataset", TaskSupport = ToolTaskSupport.Optional)] + [McpServerTool(Name = "AnalyzeDataset", TaskSupport = ToolTaskSupport.Required)] [Description("Analyze a tabular dataset and return summary statistics. This tool simulates a long-running analytic job (~15 seconds).")] public static async Task AnalyzeDatasetAsync( [Description("The dataset identifier, e.g. 'sales-2025-q1'.")] string datasetName, diff --git a/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_LongRunningTask_Client/README.md b/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_LongRunningTask_Client/README.md index 00dae7d4ae..76d884952c 100644 --- a/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_LongRunningTask_Client/README.md +++ b/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_LongRunningTask_Client/README.md @@ -6,7 +6,7 @@ This sample demonstrates Microsoft Agent Framework's MCP long-running task suppo - Using `McpClient.ListAgentToolsWithTaskSupportAsync(...)` (in `Microsoft.Agents.AI.Mcp`) to wrap MCP tools with task-aware behavior. - Configuring `McpTaskOptions.DefaultTimeToLive` to bound the server-side task. -- Hosting a small MCP server (in this same executable, launched with `--server`) that advertises `execution.taskSupport=optional` on a tool that sleeps for ~15 seconds. +- Hosting a small MCP server (in this same executable, launched with `--server`) that advertises `execution.taskSupport=required` on a tool that sleeps for ~15 seconds. - No application-level polling, continuation tokens, or `AllowBackgroundResponses` flag are required. The decorator drives the lifecycle internally: diff --git a/dotnet/src/Microsoft.Agents.AI.Mcp/McpClientTaskExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Mcp/McpClientTaskExtensions.cs index f091fe7d79..77bbf6053c 100644 --- a/dotnet/src/Microsoft.Agents.AI.Mcp/McpClientTaskExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Mcp/McpClientTaskExtensions.cs @@ -18,9 +18,11 @@ public static class McpClientTaskExtensions { /// /// Lists tools advertised by the connected MCP server and returns each as an - /// . Tools that declare or - /// are wrapped with task-aware behavior so an agent - /// can transparently drive long-running invocations. All other tools are returned as-is. + /// . Tools that declare + /// are wrapped with task-aware behavior so an agent can transparently drive long-running + /// invocations. All other tools — including those that declare + /// — are returned as-is, preserving inline + /// (synchronous) invocation semantics by default. /// /// The connected MCP client. /// @@ -44,7 +46,7 @@ public static class McpClientTaskExtensions for (int i = 0; i < tools.Count; i++) { ToolTaskSupport? taskSupport = tools[i].ProtocolTool.Execution?.TaskSupport; - if (taskSupport is ToolTaskSupport.Optional or ToolTaskSupport.Required) + if (taskSupport is ToolTaskSupport.Required) { result[i] = new TaskAwareMcpClientAIFunction(client, tools[i], effectiveOptions); } diff --git a/dotnet/src/Microsoft.Agents.AI.Mcp/TaskAwareMcpClientAIFunction.cs b/dotnet/src/Microsoft.Agents.AI.Mcp/TaskAwareMcpClientAIFunction.cs index 26091b2b60..45ffdb4ce0 100644 --- a/dotnet/src/Microsoft.Agents.AI.Mcp/TaskAwareMcpClientAIFunction.cs +++ b/dotnet/src/Microsoft.Agents.AI.Mcp/TaskAwareMcpClientAIFunction.cs @@ -31,13 +31,12 @@ namespace Microsoft.Agents.AI.Mcp; /// /// /// This wrapper is intended to be applied only to tools whose -/// is or -/// (selected by -/// ). As a defensive -/// fallback, if the server still rejects the task-augmented call with JSON-RPC error -/// -32601 (e.g. because tool-level capabilities changed between tools/list and -/// invocation), the wrapper transparently falls back to a non-augmented call through the -/// inner . +/// is +/// (selected by ). +/// As a defensive fallback, if the server still rejects the task-augmented call with +/// (e.g. because tool-level capabilities changed +/// between tools/list and invocation), the wrapper transparently falls back to a +/// non-augmented call through the inner . /// /// internal sealed class TaskAwareMcpClientAIFunction : AIFunction @@ -96,7 +95,7 @@ internal sealed class TaskAwareMcpClientAIFunction : AIFunction options: null, cancellationToken: cancellationToken).ConfigureAwait(false); } - catch (McpProtocolException ex) when ((int)ex.ErrorCode == -32601) + catch (McpProtocolException ex) when (ex.ErrorCode == McpErrorCode.MethodNotFound) { // Defensive fallback: the server's advertised TaskSupport indicated this tool // could be invoked as a task, but the server now rejects task augmentation for it diff --git a/dotnet/tests/Microsoft.Agents.AI.Mcp.UnitTests/ListAgentToolsWithTaskSupportTests.cs b/dotnet/tests/Microsoft.Agents.AI.Mcp.UnitTests/ListAgentToolsWithTaskSupportTests.cs index 9fb9aa24d9..56544a44fb 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Mcp.UnitTests/ListAgentToolsWithTaskSupportTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Mcp.UnitTests/ListAgentToolsWithTaskSupportTests.cs @@ -34,8 +34,8 @@ public class ListAgentToolsWithTaskSupportTests AIFunction forb = result.Single(f => f.Name == "forb"); AIFunction none = result.Single(f => f.Name == "none"); - opt.Should().BeOfType("Optional tools must be wrapped"); req.Should().BeOfType("Required tools must be wrapped"); + opt.Should().NotBeOfType("Optional tools must not be wrapped; inline invocation is preserved by default"); forb.Should().NotBeOfType("Forbidden tools must not be wrapped"); none.Should().NotBeOfType("Tools without execution metadata must not be wrapped"); } diff --git a/dotnet/tests/Microsoft.Agents.AI.Mcp.UnitTests/TaskAwareMcpClientAIFunctionTests.cs b/dotnet/tests/Microsoft.Agents.AI.Mcp.UnitTests/TaskAwareMcpClientAIFunctionTests.cs index 7d923cc17a..309fece743 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Mcp.UnitTests/TaskAwareMcpClientAIFunctionTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Mcp.UnitTests/TaskAwareMcpClientAIFunctionTests.cs @@ -14,27 +14,6 @@ namespace Microsoft.Agents.AI.Mcp.UnitTests; public class TaskAwareMcpClientAIFunctionTests { - [Fact] - public async Task InvokeAsync_OptionalTool_HappyPath_ReturnsResultAsync() - { - // Arrange - McpServerPrimitiveCollection tools = [ - TestTools.Create("opt", ToolTaskSupport.Optional, () => "optional-result"), - ]; - await using InMemoryMcpServerFixture fixture = await InMemoryMcpServerFixture.CreateAsync(tools); - var result = await fixture.Client.ListAgentToolsWithTaskSupportAsync(); - AIFunction opt = result.Single(f => f.Name == "opt"); - opt.Should().BeOfType(); - - // Act - object? invokeResult = await opt.InvokeAsync(arguments: null, CancellationToken.None); - - // Assert — wrapper returns a JsonElement containing the serialized CallToolResult - // (same wire shape as McpClientTool.InvokeAsync). - JsonElement payload = invokeResult.Should().BeOfType().Subject; - ExtractTextContent(payload).Should().Be("optional-result"); - } - [Fact] public async Task InvokeAsync_RequiredTool_HappyPath_ReturnsResultAsync() {