Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions dotnet/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,15 @@
<!-- System.* -->
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.11" />
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
<PackageVersion Include="Microsoft.Bcl.Memory" Version="10.0.5" />
<PackageVersion Include="Microsoft.Bcl.Memory" Version="10.0.10" />
<PackageVersion Include="System.ClientModel" Version="1.14.0" />
<PackageVersion Include="System.CodeDom" Version="10.0.0" />
<PackageVersion Include="System.Collections.Immutable" Version="10.0.1" />
<PackageVersion Include="System.Collections.Immutable" Version="10.0.10" />
<PackageVersion Include="System.CommandLine" Version="2.0.0-rc.2.25502.107" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="10.0.11" />
<PackageVersion Include="System.Linq.AsyncEnumerable" Version="10.0.5" />
<PackageVersion Include="System.Net.Http.Json" Version="10.0.0" />
<PackageVersion Include="System.Net.ServerSentEvents" Version="10.0.8" />
<PackageVersion Include="System.Net.ServerSentEvents" Version="10.0.10" />
<!-- AG-UI .NET SDK packages (published by the AG-UI team). -->
<PackageVersion Include="AGUI.Abstractions" Version="0.0.5" />
<PackageVersion Include="AGUI.Formatting" Version="0.0.5" />
Expand Down Expand Up @@ -119,7 +119,8 @@
<PackageVersion Include="A2A" Version="1.0.0-preview2" />
<PackageVersion Include="A2A.AspNetCore" Version="1.0.0-preview2" />
<!-- MCP -->
<PackageVersion Include="ModelContextProtocol" Version="1.2.0" />
<PackageVersion Include="ModelContextProtocol" Version="2.1.0" />
<PackageVersion Include="ModelContextProtocol.Extensions.Tasks" Version="2.1.0" />
<!-- Hyperlight -->
<PackageVersion Include="Hyperlight.HyperlightSandbox.Api" Version="0.4.0" />
<PackageVersion Include="Hyperlight.HyperlightSandbox.Guest.Python" Version="0.4.0" />
Expand Down
1 change: 1 addition & 0 deletions dotnet/eng/verify-samples/AgentsSamples.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1303,6 +1303,7 @@ internal static class AgentsSamples
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
MustContain =
[
"MCP 2026-07-28 Tasks extension enabled.",
"=== Transparent long-running MCP task (RunAsync) ===",
"=== Transparent long-running MCP task (RunStreamingAsync) ===",
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="ModelContextProtocol" />
<PackageReference Include="ModelContextProtocol.Extensions.Tasks" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// 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
// McpClientTaskExtensions.ListAgentToolsWithTasksAsync, 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
Expand All @@ -14,9 +14,9 @@
// 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
// 1. Calls tools/call with the io.modelcontextprotocol/tasks extension capability
// 2. Accepts either an inline result or a task handle
// 3. Polls tasks/get until the final result is available
//
// No application-level loop or continuation tokens are required in either mode.

Expand All @@ -29,8 +29,8 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ModelContextProtocol;
using ModelContextProtocol.Client;
using ModelContextProtocol.Extensions.Tasks;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
using OpenAI.Chat;
Expand All @@ -53,15 +53,9 @@
Arguments = [thisAssemblyPath, "--server"],
}));

// Wrap each MCP tool with task-aware behavior. The wrapper inspects the server's
// 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),
};
var mcpTools = await mcpClient.ListAgentToolsWithTaskSupportAsync(taskOptions);
// Wrap each MCP tool with task-aware behavior. Each invocation opts into the Tasks extension;
// a task-capable server may return a task handle, while other servers can return inline.
var mcpTools = await mcpClient.ListAgentToolsWithTasksAsync();

// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
Expand All @@ -76,6 +70,8 @@

const string Prompt = "Analyze the dataset named 'sales-2025-q1' and summarize the findings.";

Console.WriteLine("MCP 2026-07-28 Tasks extension enabled.");
Console.WriteLine();
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.");
Expand Down Expand Up @@ -117,12 +113,10 @@ static async Task RunMcpServerAsync()
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" };
})
o.ServerInfo = new Implementation { Name = "DatasetAnalyzer", Version = "1.0.0" })
.WithStdioServerTransport()
.WithTools<DatasetAnalysisTools>();
.WithTools<DatasetAnalysisTools>()
.WithTasks(new InMemoryMcpTaskStore());

await builder.Build().RunAsync();
}
Expand All @@ -132,7 +126,7 @@ static async Task RunMcpServerAsync()
internal sealed class DatasetAnalysisTools
#pragma warning restore CA1812
{
[McpServerTool(Name = "AnalyzeDataset", TaskSupport = ToolTaskSupport.Required)]
[McpServerTool(Name = "AnalyzeDataset")]
[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,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
# Agent with MCP long-running task (transparent polling)
# Agent with MCP Tasks extension (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.
This sample demonstrates Microsoft Agent Framework's support for the MCP 2026-07-28 Tasks extension: 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=required` on a tool that sleeps for ~15 seconds.
- Using `McpClient.ListAgentToolsWithTasksAsync(...)` (in `Microsoft.Agents.AI.Mcp`) to wrap MCP tools with task-aware behavior.
- Hosting a small MCP server (in this same executable, launched with `--server`) that enables `io.modelcontextprotocol/tasks` with `WithTasks(...)` and exposes a tool that sleeps for ~15 seconds.
- Allowing the server to return either an inline result or a task handle after the client opts into the extension.
- 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
1. `tools/call` includes the Tasks extension capability.
2. The server returns either the ordinary tool result or a task handle.
3. `tasks/get` is polled until it carries the final result, which is returned to the function-calling loop.

The transparent adapter retains the created task handle while it polls. By default, cancelling the local invocation also sends a best-effort `tasks/cancel` so abandoned server work can stop cooperatively. Set `McpTaskOptions.CancelRemoteTaskOnLocalCancellation` to `false` when server work should continue independently after the caller stops waiting.

The adapter also rejects unusable server polling intervals and bounds unique mid-flight input requests. If either safety limit is exceeded while the task may still be active, the adapter fails the invocation and sends a best-effort `tasks/cancel`.

The sample exercises both invocation styles against the same wrapper:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Authentication;
using ModelContextProtocol.Client;
using OpenAI.Chat;

Expand Down Expand Up @@ -39,7 +40,7 @@
ClientName = "ProtectedMcpClient",
},
RedirectUri = new Uri("http://localhost:1179/callback"),
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
AuthorizationCallbackHandler = HandleAuthorizationCallbackAsync,
}
}, httpClient, consoleLoggerFactory);

Expand All @@ -63,12 +64,14 @@

// Handles the OAuth authorization URL by starting a local HTTP server and opening a browser.
// This implementation demonstrates how SDK consumers can provide their own authorization flow.
static async Task<string?> HandleAuthorizationUrlAsync(Uri authorizationUrl, Uri redirectUri, CancellationToken cancellationToken)
static async Task<AuthorizationResult?> HandleAuthorizationCallbackAsync(
AuthorizationCallbackContext callbackContext,
CancellationToken cancellationToken)
{
Console.WriteLine("Starting OAuth authorization flow...");
Console.WriteLine($"Opening browser to: {authorizationUrl}");
Console.WriteLine($"Opening browser to: {callbackContext.AuthorizationUri}");

var listenerPrefix = redirectUri.GetLeftPart(UriPartial.Authority);
var listenerPrefix = callbackContext.RedirectUri.GetLeftPart(UriPartial.Authority);
if (!listenerPrefix.EndsWith("/", StringComparison.InvariantCultureIgnoreCase))
{
listenerPrefix += "/";
Expand All @@ -82,11 +85,13 @@
listener.Start();
Console.WriteLine($"Listening for OAuth callback on: {listenerPrefix}");

OpenBrowser(authorizationUrl);
OpenBrowser(callbackContext.AuthorizationUri);

var context = await listener.GetContextAsync();
var query = HttpUtility.ParseQueryString(context.Request.Url?.Query ?? string.Empty);
var code = query["code"];
var state = query["state"];
var issuer = query["iss"];
var error = query["error"];

const string ResponseHtml = "<html><body><h1>Authentication complete</h1><p>You can close this window now.</p></body></html>";
Expand All @@ -102,14 +107,19 @@
return null;
}

if (string.IsNullOrEmpty(code))
if (string.IsNullOrEmpty(code) || string.IsNullOrEmpty(state))
{
Console.WriteLine("No authorization code received");
Console.WriteLine("The authorization response did not contain both code and state.");
return null;
}

Console.WriteLine("Authorization code received successfully.");
return code;
return new AuthorizationResult
{
Code = code,
State = state,
Iss = issuer,
};
}
catch (Exception ex)
{
Expand Down
2 changes: 1 addition & 1 deletion dotnet/samples/02-agents/ModelContextProtocol/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Before you begin, ensure you have the following prerequisites:
|[Agent with MCP server tools and authorization](./Agent_MCP_Server_Auth/)|This sample demonstrates how to use MCP Server tools from a protected MCP server with a simple agent|
|[Agent with per-run MCP authentication headers](./Agent_MCP_PerRun_AuthHeaders/)|This sample demonstrates how to attach per-run, refreshable authentication headers to MCP requests using a custom HttpClient handler and an AsyncLocal scope. Uses Microsoft Foundry (`FOUNDRY_PROJECT_ENDPOINT` / `FOUNDRY_MODEL`) rather than the Azure OpenAI variables in the prerequisites above.|
|[Responses Agent with Hosted MCP tool](./ResponseAgent_Hosted_MCP/)|This sample demonstrates how to use the Hosted MCP tool with the Responses Service, where the service invokes any MCP tools directly|
|[Agent with long-running MCP task (transparent polling)](./Agent_MCP_LongRunningTask_Client/)|This sample demonstrates how an agent transparently drives a long-running MCP task (SEP-2663) to completion. The wrapper polls the task internally on both `RunAsync` and `RunStreamingAsync` invocations.|
|[Agent with MCP Tasks extension (transparent polling)](./Agent_MCP_LongRunningTask_Client/)|This sample demonstrates how an agent transparently drives an MCP 2026-07-28 Tasks extension invocation to completion. The wrapper handles inline fallback and polls task-backed calls internally for both `RunAsync` and `RunStreamingAsync`.|

## Running the samples from the console

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="$(AgentFrameworkVersion)" />
<PackageReference Include="Azure.AI.Projects" Version="2.1.0-beta.4" />
<PackageReference Include="Azure.Identity" Version="1.21.0" />
<PackageReference Include="ModelContextProtocol" Version="1.2.0" />
<PackageReference Include="ModelContextProtocol" Version="2.1.0" />
<PackageReference Include="DotNetEnv" Version="3.1.1" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
<PackageReference Include="Microsoft.Agents.AI.Mcp" Version="1.15.0-alpha.260722.1" />
<PackageReference Include="Azure.AI.Projects" Version="2.1.0-beta.4" />
<PackageReference Include="Azure.Identity" Version="1.21.0" />
<PackageReference Include="ModelContextProtocol" Version="1.2.0" />
<PackageReference Include="ModelContextProtocol" Version="2.1.0" />
<PackageReference Include="DotNetEnv" Version="3.1.1" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<PackageReference Include="Microsoft.Agents.AI.Hosting" Version="$(AgentFrameworkVersion)" />
<PackageReference Include="Azure.AI.OpenAI" Version="2.9.0-beta.1" />
<PackageReference Include="Azure.Identity" Version="1.21.0" />
<PackageReference Include="ModelContextProtocol" Version="1.2.0" />
<PackageReference Include="ModelContextProtocol" Version="2.1.0" />
<PackageReference Include="DotNetEnv" Version="3.1.1" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -600,9 +600,10 @@ private async Task<ToolboxOpenResult> OpenToolboxAsync(
}
};

// McpClient.CreateAsync runs the MCP initialize handshake and can throw for an unreachable
// proxy (the deferred-toolbox case, retried per request). Keep it inside the try so the
// HttpClient is always disposed on failure rather than leaking a socket on every retry.
// McpClient.CreateAsync performs discovery-first negotiation with down-level fallback and
// can throw for an unreachable proxy (the deferred-toolbox case, retried per request).
// Keep it inside the try so the HttpClient is always disposed on failure rather than
// leaking a socket on every retry.
McpClient? client = null;
IList<McpClientTool> mcpTools;
try
Expand Down
Loading
Loading