Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
c73282b
ADR: Supporting user approvals
westey-m Jul 22, 2025
9e3b98b
Add more context.
westey-m Jul 22, 2025
a9a6a97
Add further justification
westey-m Jul 22, 2025
61063c0
Add decision drivers.
westey-m Jul 22, 2025
3a0dabf
Update with Copilot Suggestion
westey-m Jul 22, 2025
786374c
Merge branch 'main' into userapproval-adr
westey-m Jul 23, 2025
eace4e7
Update adr with feedback.
westey-m Jul 23, 2025
4076ed3
Adding another generalized option for all user input.
westey-m Jul 23, 2025
23b4fe5
Merge branch 'main' into userapproval-adr
westey-m Jul 24, 2025
12817f1
Add another base class based UserInput option
westey-m Jul 25, 2025
84e2855
Remove TextContent inheritance.
westey-m Jul 25, 2025
632635c
Change Schema and Strucutured input data types to JsonElement
westey-m Jul 25, 2025
461c8ef
Rename JsonSchema to Schema
westey-m Jul 25, 2025
7de1787
Merge branch 'main' into userapproval-adr
westey-m Jul 29, 2025
aa3c14e
Merge branch 'main' into userapproval-adr
crickman Jul 30, 2025
ab04590
Address some feedback.
westey-m Jul 30, 2025
aba7dc6
Merge branch 'main' into userapproval-adr
westey-m Jul 30, 2025
89ff266
Merge branch 'main' into userapproval-adr
crickman Jul 30, 2025
629204e
Add base user input request and response types with FunctionApproval …
westey-m Jul 31, 2025
3783074
Merge branch 'main' into userapproval-adr
crickman Jul 31, 2025
d3a9319
Merge branch 'main' into userapproval-adr
westey-m Aug 1, 2025
74d1f5d
Add POC ApprovalGeneratingChatClient
westey-m Aug 1, 2025
3c40615
Merge branch 'main' into userapproval-adr
crickman Aug 5, 2025
c068247
Add pre-FunctionInvokingChatClient ApprovalGeneratingChatClient POC
westey-m Aug 5, 2025
d8626a4
Merge branch 'userapproval-adr' of https://github.com/westey-m/agent-…
westey-m Aug 5, 2025
45254f3
Merge branch 'main' into userapproval-adr
westey-m Aug 5, 2025
3300dce
Merge branch 'main' into userapproval-adr
westey-m Aug 6, 2025
c8e42de
Fix namespaces
westey-m Aug 6, 2025
97352a0
Add combined FICC with approvals
westey-m Aug 7, 2025
2c39648
Bug fix.
westey-m Aug 7, 2025
5293568
Add MCP and ApprovalRequiredAIFunction POC
westey-m Aug 8, 2025
c5bedd0
Bug fixes
westey-m Aug 11, 2025
edcad34
Address PR Feedback.
westey-m Aug 11, 2025
0a61294
Add AIFunctionApprovalContext with some small efficiency improvements.
westey-m Aug 11, 2025
436c490
Remove unecessary helpers that are only used in one place.
westey-m Aug 11, 2025
0d5fcbf
Replace dictionary with array and lazy initialize.
westey-m Aug 11, 2025
8beb0f6
Improve sample code
westey-m Aug 11, 2025
9fbd0a7
Make user input id required and rename it to be more general.
westey-m Aug 12, 2025
302d43d
Merge branch 'main' into userapproval-adr
westey-m Aug 12, 2025
8ad466b
Upgrade packages to resolve conflicts.
westey-m Aug 12, 2025
049c9d3
Move some code into methods to simply main GetResponse.
westey-m Aug 12, 2025
39ef6bd
Adding approvals support to streaming
westey-m Aug 13, 2025
14d4773
Address PR comments.
westey-m Aug 13, 2025
b288fed
Change approval response type and naming
westey-m Aug 13, 2025
fc0bf74
Remove commented out validation checks that won't work for server sid…
westey-m Aug 13, 2025
d1939ad
Remove original metadata and read metadata from approval request mess…
westey-m Aug 14, 2025
6d8b20e
Small code refactoring and clarifying comments.
westey-m Aug 14, 2025
2bc90af
Remove failed POCs
westey-m Aug 14, 2025
d12cdf8
Update ADR with latest changes
westey-m Aug 14, 2025
3420337
Explain case when pre FICC AGCC fails
westey-m Aug 14, 2025
6a6f9dd
Add support for mcp servers over http.
westey-m Aug 14, 2025
f05bf90
Merge branch 'main' into userapproval-adr
westey-m Aug 14, 2025
3105941
Addressing PR comments.
westey-m Aug 15, 2025
26eb742
Address PR comments
westey-m Aug 15, 2025
0adf519
Merge branch 'main' into userapproval-adr
westey-m Aug 15, 2025
b2a4915
Merge latest updates from main
westey-m Aug 15, 2025
5e52c11
Add FICC unit tests and fix bugs.
westey-m Aug 19, 2025
b1289e3
Add additional unit test.
westey-m Aug 19, 2025
0b4405e
Refactor ConvertToFunctionCallContentMessages to avoid unecessary all…
westey-m Aug 20, 2025
545c231
Refactor preinvocation logic to share between streaming and non-strea…
westey-m Aug 20, 2025
e6aeebb
Move some approval generation code to a separate method to simplify m…
westey-m Aug 20, 2025
a1b2973
More refactoring, additional unit tests and bug fixes.
westey-m Aug 20, 2025
906ad2f
Remove comments delineating additions.
westey-m Aug 20, 2025
4c59b25
Merge latest changes from main
westey-m Aug 20, 2025
66b904e
Fixing some typos.
westey-m Aug 20, 2025
d3dd02e
Merge branch 'main' into userapproval-adr
westey-m Aug 21, 2025
21680be
Address PR comments.
westey-m Aug 22, 2025
a712408
Address PR comments.
westey-m Aug 22, 2025
c1da46b
Address PR feedback.
westey-m Aug 22, 2025
091f754
Fix bug for server side threads.
westey-m Aug 22, 2025
7518b70
Address PR comments
westey-m Aug 25, 2025
a349c79
Merge from main
westey-m Aug 25, 2025
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
522 changes: 522 additions & 0 deletions docs/decisions/00NN-userapproval-content-types.md

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions dotnet/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.3" />
<!-- System.* -->
<PackageVersion Include="System.Linq.Async" Version="6.0.3" />
<PackageVersion Include="System.Net.ServerSentEvents" Version="9.0.8" />
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
<PackageVersion Include="System.Net.ServerSentEvents" Version="10.0.0-preview.6.25358.103" />
<PackageVersion Include="System.Text.Json" Version="9.0.8" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="9.0.8" />
<PackageVersion Include="System.Threading.Channels" Version="9.0.8" />
Expand All @@ -47,7 +48,7 @@
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
<!-- Microsoft.Extensions.* -->
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.8" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.0-preview.6.25358.103" />
<PackageVersion Include="OpenAI" Version="2.3.0" />
<PackageVersion Include="Microsoft.Extensions.AI" Version="9.8.0" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="9.8.0-preview.1.25412.6" />
Expand All @@ -72,6 +73,8 @@
<PackageVersion Include="Microsoft.Agents.CopilotStudio.Client" Version="1.1.151" />
<!-- Identity -->
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.74.1" />
<!-- Other -->
<PackageVersion Include="ModelContextProtocol" Version="0.3.0-preview.2" />
<!-- Test -->
<PackageVersion Include="FluentAssertions" Version="8.5.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
Expand Down
1 change: 1 addition & 0 deletions dotnet/agent-framework-dotnet.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@
<Project Path="src/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.csproj" />
<Project Path="src/Microsoft.Extensions.AI.Agents.Runtime/Microsoft.Extensions.AI.Agents.Runtime.csproj" />
<Project Path="src/Microsoft.Extensions.AI.Agents/Microsoft.Extensions.AI.Agents.csproj" />
<Project Path="src/Microsoft.Extensions.AI.ModelContextProtocol/Microsoft.Extensions.AI.ModelContextProtocol.csproj" Id="4859a869-0c6b-417d-b6d8-6ccbcb60786c" />
</Folder>
<Folder Name="/Tests/" />
<Folder Name="/Tests/Cosmos/">
Expand Down
2 changes: 2 additions & 0 deletions dotnet/samples/GettingStarted/GettingStarted.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,15 @@
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
<PackageReference Include="System.Linq.Async" />
<PackageReference Include="System.Net.Http" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Agents.Orchestration\Microsoft.Agents.Orchestration.csproj" />
<ProjectReference Include="..\..\src\Microsoft.Extensions.AI.Agents.AzureAI\Microsoft.Extensions.AI.Agents.AzureAI.csproj" />
<ProjectReference Include="..\..\src\Microsoft.Extensions.AI.Agents.OpenAI\Microsoft.Extensions.AI.Agents.OpenAI.csproj" />
<ProjectReference Include="..\..\src\Microsoft.Extensions.AI.Agents\Microsoft.Extensions.AI.Agents.csproj" />
<ProjectReference Include="..\..\src\Microsoft.Extensions.AI.ModelContextProtocol\Microsoft.Extensions.AI.ModelContextProtocol.csproj" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
// Copyright (c) Microsoft. All rights reserved.

using System.ComponentModel;
#if NETFRAMEWORK
using System.Net.Http;
#endif
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Extensions.AI.ModelContextProtocol;
using Microsoft.Extensions.DependencyInjection;

namespace Steps;

Expand Down Expand Up @@ -32,7 +37,12 @@ public async Task ApprovalsWithTools(ChatClientProviders provider)
tools: [
new ApprovalRequiredAIFunction(AIFunctionFactory.Create(menuTools.GetMenu)),
new ApprovalRequiredAIFunction(AIFunctionFactory.Create(menuTools.GetSpecials)),
AIFunctionFactory.Create(menuTools.GetItemPrice)
AIFunctionFactory.Create(menuTools.GetItemPrice),
new HostedMcpServerTool("Tiktoken Documentation", new Uri("https://gitmcp.io/openai/tiktoken"))
{
AllowedTools = ["search_tiktoken_documentation", "fetch_tiktoken_documentation"],
ApprovalMode = HostedMcpServerToolApprovalMode.AlwaysRequire,
}
]);

// Create the server-side agent Id when applicable (depending on the provider).
Expand All @@ -41,15 +51,34 @@ public async Task ApprovalsWithTools(ChatClientProviders provider)
// Get the chat client to use for the agent.
using var chatClient = base.GetChatClient(provider, agentOptions);

// Modify the chat client to include MCP and built-in approvals if not already present.
var chatBuilder = chatClient.AsBuilder();
if (chatClient.GetService<HostedMCPChatClient>() is null)
{
chatBuilder.Use((IChatClient innerClient, IServiceProvider services) =>
{
return new HostedMCPChatClient(innerClient, new HttpClient());
});
}
if (chatClient.GetService<NewFunctionInvokingChatClient>() is null)
{
chatBuilder.Use((IChatClient innerClient, IServiceProvider services) =>
{
return new NewFunctionInvokingChatClient(innerClient, null, services);
});
}
using var chatClientWithMCPAndApprovals = chatBuilder.Build();

// Define the agent
var agent = new ChatClientAgent(chatClient, agentOptions);
var agent = new ChatClientAgent(chatClientWithMCPAndApprovals, agentOptions);

// Create the chat history thread to capture the agent interaction.
var thread = agent.GetNewThread();

// Respond to user input, invoking functions where appropriate.
await RunAgentAsync("What is the special soup and its price?");
await RunAgentAsync("What is the special drink?");
await RunAgentAsync("how does tiktoken work?");

async Task RunAgentAsync(string input)
{
Expand All @@ -63,7 +92,7 @@ async Task RunAgentAsync(string input)
// Approve GetSpecials function calls, reject all others.
List<ChatMessage> nextIterationMessages = userInputRequests?.Select((request) => request switch
{
FunctionApprovalRequestContent functionApprovalRequest when functionApprovalRequest.FunctionCall.Name == "GetSpecials" =>
FunctionApprovalRequestContent functionApprovalRequest when functionApprovalRequest.FunctionCall.Name == "GetSpecials" || functionApprovalRequest.FunctionCall.Name == "add" || functionApprovalRequest.FunctionCall.Name == "search_tiktoken_documentation" =>
new ChatMessage(ChatRole.User, [functionApprovalRequest.CreateResponse(approved: true)]),

FunctionApprovalRequestContent functionApprovalRequest =>
Expand Down Expand Up @@ -110,6 +139,11 @@ public async Task ApprovalsWithToolsStreaming(ChatClientProviders provider)
new ApprovalRequiredAIFunction(AIFunctionFactory.Create(menuTools.GetMenu)),
new ApprovalRequiredAIFunction(AIFunctionFactory.Create(menuTools.GetSpecials)),
AIFunctionFactory.Create(menuTools.GetItemPrice),
new HostedMcpServerTool("Tiktoken Documentation", new Uri("https://gitmcp.io/openai/tiktoken"))
{
AllowedTools = ["search_tiktoken_documentation", "fetch_tiktoken_documentation"],
ApprovalMode = HostedMcpServerToolApprovalMode.AlwaysRequire,
}
]);

// Create the server-side agent Id when applicable (depending on the provider).
Expand All @@ -118,15 +152,34 @@ public async Task ApprovalsWithToolsStreaming(ChatClientProviders provider)
// Get the chat client to use for the agent.
using var chatClient = base.GetChatClient(provider, agentOptions);

// Modify the chat client to include MCP and built-in approvals if not already present.
var chatBuilder = chatClient.AsBuilder();
if (chatClient.GetService<HostedMCPChatClient>() is null)
{
chatBuilder.Use((IChatClient innerClient, IServiceProvider services) =>
{
return new HostedMCPChatClient(innerClient, new HttpClient());
});
}
if (chatClient.GetService<NewFunctionInvokingChatClient>() is null)
{
chatBuilder.Use((IChatClient innerClient, IServiceProvider services) =>
{
return new NewFunctionInvokingChatClient(innerClient, null, services);
});
}
using var chatClientWithMCPAndApprovals = chatBuilder.Build();

// Define the agent
var agent = new ChatClientAgent(chatClient, agentOptions);
var agent = new ChatClientAgent(chatClientWithMCPAndApprovals, agentOptions);

// Create the chat history thread to capture the agent interaction.
var thread = agent.GetNewThread();

// Respond to user input, invoking functions where appropriate.
await RunAgentAsync("What is the special soup and its price?");
await RunAgentAsync("What is the special drink?");
await RunAgentAsync("how does tiktoken work?");

async Task RunAgentAsync(string input)
{
Expand All @@ -140,7 +193,7 @@ async Task RunAgentAsync(string input)
// Approve GetSpecials function calls, reject all others.
List<ChatMessage> nextIterationMessages = userInputRequests?.Select((request) => request switch
{
FunctionApprovalRequestContent functionApprovalRequest when functionApprovalRequest.FunctionCall.Name == "GetSpecials" =>
FunctionApprovalRequestContent functionApprovalRequest when functionApprovalRequest.FunctionCall.Name == "GetSpecials" || functionApprovalRequest.FunctionCall.Name == "add" || functionApprovalRequest.FunctionCall.Name == "search_tiktoken_documentation" =>
new ChatMessage(ChatRole.User, [functionApprovalRequest.CreateResponse(approved: true)]),

FunctionApprovalRequestContent functionApprovalRequest =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Collections.Generic;
using Microsoft.Shared.Diagnostics;

namespace Microsoft.Extensions.AI;

/// <summary>
/// Represents a hosted MCP server tool that can be specified to an AI service.
/// </summary>
public class HostedMcpServerTool : AITool
{
/// <summary>
/// Initializes a new instance of the <see cref="HostedMcpServerTool"/> class.
/// </summary>
/// <param name="serverName">The name of the remote MCP server.</param>
/// <param name="url">The URL of the remote MCP server.</param>
public HostedMcpServerTool(string serverName, Uri url)
{
ServerName = Throw.IfNullOrWhitespace(serverName);
Url = Throw.IfNull(url);
}

/// <summary>
/// Gets the name of the remote MCP server that is used to identify it.
/// </summary>
public string ServerName { get; }

/// <summary>
/// Gets the URL of the remote MCP server.
/// </summary>
public Uri Url { get; }

/// <summary>
/// Gets or sets the description of the remote MCP server, used to provide more context to the AI service.
/// </summary>
public string? ServerDescription { get; set; }

/// <summary>
/// Gets or sets the list of tools allowed to be used by the AI service.
/// </summary>
public IList<string>? AllowedTools { get; set; }

/// <summary>
/// Gets or sets the approval mode that indicates when the AI service should require user approval for tool calls to the remote MCP server.
/// </summary>
/// <remarks>
/// You can set this property to <see cref="HostedMcpServerToolApprovalMode.AlwaysRequire"/> to require approval for all tool calls,
/// or to <see cref="HostedMcpServerToolApprovalMode.NeverRequire"/> to never require approval.
/// </remarks>
public HostedMcpServerToolApprovalMode? ApprovalMode { get; set; }

/// <summary>
/// Gets or sets the HTTP headers that the AI service should use when making tool calls to the remote MCP server.
/// </summary>
/// <remarks>
/// This property is useful for specifying the authentication header or other headers required by the MCP server.
/// </remarks>
public IDictionary<string, string>? Headers { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Diagnostics;

namespace Microsoft.Extensions.AI;

/// <summary>
/// Indicates that approval is always required for tool calls to a hosted MCP server.
/// </summary>
/// <remarks>
/// Use <see cref="HostedMcpServerToolApprovalMode.AlwaysRequire"/> to get an instance of <see cref="HostedMcpServerToolAlwaysRequireApprovalMode"/>.
/// </remarks>
[DebuggerDisplay(nameof(AlwaysRequire))]
public sealed class HostedMcpServerToolAlwaysRequireApprovalMode : HostedMcpServerToolApprovalMode;
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Collections.Generic;

namespace Microsoft.Extensions.AI;

/// <summary>
/// Describes how approval is required for tool calls to a hosted MCP server.
/// </summary>
/// <remarks>
/// The predefined values <see cref="AlwaysRequire" />, and <see cref="NeverRequire"/> are provided to specify handling for all tools.
/// To specify approval behavior for individual tool names, use <see cref="RequireSpecific(IList{string}, IList{string})"/>.
/// </remarks>
#pragma warning disable CA1052 // Static holder types should be Static or NotInheritable
public class HostedMcpServerToolApprovalMode
#pragma warning restore CA1052
{
/// <summary>
/// Gets a predefined <see cref="HostedMcpServerToolApprovalMode"/> indicating that all tool calls to a hosted MCP server always require approval.
/// </summary>
public static HostedMcpServerToolAlwaysRequireApprovalMode AlwaysRequire { get; } = new();

/// <summary>
/// Gets a predefined <see cref="HostedMcpServerToolApprovalMode"/> indicating that all tool calls to a hosted MCP server never require approval.
/// </summary>
public static HostedMcpServerToolNeverRequireApprovalMode NeverRequire { get; } = new();

private protected HostedMcpServerToolApprovalMode()
{
}

/// <summary>
/// Instantiates a <see cref="HostedMcpServerToolApprovalMode"/> that specifies approval behavior for individual tool names.
/// </summary>
/// <param name="alwaysRequireApprovalToolNames">The list of tools names that always require approval.</param>
/// <param name="neverRequireApprovalToolNames">The list of tools names that never require approval.</param>
/// <returns>An instance of <see cref="HostedMcpServerToolRequireSpecificApprovalMode"/> for the specified tool names.</returns>
public static HostedMcpServerToolRequireSpecificApprovalMode RequireSpecific(IList<string>? alwaysRequireApprovalToolNames, IList<string>? neverRequireApprovalToolNames)
=> new(alwaysRequireApprovalToolNames, neverRequireApprovalToolNames);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Diagnostics;

namespace Microsoft.Extensions.AI;

/// <summary>
/// Indicates that approval is never required for tool calls to a hosted MCP server.
/// </summary>
/// <remarks>
/// Use <see cref="HostedMcpServerToolApprovalMode.NeverRequire"/> to get an instance of <see cref="HostedMcpServerToolNeverRequireApprovalMode"/>.
/// </remarks>
[DebuggerDisplay(nameof(NeverRequire))]
public sealed class HostedMcpServerToolNeverRequireApprovalMode : HostedMcpServerToolApprovalMode;
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Collections.Generic;

namespace Microsoft.Extensions.AI;

/// <summary>
/// Represents a mode where approval behavior is specified for individual tool names.
/// </summary>
public sealed class HostedMcpServerToolRequireSpecificApprovalMode : HostedMcpServerToolApprovalMode
{
/// <summary>
/// Initializes a new instance of the <see cref="HostedMcpServerToolRequireSpecificApprovalMode"/> class that specifies approval behavior for individual tool names.
/// </summary>
/// <param name="alwaysRequireApprovalToolNames">The list of tools names that always require approval.</param>
/// <param name="neverRequireApprovalToolNames">The list of tools names that never require approval.</param>
public HostedMcpServerToolRequireSpecificApprovalMode(IList<string>? alwaysRequireApprovalToolNames, IList<string>? neverRequireApprovalToolNames)
{
AlwaysRequireApprovalToolNames = alwaysRequireApprovalToolNames;
NeverRequireApprovalToolNames = neverRequireApprovalToolNames;
}

/// <summary>
/// Gets or sets the list of tool names that always require approval.
/// </summary>
public IList<string>? AlwaysRequireApprovalToolNames { get; set; }

/// <summary>
/// Gets or sets the list of tool names that never require approval.
/// </summary>
public IList<string>? NeverRequireApprovalToolNames { get; set; }
}
Loading
Loading