-
Notifications
You must be signed in to change notification settings - Fork 857
Description
Background and motivation
Two proposals in one:
- Abstractions for input requests and responses, and concrete types for handling function approvals req/resp.
- Abstractions for interacting with MCP capabilities available in service providers like OpenAI and Anthropic, depends on the one above for approval flows.
These APIs are already shipping as Experimental.
Differences with main.
- UserInput -> Input
- Id -> RequestId
- Contains proposed changes from MCP/Approvals stabilization #7245
- MCP contents now derive from Function contents
- MCP approvals now use Function approval types
- FunctionCallContent/FunctionResultContent include polymorphic attributes in reaction to now-derived MCP contents
API Proposal
namespace Microsoft.Extensions.AI;
// Contents
[JsonPolymorphic]
[JsonDerivedType(typeof(FunctionApprovalRequestContent), "functionApprovalRequest")]
public abstract class InputRequestContent : AIContent
{
public abstract string RequestId { get; }
}
[JsonPolymorphic]
[JsonDerivedType(typeof(FunctionApprovalResponseContent), "functionApprovalResponse")]
public abstract class InputResponseContent : AIContent
{
public abstract string RequestId { get; }
}
public sealed class FunctionApprovalRequestContent : InputRequestContent
{
public FunctionApprovalRequestContent(string requestId, FunctionCallContent functionCall);
public FunctionCallContent FunctionCall { get; }
public FunctionApprovalResponseContent CreateResponse(bool approved, string? reason = null);
}
public sealed class FunctionApprovalResponseContent : InputResponseContent
{
public FunctionApprovalResponseContent(string requestId, bool approved, FunctionCallContent functionCall);
public bool Approved { get; }
public FunctionCallContent FunctionCall { get; }
public string? Reason { get; set; }
}
public sealed class McpServerToolCallContent : FunctionCallContent
{
public McpServerToolCallContent(string callId, string name, string? serverName);
public string? ServerName { get; }
}
public sealed class McpServerToolResultContent : FunctionResultContent
{
public McpServerToolResultContent(string callId);
}
[JsonPolymorphic]
[JsonDerivedType(typeof(McpServerToolCallContent), "mcpServerToolCall")]
public class FunctionCallContent : AIContent // Existing type, attributes are new.
{
[JsonConstructor]
public FunctionCallContent(string callId, string name, IDictionary<string, object?>? arguments = null);
public string CallId { get; }
public string Name { get; }
public IDictionary<string, object?>? Arguments { get; set; }
[JsonIgnore]
public Exception? Exception { get; set; }
public bool InformationalOnly { get; set; }
public static FunctionCallContent CreateFromParsedArguments<TEncoding>(
TEncoding encodedArguments,
string callId,
string name,
Func<TEncoding, IDictionary<string, object?>?> argumentParser);
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private string DebuggerDisplay { get; }
}
[JsonPolymorphic]
[JsonDerivedType(typeof(McpServerToolResultContent), "mcpServerToolResult")]
public class FunctionResultContent : AIContent // Existing type, attributes are new.
{
[JsonConstructor]
public FunctionResultContent(string callId, object? result);
public string CallId { get; }
public object? Result { get; set; }
[JsonIgnore]
public Exception? Exception { get; set; }
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private string DebuggerDisplay { get; }
}
// Tools.
public sealed class ApprovalRequiredAIFunction : DelegatingAIFunction
{
public ApprovalRequiredAIFunction(AIFunction innerFunction);
}
public class HostedMcpServerTool : AITool
{
public HostedMcpServerTool(string serverName, string serverAddress);
public HostedMcpServerTool(string serverName, string serverAddress, IReadOnlyDictionary<string, object?>? additionalProperties);
public HostedMcpServerTool(string serverName, Uri serverUrl);
public HostedMcpServerTool(string serverName, Uri serverUrl, IReadOnlyDictionary<string, object?>? additionalProperties);
public override IReadOnlyDictionary<string, object?> AdditionalProperties { get; }
public IList<string>? AllowedTools { get; set; }
public HostedMcpServerToolApprovalMode? ApprovalMode { get; set; }
public string? AuthorizationToken { get; set; }
public IDictionary<string, string> Headers { get; }
public override string Name { get; }
public string ServerAddress { get; }
public string? ServerDescription { get; set; }
public string ServerName { get; }
}
// HostedMcpServerTool utilities.
[JsonPolymorphic]
[JsonDerivedType(typeof(HostedMcpServerToolAlwaysRequireApprovalMode), "always")]
[JsonDerivedType(typeof(HostedMcpServerToolNeverRequireApprovalMode), "never")]
[JsonDerivedType(typeof(HostedMcpServerToolRequireSpecificApprovalMode), "requireSpecific")]
public class HostedMcpServerToolApprovalMode // Follows the pattern of `ChatToolMode` https://source.dot.net/#Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatToolMode.cs,854705957c5b4bd1.
{
public static HostedMcpServerToolAlwaysRequireApprovalMode AlwaysRequire { get; }
public static HostedMcpServerToolNeverRequireApprovalMode NeverRequire { get; }
public static HostedMcpServerToolRequireSpecificApprovalMode RequireSpecific(IList<string>? alwaysRequireApprovalToolNames, IList<string>? neverRequireApprovalToolNames);
}
[DebuggerDisplay("AlwaysRequire")]
public sealed class HostedMcpServerToolAlwaysRequireApprovalMode : HostedMcpServerToolApprovalMode
{
public HostedMcpServerToolAlwaysRequireApprovalMode();
public override bool Equals(object? obj);
public override int GetHashCode();
}
[DebuggerDisplay("NeverRequire")]
public sealed class HostedMcpServerToolNeverRequireApprovalMode : HostedMcpServerToolApprovalMode
{
public HostedMcpServerToolNeverRequireApprovalMode();
public override bool Equals(object? obj);
public override int GetHashCode();
}
public sealed class HostedMcpServerToolRequireSpecificApprovalMode : HostedMcpServerToolApprovalMode
{
public HostedMcpServerToolRequireSpecificApprovalMode(IList<string>? alwaysRequireApprovalToolNames, IList<string>? neverRequireApprovalToolNames);
public IList<string>? AlwaysRequireApprovalToolNames { get; set; }
public IList<string>? NeverRequireApprovalToolNames { get; set; }
public override bool Equals(object? obj);
public override int GetHashCode();
}API Usage
using Microsoft.Extensions.AI;
IChatClient client = GetMyChatClient();
var calendarMcp = new HostedMcpServerTool("google-calendar", new Uri("https://mcp.google.example/calendar"))
{
ApprovalMode = HostedMcpServerToolApprovalMode.RequireSpecific(
alwaysRequireApprovalToolNames: ["create_event", "delete_event"],
neverRequireApprovalToolNames: ["get_free_busy", "list_events"])
};
var bookFlight = new ApprovalRequiredAIFunction(
AIFunctionFactory.Create((string origin, string destination, string date) =>
new { Confirmation = $"UA-{Random.Shared.Next(100000, 999999)}" },
name: "book_flight"));
ChatOptions options = new() { Tools = [calendarMcp, bookFlight] };
List<ChatMessage> messages = [new(ChatRole.User, "Book a Seattle to NYC flight next week when I'm free")];
while (true)
{
ChatResponse response = await client.GetResponseAsync(messages, options);
var approvalRequests = response.Messages
.SelectMany(m => m.Contents)
.OfType<FunctionApprovalRequestContent>()
.ToList();
if (approvalRequests.Count == 0)
{
Console.WriteLine(response.Text); // Final response.
break;
}
// In production: prompt user. Here we auto-approve.
messages.AddRange(response.Messages);
messages.Add(new(ChatRole.User, approvalRequests.Select(a => a.CreateResponse(true)).ToArray()));
}Alternative Designs
Do not extend FCC/FRC
Instead of extending FunctionCallContent/FunctionResultContent, MCP contents could remain as standalone AIContent types—aligning with CodeInterpreterToolCallContent and ImageGenerationToolCallContent which represent similar hosted service capabilities. This maintains a cleaner semantic separation between client-invokable functions and hosted tool invocations, avoiding inherited properties (InvocationRequired, Exception) that aren't meaningful for server-side tools. The trade-off is that approvals would require either dedicated MCP approval types (as currently exists) or generalizing FunctionApprovalRequestContent.FunctionCall to accept a broader AIContent/CallContent base type.
public sealed class McpServerToolCallContent : AIContent
{
public McpServerToolCallContent(string callId, string toolName, string? serverName);
public string CallId { get; }
public string ToolName { get; }
public string? ServerName { get; }
public IReadOnlyDictionary<string, object?>? Arguments { get; set; }
}
public sealed class McpServerToolResultContent : AIContent
{
public McpServerToolResultContent(string callId);
public string CallId { get; }
public IList<AIContent>? Outputs { get; set; }
}FunctionApprovalRequestContent.CreateResponse(bool, string?) returns FunctionApprovalResponseContent
This is symmetric, but may be error-prone: users must decide which ChatRole to use when wrapping the response in a ChatMessage, typically ChatRole.User, since approvals represent user decisions in most cases (not all, that's why we are dropping User from InputRequestContent). It's also problematic for IChatClients that handle contents differently per role. However, if CreateResponse returned a ChatMessage instead, approvals would be forced to one per message, which is cumbersome for multi-approvals scenarios.
We could add CreateResponseMessage(bool, string?, ChatRole?).
Notes
McpServerToolCallContent/McpServerToolResultContent were originally prefixed with Hosted to follow convention with HostedFileContent and HostedVectorStoreContent, but the prefix was later dropped. The distinction is that Hosted*Content types represent references to provider-hosted resources (file IDs, vector store IDs), while *ToolCallContent/*ToolResultContent types represent tool invocation data—describing what the tool was asked to do and what it returned. This pattern is consistent with CodeInterpreterToolCallContent/CodeInterpreterToolResultContent and ImageGenerationToolCallContent/ImageGenerationToolResultContent, which also omit the Hosted prefix despite being invoked by hosted services. The Hosted prefix remains on HostedMcpServerTool (and similar AITool types like HostedCodeInterpreterTool) because these are configuration markers telling the provider to use a remotely-hosted capability, not actual tool implementations.
Risks
No response
cc @westey-m