diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index 5cc3327b8d..9d9ee29b22 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -283,6 +283,9 @@
+
+
+
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/HostedToolbox.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/HostedToolbox.csproj
new file mode 100644
index 0000000000..1e3c4e10c2
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/HostedToolbox.csproj
@@ -0,0 +1,30 @@
+
+
+
+ net10.0
+ enable
+ enable
+ false
+ HostedToolbox
+ HostedToolbox
+ $(NoWarn);
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/Program.cs
new file mode 100644
index 0000000000..959cc2d4f5
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/Program.cs
@@ -0,0 +1,113 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// Foundry Toolbox Agent - A hosted agent that uses Foundry Toolset MCP tools.
+//
+// Demonstrates how to register one or more Foundry toolsets so the agent can
+// call tools provided by the Foundry platform's managed MCP proxy.
+//
+// Required environment variables:
+// AZURE_AI_PROJECT_ENDPOINT - Azure AI Foundry project endpoint
+// AZURE_AI_MODEL_DEPLOYMENT_NAME - Model deployment name (default: gpt-4o)
+// FOUNDRY_AGENT_TOOLSET_ENDPOINT - Foundry Toolsets proxy base URL
+// (injected automatically by Foundry platform at runtime)
+//
+// Optional:
+// FOUNDRY_TOOLSET_NAME - Name of the toolset to load (default: my-toolset)
+// FOUNDRY_AGENT_NAME - Client name reported to MCP server
+// FOUNDRY_AGENT_VERSION - Client version reported to MCP server
+// FOUNDRY_AGENT_TOOLSET_FEATURES - Feature flags sent to Foundry proxy via header
+
+using Azure.AI.Projects;
+using Azure.Core;
+using Azure.Identity;
+using DotNetEnv;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.Foundry.Hosting;
+
+// Load .env file if present (for local development)
+Env.TraversePath().Load();
+
+string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT")
+ ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
+string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o";
+string toolsetName = Environment.GetEnvironmentVariable("FOUNDRY_TOOLSET_NAME") ?? "my-toolset";
+
+// Use a chained credential: try a temporary dev token first (for local Docker debugging),
+// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in production).
+TokenCredential credential = new ChainedTokenCredential(
+ new DevTemporaryTokenCredential(),
+ new DefaultAzureCredential());
+
+// ── Create agent ─────────────────────────────────────────────────────────────
+
+AIAgent agent = new AIProjectClient(new Uri(endpoint), credential)
+ .AsAIAgent(
+ model: deploymentName,
+ instructions: """
+ You are a helpful assistant with access to tools provided by the Foundry Toolset.
+ Use the available tools to answer user questions.
+ If a tool is not available for a request, let the user know clearly.
+ """,
+ name: Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-toolbox-agent",
+ description: "Hosted agent backed by Foundry Toolset MCP tools");
+
+// ── Build the host ────────────────────────────────────────────────────────────
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Register the agent and response handler
+builder.Services.AddFoundryResponses(agent);
+
+// Register Foundry Toolbox: connects to the MCP proxy at startup and makes tools available.
+// The toolset name must match a toolset registered in your Foundry project.
+// When FOUNDRY_AGENT_TOOLSET_ENDPOINT is absent (e.g., in local development without Foundry
+// infrastructure), startup succeeds without error and no toolbox tools are loaded.
+builder.Services.AddFoundryToolboxes(toolsetName);
+
+var app = builder.Build();
+app.MapFoundryResponses();
+
+if (app.Environment.IsDevelopment())
+{
+ app.MapFoundryResponses("openai/v1");
+}
+
+app.Run();
+
+// ── DevTemporaryTokenCredential ───────────────────────────────────────────────
+
+///
+/// A for local Docker debugging only.
+/// Reads a pre-fetched bearer token from the AZURE_BEARER_TOKEN environment variable
+/// once at startup. This should NOT be used in production.
+///
+/// Generate a token on your host and pass it to the container:
+/// export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)
+/// docker run -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN ...
+///
+internal sealed class DevTemporaryTokenCredential : TokenCredential
+{
+ private const string EnvironmentVariable = "AZURE_BEARER_TOKEN";
+ private readonly string? _token;
+
+ public DevTemporaryTokenCredential()
+ {
+ this._token = Environment.GetEnvironmentVariable(EnvironmentVariable);
+ }
+
+ public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
+ => this.GetAccessToken();
+
+ public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
+ => new(this.GetAccessToken());
+
+ private AccessToken GetAccessToken()
+ {
+ if (string.IsNullOrEmpty(this._token) || this._token == "DefaultAzureCredential")
+ {
+ throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set.");
+ }
+
+ return new AccessToken(this._token, DateTimeOffset.UtcNow.AddHours(1));
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs
index ab0d4f50aa..be7566d5bb 100644
--- a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs
@@ -21,6 +21,7 @@ public class AgentFrameworkResponseHandler : ResponseHandler
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger _logger;
+ private readonly FoundryToolboxService? _toolboxService;
///
/// Initializes a new instance of the class
@@ -28,15 +29,18 @@ public class AgentFrameworkResponseHandler : ResponseHandler
///
/// The service provider for resolving agents.
/// The logger instance.
+ /// Optional Foundry Toolbox service providing MCP tools.
public AgentFrameworkResponseHandler(
IServiceProvider serviceProvider,
- ILogger logger)
+ ILogger logger,
+ FoundryToolboxService? toolboxService = null)
{
ArgumentNullException.ThrowIfNull(serviceProvider);
ArgumentNullException.ThrowIfNull(logger);
this._serviceProvider = serviceProvider;
this._logger = logger;
+ this._toolboxService = toolboxService;
}
///
@@ -92,14 +96,33 @@ public class AgentFrameworkResponseHandler : ResponseHandler
// 5. Build chat options
var chatOptions = InputConverter.ConvertToChatOptions(request);
chatOptions.Instructions = request.Instructions;
+
+ // Inject Foundry Toolbox tools when the toolbox service is available
+ if (this._toolboxService is not null)
+ {
+ var toolboxTools = this._toolboxService.Tools;
+ if (toolboxTools.Count > 0)
+ {
+ chatOptions.Tools = [.. chatOptions.Tools ?? [], .. toolboxTools];
+ }
+ }
+
var options = new ChatClientAgentRunOptions(chatOptions);
- // 6. Run the agent and convert output
+ // 6. Set up consent context for -32006 OAuth consent interception.
+ // We create a linked CTS so the consent-aware tool wrapper can cancel the agent
+ // run mid-loop when a -32006 error is returned by the proxy. The RequestConsentState
+ // is a shared mutable object that flows via AsyncLocal to the tool wrapper.
+ using var consentCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+ var consentState = new RequestConsentState { CancellationSource = consentCts };
+ McpConsentContext.Current.Value = consentState;
+
+ // 7. Run the agent and convert output
// NOTE: C# forbids 'yield return' inside a try block that has a catch clause,
// and inside catch blocks. We use a flag to defer the yield to outside the try/catch.
bool emittedTerminal = false;
var enumerator = OutputConverter.ConvertUpdatesToEventsAsync(
- agent.RunStreamingAsync(messages, session, options: options, cancellationToken: cancellationToken),
+ agent.RunStreamingAsync(messages, session, options: options, cancellationToken: consentCts.Token),
stream,
cancellationToken).GetAsyncEnumerator(cancellationToken);
try
@@ -107,6 +130,7 @@ public class AgentFrameworkResponseHandler : ResponseHandler
while (true)
{
bool shutdownDetected = false;
+ McpConsentInfo? consentInfo = null;
ResponseStreamEvent? failedEvent = null;
ResponseStreamEvent? evt = null;
try
@@ -118,6 +142,11 @@ public class AgentFrameworkResponseHandler : ResponseHandler
evt = enumerator.Current;
}
+ catch (OperationCanceledException) when (!emittedTerminal && consentState.Pending is not null)
+ {
+ // -32006 consent error: the tool wrapper cancelled consentCts and stored consent info.
+ consentInfo = consentState.Pending;
+ }
catch (OperationCanceledException) when (context.IsShutdownRequested && !emittedTerminal)
{
shutdownDetected = true;
@@ -137,6 +166,21 @@ public class AgentFrameworkResponseHandler : ResponseHandler
ex.Message);
}
+ if (consentInfo is not null)
+ {
+ // Emit mcp_approval_request output item + incomplete for the consent URL.
+ foreach (var approvalEvent in stream.OutputItemMcpApprovalRequest(
+ consentInfo.ToolsetName,
+ consentInfo.ToolName,
+ consentInfo.ConsentUrl))
+ {
+ yield return approvalEvent;
+ }
+
+ yield return stream.EmitIncomplete(reason: null);
+ yield break;
+ }
+
if (failedEvent is not null)
{
yield return failedEvent;
diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ConsentAwareMcpClientTool.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ConsentAwareMcpClientTool.cs
new file mode 100644
index 0000000000..467660756f
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ConsentAwareMcpClientTool.cs
@@ -0,0 +1,70 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.AI;
+using ModelContextProtocol;
+using ModelContextProtocol.Client;
+
+namespace Microsoft.Agents.AI.Foundry.Hosting;
+
+///
+/// An wrapper around that intercepts
+/// JSON-RPC error -32006 (OAuth consent required) from the Foundry Toolsets proxy and
+/// propagates it back to via
+/// .
+///
+///
+///
+/// When the proxy returns -32006, the consent URL is stored in
+/// and the per-request is cancelled. This causes
+/// to stop the tool loop (it guards
+/// exceptions with when (!ct.IsCancellationRequested)) and surfaces an
+/// to the handler. The handler then emits the
+/// mcp_approval_request output item and marks the response as incomplete.
+///
+///
+internal sealed class ConsentAwareMcpClientTool : AIFunction
+{
+ private readonly McpClientTool _inner;
+ private readonly string _toolsetName;
+
+ internal ConsentAwareMcpClientTool(McpClientTool inner, string toolsetName)
+ {
+ this._inner = inner;
+ this._toolsetName = toolsetName;
+ }
+
+ public override string Name => this._inner.Name;
+
+ public override string Description => this._inner.Description;
+
+ public override JsonElement JsonSchema => this._inner.JsonSchema;
+
+ public override JsonElement? ReturnJsonSchema => this._inner.ReturnJsonSchema;
+
+ public override JsonSerializerOptions JsonSerializerOptions => this._inner.JsonSerializerOptions;
+
+ protected override async ValueTask