Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 4 additions & 3 deletions dotnet/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@
<!-- 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.15.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" />
Expand Down Expand Up @@ -120,7 +120,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`. `McpTaskOptions` can adjust the remote-cancellation timeout and accepted polling-interval range when deployment requirements differ from the defaults.

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