Rename toolset to toolbox in user-facing API; rename ConsentAwareMcpClientTool to ConsentAwareMcpClientAIFunction

This commit is contained in:
Roger Barreto
2026-04-16 23:19:02 +01:00
Unverified
parent 9baf76ea77
commit 1f4bcd6c88
7 changed files with 43 additions and 43 deletions
@@ -12,7 +12,7 @@
// (injected automatically by Foundry platform at runtime)
//
// Optional:
// FOUNDRY_TOOLSET_NAME - Name of the toolset to load (default: my-toolset)
// FOUNDRY_TOOLBOX_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
@@ -30,7 +30,7 @@ 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";
string toolboxName = Environment.GetEnvironmentVariable("FOUNDRY_TOOLBOX_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).
@@ -62,7 +62,7 @@ builder.Services.AddFoundryResponses(agent);
// 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);
builder.Services.AddFoundryToolboxes(toolboxName);
var app = builder.Build();
app.MapFoundryResponses();
@@ -170,7 +170,7 @@ public class AgentFrameworkResponseHandler : ResponseHandler
{
// Emit mcp_approval_request output item + incomplete for the consent URL.
foreach (var approvalEvent in stream.OutputItemMcpApprovalRequest(
consentInfo.ToolsetName,
consentInfo.ToolboxName,
consentInfo.ToolName,
consentInfo.ConsentUrl))
{
@@ -25,15 +25,15 @@ namespace Microsoft.Agents.AI.Foundry.Hosting;
/// <c>mcp_approval_request</c> output item and marks the response as <c>incomplete</c>.
/// </para>
/// </remarks>
internal sealed class ConsentAwareMcpClientTool : AIFunction
internal sealed class ConsentAwareMcpClientAIFunction : AIFunction
{
private readonly McpClientTool _inner;
private readonly string _toolsetName;
private readonly string _toolboxName;
internal ConsentAwareMcpClientTool(McpClientTool inner, string toolsetName)
internal ConsentAwareMcpClientAIFunction(McpClientTool inner, string toolboxName)
{
this._inner = inner;
this._toolsetName = toolsetName;
this._toolboxName = toolboxName;
}
public override string Name => this._inner.Name;
@@ -59,7 +59,7 @@ internal sealed class ConsentAwareMcpClientTool : AIFunction
var state = McpConsentContext.Current.Value;
if (state is not null)
{
state.Pending = new McpConsentInfo(this._toolsetName, this._inner.Name, ex.Message);
state.Pending = new McpConsentInfo(this._toolboxName, this._inner.Name, ex.Message);
state.CancellationSource?.Cancel();
}
@@ -10,12 +10,12 @@ namespace Microsoft.Agents.AI.Foundry.Hosting;
public sealed class FoundryToolboxOptions
{
/// <summary>
/// Gets the list of toolset names to connect to at startup.
/// Each name corresponds to a toolset registered in the Foundry project.
/// Gets the list of toolbox names to connect to at startup.
/// Each name corresponds to a toolbox registered in the Foundry project.
/// The platform proxy URL is constructed as:
/// <c>{FOUNDRY_AGENT_TOOLSET_ENDPOINT}/{toolsetName}/mcp?api-version={ApiVersion}</c>
/// <c>{FOUNDRY_AGENT_TOOLSET_ENDPOINT}/{toolboxName}/mcp?api-version={ApiVersion}</c>
/// </summary>
public IList<string> ToolsetNames { get; } = new List<string>();
public IList<string> ToolboxNames { get; } = [];
/// <summary>
/// Gets or sets the Toolsets API version to use when constructing proxy URLs.
@@ -16,7 +16,7 @@ using ModelContextProtocol.Client;
namespace Microsoft.Agents.AI.Foundry.Hosting;
/// <summary>
/// An <see cref="IHostedService"/> that eagerly connects to the Foundry Toolsets MCP proxy at
/// An <see cref="IHostedService"/> that eagerly connects to the Foundry Toolboxes MCP proxy at
/// container startup, discovers tools via <c>tools/list</c>, and caches them so they can be
/// injected into every <see cref="ChatOptions"/> by
/// <see cref="AgentFrameworkResponseHandler"/>.
@@ -28,7 +28,7 @@ namespace Microsoft.Agents.AI.Foundry.Hosting;
/// </para>
/// <para>
/// Initialization is performed in <see cref="StartAsync"/> so the readiness probe is only satisfied
/// after all configured toolsets are connected and their tools discovered (spec §3.1 SHOULD).
/// after all configured toolboxes are connected and their tools discovered (spec §3.1 SHOULD).
/// </para>
/// </remarks>
public sealed class FoundryToolboxService : IHostedService, IAsyncDisposable
@@ -41,8 +41,8 @@ public sealed class FoundryToolboxService : IHostedService, IAsyncDisposable
private readonly List<HttpClient> _httpClients = [];
/// <summary>
/// Gets the cached list of <see cref="AITool"/> instances discovered from all connected toolsets.
/// Always non-null after startup; returns an empty list when no toolset endpoint is configured.
/// Gets the cached list of <see cref="AITool"/> instances discovered from all connected toolboxes.
/// Always non-null after startup; returns an empty list when no toolbox endpoint is configured.
/// </summary>
public IReadOnlyList<AITool> Tools { get; private set; } = [];
@@ -75,9 +75,9 @@ public sealed class FoundryToolboxService : IHostedService, IAsyncDisposable
return;
}
if (this._options.ToolsetNames.Count == 0)
if (this._options.ToolboxNames.Count == 0)
{
this._logger.LogInformation("No toolset names configured; toolbox support is disabled.");
this._logger.LogInformation("No toolbox names configured; toolbox support is disabled.");
this.Tools = [];
return;
}
@@ -88,21 +88,21 @@ public sealed class FoundryToolboxService : IHostedService, IAsyncDisposable
var allTools = new List<AITool>();
// Deduplicate toolset names to avoid duplicate MCP clients and ambiguous tool exposure
// Deduplicate toolbox names to avoid duplicate MCP clients and ambiguous tool exposure
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var toolsetName in this._options.ToolsetNames)
foreach (var toolboxName in this._options.ToolboxNames)
{
if (!seen.Add(toolsetName))
if (!seen.Add(toolboxName))
{
continue;
}
var proxyUrl = $"{endpoint.TrimEnd('/')}/{toolsetName}/mcp?api-version={this._options.ApiVersion}";
var proxyUrl = $"{endpoint.TrimEnd('/')}/{toolboxName}/mcp?api-version={this._options.ApiVersion}";
if (this._logger.IsEnabled(LogLevel.Information))
{
this._logger.LogInformation("Connecting to toolset '{ToolsetName}' at {ProxyUrl}.", toolsetName, proxyUrl);
this._logger.LogInformation("Connecting to toolbox '{ToolboxName}' at {ProxyUrl}.", toolboxName, proxyUrl);
}
try
@@ -118,7 +118,7 @@ public sealed class FoundryToolboxService : IHostedService, IAsyncDisposable
var transportOptions = new HttpClientTransportOptions
{
Endpoint = new Uri(proxyUrl),
Name = toolsetName,
Name = toolboxName,
};
var transport = new HttpClientTransport(transportOptions, httpClient);
@@ -144,22 +144,22 @@ public sealed class FoundryToolboxService : IHostedService, IAsyncDisposable
if (this._logger.IsEnabled(LogLevel.Information))
{
this._logger.LogInformation(
"Toolset '{ToolsetName}': discovered {ToolCount} tool(s).",
toolsetName,
"Toolbox '{ToolboxName}': discovered {ToolCount} tool(s).",
toolboxName,
tools.Count);
}
foreach (var tool in tools)
{
allTools.Add(new ConsentAwareMcpClientTool(tool, toolsetName));
allTools.Add(new ConsentAwareMcpClientAIFunction(tool, toolboxName));
}
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
this._logger.LogError(
ex,
"Failed to connect to toolset '{ToolsetName}'. Tools from this toolset will not be available.",
toolsetName);
"Failed to connect to toolbox '{ToolboxName}'. Tools from this toolbox will not be available.",
toolboxName);
}
}
@@ -8,13 +8,13 @@ namespace Microsoft.Agents.AI.Foundry.Hosting;
/// <summary>
/// Carries OAuth consent information for a single tool call that returned JSON-RPC error -32006.
/// </summary>
/// <param name="ToolsetName">The toolset name that owns the tool.</param>
/// <param name="ToolboxName">The toolbox name that owns the tool.</param>
/// <param name="ToolName">Fully-qualified tool name (e.g., <c>logicapps.send_email</c>).</param>
/// <param name="ConsentUrl">The OAuth consent URL the user must visit.</param>
internal sealed record McpConsentInfo(string ToolsetName, string ToolName, string ConsentUrl);
internal sealed record McpConsentInfo(string ToolboxName, string ToolName, string ConsentUrl);
/// <summary>
/// Per-request mutable state shared between <see cref="ConsentAwareMcpClientTool"/> (child context)
/// Per-request mutable state shared between <see cref="ConsentAwareMcpClientAIFunction"/> (child context)
/// and <see cref="AgentFrameworkResponseHandler"/> (parent context) via <see cref="McpConsentContext.Current"/>.
/// </summary>
/// <remarks>
@@ -31,7 +31,7 @@ internal sealed class RequestConsentState
}
/// <summary>
/// Thread-static (AsyncLocal) context that enables <see cref="ConsentAwareMcpClientTool"/>
/// Thread-static (AsyncLocal) context that enables <see cref="ConsentAwareMcpClientAIFunction"/>
/// to signal a consent error back to <see cref="AgentFrameworkResponseHandler"/> through the
/// <see cref="FunctionInvokingChatClient"/> tool loop.
/// </summary>
@@ -100,14 +100,14 @@ public static class FoundryHostingExtensions
}
/// <summary>
/// Registers the Foundry Toolbox service, which eagerly connects to the Foundry Toolsets
/// Registers the Foundry Toolbox service, which eagerly connects to the Foundry Toolboxes
/// MCP proxy at startup and provides MCP tools to <see cref="AgentFrameworkResponseHandler"/>.
/// </summary>
/// <remarks>
/// <para>
/// Each string in <paramref name="toolsetNames"/> is a toolset name registered in the Foundry
/// project. The proxy URL per toolset is constructed as:
/// <c>{FOUNDRY_AGENT_TOOLSET_ENDPOINT}/{toolsetName}/mcp?api-version=2025-05-01-preview</c>
/// Each string in <paramref name="toolboxNames"/> is a toolbox name registered in the Foundry
/// project. The proxy URL per toolbox is constructed as:
/// <c>{FOUNDRY_AGENT_TOOLSET_ENDPOINT}/{toolboxName}/mcp?api-version=2025-05-01-preview</c>
/// </para>
/// <para>
/// When <c>FOUNDRY_AGENT_TOOLSET_ENDPOINT</c> is absent, startup succeeds without error and
@@ -116,26 +116,26 @@ public static class FoundryHostingExtensions
/// <para>
/// Example:
/// <code>
/// builder.Services.AddFoundryToolboxes("my-tools", "another-toolset");
/// builder.Services.AddFoundryToolboxes("my-toolbox", "another-toolbox");
/// </code>
/// </para>
/// </remarks>
/// <param name="services">The service collection.</param>
/// <param name="toolsetNames">Names of the Foundry toolsets to connect to.</param>
/// <param name="toolboxNames">Names of the Foundry toolboxes to connect to.</param>
/// <returns>The service collection for chaining.</returns>
public static IServiceCollection AddFoundryToolboxes(
this IServiceCollection services,
params string[] toolsetNames)
params string[] toolboxNames)
{
ArgumentNullException.ThrowIfNull(services);
services.Configure<FoundryToolboxOptions>(opt =>
{
foreach (var name in toolsetNames)
foreach (var name in toolboxNames)
{
if (!string.IsNullOrWhiteSpace(name))
{
opt.ToolsetNames.Add(name);
opt.ToolboxNames.Add(name);
}
}
});