mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Rename toolset to toolbox in user-facing API; rename ConsentAwareMcpClientTool to ConsentAwareMcpClientAIFunction
This commit is contained in:
@@ -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))
|
||||
{
|
||||
|
||||
+5
-5
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user