-
Notifications
You must be signed in to change notification settings - Fork 1.9k
.NET: Add MCP long-running task support for MCP client tools #5994
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 2 commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
0a7a372
Add MCP long-running task support for MCP client tools
peibekwe d53645b
Fixed project file formatting issue.
peibekwe 53edd12
Removed experimentation tag from MCP alpha project.
peibekwe 42b2954
Merge branch 'main' into peibekwe/mcp-lro
peibekwe fed973b
Addressed PR comments
peibekwe File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
25 changes: 25 additions & 0 deletions
25
...lContextProtocol/Agent_MCP_LongRunningTask_Client/Agent_MCP_LongRunningTask_Client.csproj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <OutputType>Exe</OutputType> | ||
| <TargetFrameworks>net10.0</TargetFrameworks> | ||
|
|
||
| <Nullable>enable</Nullable> | ||
| <ImplicitUsings>enable</ImplicitUsings> | ||
| <NoWarn>$(NoWarn);MAAI001;MEAI001;MCPEXP001</NoWarn> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="Azure.AI.OpenAI" /> | ||
| <PackageReference Include="Azure.Identity" /> | ||
| <PackageReference Include="Microsoft.Extensions.AI.OpenAI" /> | ||
| <PackageReference Include="Microsoft.Extensions.Hosting" /> | ||
| <PackageReference Include="ModelContextProtocol" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Mcp\Microsoft.Agents.AI.Mcp.csproj" /> | ||
| <ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> |
144 changes: 144 additions & 0 deletions
144
dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_LongRunningTask_Client/Program.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| // Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| // This sample demonstrates the Microsoft Agent Framework's MCP long-running task support. | ||
| // | ||
| // A small MCP server (hosted in this same executable when launched with "--server") exposes | ||
| // a single task-supporting tool "AnalyzeDataset" that simulates ~15 seconds of work. The | ||
| // client (default mode) connects to it over stdio via Microsoft.Agents.AI.Mcp's | ||
| // McpClientTaskExtensions.ListAgentToolsWithTaskSupportAsync, hands the wrapped tools to a | ||
| // ChatClientAgent, and exercises both invocation styles: | ||
| // * RunAsync — blocks until the agent's final response is ready. | ||
| // * RunStreamingAsync — yields response updates as the model produces them; the model | ||
| // still waits for the tool's terminal result before it can begin | ||
| // producing the final answer, so the perceived "pause" reflects | ||
| // tool execution time, not stream-channel latency. | ||
| // | ||
| // In both cases the wrapper transparently: | ||
| // 1. Calls tools/call with task augmentation (CallToolAsTaskAsync) | ||
| // 2. Polls tasks/get until terminal (PollTaskUntilCompleteAsync) | ||
| // 3. Fetches tasks/result and returns the final result to the function-calling loop | ||
| // | ||
| // No application-level loop or continuation tokens are required in either mode. | ||
|
|
||
| using System.ComponentModel; | ||
| using Azure.AI.OpenAI; | ||
| using Azure.Identity; | ||
| using Microsoft.Agents.AI; | ||
| using Microsoft.Agents.AI.Mcp; | ||
| using Microsoft.Extensions.AI; | ||
| using Microsoft.Extensions.DependencyInjection; | ||
| using Microsoft.Extensions.Hosting; | ||
| using Microsoft.Extensions.Logging; | ||
| using ModelContextProtocol; | ||
| using ModelContextProtocol.Client; | ||
| using ModelContextProtocol.Protocol; | ||
| using ModelContextProtocol.Server; | ||
| using OpenAI.Chat; | ||
|
|
||
| if (args.Length > 0 && args[0] == "--server") | ||
| { | ||
| await RunMcpServerAsync(); | ||
| return; | ||
| } | ||
|
|
||
| var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); | ||
| var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini"; | ||
|
|
||
| // Launch this same assembly as a stdio MCP server in a child process. | ||
| var thisAssemblyPath = typeof(Program).Assembly.Location; | ||
| await using var mcpClient = await McpClient.CreateAsync(new StdioClientTransport(new() | ||
| { | ||
| Name = "DatasetAnalyzer", | ||
| Command = "dotnet", | ||
| Arguments = [thisAssemblyPath, "--server"], | ||
| })); | ||
|
|
||
| // 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. | ||
| var taskOptions = new McpTaskOptions | ||
| { | ||
| DefaultTimeToLive = TimeSpan.FromMinutes(5), | ||
| }; | ||
| var mcpTools = await mcpClient.ListAgentToolsWithTaskSupportAsync(taskOptions); | ||
|
|
||
| // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. | ||
| // In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid | ||
| // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. | ||
| AIAgent agent = new AzureOpenAIClient( | ||
| new Uri(endpoint), | ||
| new DefaultAzureCredential()) | ||
| .GetChatClient(deploymentName) | ||
| .AsAIAgent( | ||
| instructions: "You answer data-analysis questions by invoking the available tools. Always invoke a tool when one matches the request.", | ||
| tools: [.. mcpTools.Cast<AITool>()]); | ||
|
|
||
| const string Prompt = "Analyze the dataset named 'sales-2025-q1' and summarize the findings."; | ||
|
|
||
| Console.WriteLine("=== Transparent long-running MCP task (RunAsync) ==="); | ||
| Console.WriteLine("Asking the agent to analyze a dataset; the tool takes ~15s to complete."); | ||
| Console.WriteLine("RunAsync blocks while the wrapper polls the task to completion."); | ||
| Console.WriteLine(); | ||
|
|
||
| var stopwatch = System.Diagnostics.Stopwatch.StartNew(); | ||
| var response = await agent.RunAsync(Prompt); | ||
| stopwatch.Stop(); | ||
|
|
||
| Console.WriteLine($"Agent response (after {stopwatch.Elapsed.TotalSeconds:F1}s):"); | ||
| Console.WriteLine(response.Text); | ||
|
|
||
| Console.WriteLine(); | ||
| Console.WriteLine("=== Transparent long-running MCP task (RunStreamingAsync) ==="); | ||
| Console.WriteLine("Same request via the streaming API. Updates only begin to arrive after the"); | ||
| Console.WriteLine("tool's task reaches the Completed state, since the model needs the tool result"); | ||
| Console.WriteLine("before it can produce its final answer."); | ||
| Console.WriteLine(); | ||
|
|
||
| stopwatch.Restart(); | ||
| await foreach (var update in agent.RunStreamingAsync(Prompt)) | ||
| { | ||
| Console.Write(update.Text); | ||
| } | ||
| stopwatch.Stop(); | ||
|
|
||
| Console.WriteLine(); | ||
| Console.WriteLine($"(Streaming completed after {stopwatch.Elapsed.TotalSeconds:F1}s.)"); | ||
|
|
||
| // --- Server mode (launched as a child process via --server) --------------------------------- | ||
| static async Task RunMcpServerAsync() | ||
| { | ||
| var builder = Host.CreateApplicationBuilder(); | ||
|
|
||
| // Critical for stdio transport: any provider that writes to stdout will corrupt the | ||
| // JSON-RPC channel. Clear all providers; the MCP SDK routes its own diagnostics | ||
| // appropriately. | ||
| builder.Logging.ClearProviders(); | ||
| builder.Logging.AddConsole(o => o.LogToStandardErrorThreshold = LogLevel.Trace); | ||
|
|
||
| builder.Services.AddMcpServer(o => | ||
| { | ||
| o.TaskStore = new InMemoryMcpTaskStore(); | ||
| o.ServerInfo = new Implementation { Name = "DatasetAnalyzer", Version = "1.0.0" }; | ||
| }) | ||
| .WithStdioServerTransport() | ||
| .WithTools<DatasetAnalysisTools>(); | ||
|
|
||
| await builder.Build().RunAsync(); | ||
| } | ||
|
|
||
| #pragma warning disable CA1812 // Discovered by MCP SDK via [McpServerToolType] attribute | ||
| [McpServerToolType] | ||
| internal sealed class DatasetAnalysisTools | ||
| #pragma warning restore CA1812 | ||
| { | ||
| [McpServerTool(Name = "AnalyzeDataset", TaskSupport = ToolTaskSupport.Optional)] | ||
| [Description("Analyze a tabular dataset and return summary statistics. This tool simulates a long-running analytic job (~15 seconds).")] | ||
| public static async Task<string> AnalyzeDatasetAsync( | ||
| [Description("The dataset identifier, e.g. 'sales-2025-q1'.")] string datasetName, | ||
| CancellationToken cancellationToken) | ||
| { | ||
| await Task.Delay(TimeSpan.FromSeconds(15), cancellationToken).ConfigureAwait(false); | ||
|
|
||
| return $"Findings for '{datasetName}': 12,403 rows; avg revenue $48,712; 3 anomalies detected in week 7; outliers concentrated in EMEA region."; | ||
| } | ||
| } |
60 changes: 60 additions & 0 deletions
60
...mples/02-agents/ModelContextProtocol/Agent_MCP_LongRunningTask_Client/README.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| # Agent with MCP long-running task (transparent polling) | ||
|
|
||
| This sample demonstrates Microsoft Agent Framework's MCP long-running task support: an agent invokes an MCP tool whose execution takes too long for a single request/response cycle, and the framework polls it to completion behind the function-calling loop. From the agent's perspective the tool simply returns its result. | ||
|
|
||
| ## What this sample shows | ||
|
|
||
| - 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. | ||
| - No application-level polling, continuation tokens, or `AllowBackgroundResponses` flag are required. | ||
|
|
||
| The decorator drives the lifecycle internally: | ||
|
|
||
| 1. `tools/call` augmented with task metadata (`CallToolAsTaskAsync`) | ||
| 2. `tasks/get` polled until terminal (`PollTaskUntilCompleteAsync`) | ||
| 3. `tasks/result` retrieved (`GetTaskResultAsync`) and returned to the function-calling loop | ||
|
|
||
| The sample exercises both invocation styles against the same wrapper: | ||
|
|
||
| - `agent.RunAsync(...)` blocks until the tool completes (~15 seconds in this sample) and returns the final response. | ||
| - `agent.RunStreamingAsync(...)` returns immediately and yields `AgentResponseUpdate` chunks as the model emits them; in this scenario the model only begins streaming its answer once the wrapped tool's task reaches the `Completed` state, so the perceived "pause" before tokens arrive reflects tool execution time, not stream-channel latency. | ||
|
|
||
| # Prerequisites | ||
|
|
||
| - .NET 10 SDK or later | ||
| - Azure OpenAI service endpoint and a chat-completions deployment | ||
| - Azure CLI installed and authenticated (`az login`) | ||
|
|
||
| Set the following environment variables: | ||
|
|
||
| ```powershell | ||
| $env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/" | ||
| $env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-5.4-mini" # optional; defaults to gpt-5.4-mini | ||
| ``` | ||
|
|
||
| # Running | ||
|
|
||
| ```powershell | ||
| cd Agent_MCP_LongRunningTask_Client | ||
| dotnet run | ||
| ``` | ||
|
|
||
| You should see output similar to: | ||
|
|
||
| ``` | ||
| === Transparent long-running MCP task (RunAsync) === | ||
| Asking the agent to analyze a dataset; the tool takes ~15s to complete. | ||
| RunAsync blocks while the wrapper polls the task to completion. | ||
|
|
||
| Agent response (after 15.4s): | ||
| The 'sales-2025-q1' dataset contains 12,403 rows ... | ||
|
|
||
| === Transparent long-running MCP task (RunStreamingAsync) === | ||
| Same request via the streaming API. Updates only begin to arrive after the | ||
| tool's task reaches the Completed state, since the model needs the tool result | ||
| before it can produce its final answer. | ||
|
|
||
| The 'sales-2025-q1' dataset contains 12,403 rows ... | ||
| (Streaming completed after 15.7s.) | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
62 changes: 62 additions & 0 deletions
62
dotnet/src/Microsoft.Agents.AI.Mcp/McpClientTaskExtensions.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| // Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| using System.Collections.Generic; | ||
| using System.Diagnostics.CodeAnalysis; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
| using Microsoft.Extensions.AI; | ||
| using Microsoft.Shared.DiagnosticIds; | ||
| using Microsoft.Shared.Diagnostics; | ||
| using ModelContextProtocol.Client; | ||
| using ModelContextProtocol.Protocol; | ||
|
|
||
| namespace Microsoft.Agents.AI.Mcp; | ||
|
|
||
| /// <summary> | ||
| /// Extension methods on <see cref="McpClient"/> that expose MCP server tools to a Microsoft | ||
| /// Agent Framework agent with optional long-running task (SEP-2663) handling. | ||
| /// </summary> | ||
| [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] | ||
| public static class McpClientTaskExtensions | ||
| { | ||
| /// <summary> | ||
| /// Lists tools advertised by the connected MCP server and returns each as an | ||
| /// <see cref="AIFunction"/>. Tools that declare <see cref="ToolTaskSupport.Optional"/> or | ||
| /// <see cref="ToolTaskSupport.Required"/> are wrapped with task-aware behavior so an agent | ||
| /// can transparently drive long-running invocations. All other tools are returned as-is. | ||
| /// </summary> | ||
| /// <param name="client">The connected MCP client.</param> | ||
| /// <param name="options"> | ||
| /// Options that control the task lifecycle for task-capable tools. | ||
| /// When <see langword="null"/>, defaults described on <see cref="McpTaskOptions"/> apply. | ||
| /// </param> | ||
| /// <param name="cancellationToken">Token used to cancel listing the server's tools.</param> | ||
| /// <returns>The tools, ready to pass to <c>AsAIAgent(tools: …)</c>.</returns> | ||
| public static async Task<IReadOnlyList<AIFunction>> ListAgentToolsWithTaskSupportAsync( | ||
| this McpClient client, | ||
| McpTaskOptions? options = null, | ||
| CancellationToken cancellationToken = default) | ||
| { | ||
| _ = Throw.IfNull(client); | ||
|
|
||
| McpTaskOptions effectiveOptions = options ?? new McpTaskOptions(); | ||
|
|
||
| IList<McpClientTool> tools = await client.ListToolsAsync(cancellationToken: cancellationToken).ConfigureAwait(false); | ||
|
|
||
| AIFunction[] result = new AIFunction[tools.Count]; | ||
| for (int i = 0; i < tools.Count; i++) | ||
| { | ||
| ToolTaskSupport? taskSupport = tools[i].ProtocolTool.Execution?.TaskSupport; | ||
| if (taskSupport is ToolTaskSupport.Optional or ToolTaskSupport.Required) | ||
| { | ||
| result[i] = new TaskAwareMcpClientAIFunction(client, tools[i], effectiveOptions); | ||
| } | ||
| else | ||
| { | ||
| result[i] = tools[i]; | ||
| } | ||
| } | ||
|
|
||
| return result; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| // Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| using System; | ||
| using System.Diagnostics.CodeAnalysis; | ||
| using Microsoft.Shared.DiagnosticIds; | ||
|
|
||
| namespace Microsoft.Agents.AI.Mcp; | ||
|
|
||
| /// <summary> | ||
| /// Configures how an MCP client wrapper drives the | ||
| /// <see href="https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks">MCP tasks</see> | ||
| /// lifecycle when an underlying server tool returns a <c>CreateTaskResult</c>. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// <para> | ||
| /// All members of this type are subject to change. The MCP task surface is experimental | ||
| /// and tracks the in-flight specification. | ||
| /// </para> | ||
| /// </remarks> | ||
| [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] | ||
| public sealed class McpTaskOptions | ||
| { | ||
| /// <summary> | ||
| /// Gets or sets the time-to-live the wrapper attaches to a newly created server-side task. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// When <see langword="null"/> the wrapper omits the <c>ttl</c> hint and lets the server | ||
| /// pick its own value. The server's chosen TTL is always authoritative. | ||
| /// </remarks> | ||
| public TimeSpan? DefaultTimeToLive { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets a value indicating whether the wrapper should send | ||
| /// <c>tasks/cancel</c> when the local <see cref="System.Threading.CancellationToken"/> | ||
| /// fires during a tool invocation. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Defaults to <see langword="true"/>: a local cancellation means "the caller is giving up | ||
| /// on this tool invocation" and the server-side task has no further consumer. | ||
| /// </remarks> | ||
| public bool CancelRemoteTaskOnLocalCancellation { get; set; } = true; | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.