Skip to content

Commit a4db9d8

Browse files
jozkeestephentoub
andauthored
Add abstraction for remote MCP servers (#6664)
* Simpfliy Responses streaming handling * Add abstraction for remote MCP servers * Update MCP support to accomodate responses and approvals --------- Co-authored-by: Stephen Toub <[email protected]>
1 parent 3645ccc commit a4db9d8

30 files changed

+1650
-52
lines changed

src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/RequiredChatToolMode.cs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,6 @@ public RequiredChatToolMode(string? requiredFunctionName)
4141
RequiredFunctionName = requiredFunctionName;
4242
}
4343

44-
// The reason for not overriding Equals/GetHashCode (e.g., so two instances are equal if they
45-
// have the same RequiredFunctionName) is to leave open the option to unseal the type in the
46-
// future. If we did define equality based on RequiredFunctionName but a subclass added further
47-
// fields, this would lead to wrong behavior unless the subclass author remembers to re-override
48-
// Equals/GetHashCode as well, which they likely won't.
49-
5044
/// <summary>Gets a string representing this instance to display in the debugger.</summary>
5145
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
5246
private string DebuggerDisplay => $"Required: {RequiredFunctionName ?? "Any"}";

src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ namespace Microsoft.Extensions.AI;
2424
// experimental types in its source generated files.
2525
// [JsonDerivedType(typeof(FunctionApprovalRequestContent), typeDiscriminator: "functionApprovalRequest")]
2626
// [JsonDerivedType(typeof(FunctionApprovalResponseContent), typeDiscriminator: "functionApprovalResponse")]
27+
// [JsonDerivedType(typeof(McpServerToolCallContent), typeDiscriminator: "mcpServerToolCall")]
28+
// [JsonDerivedType(typeof(McpServerToolResultContent), typeDiscriminator: "mcpServerToolResult")]
29+
// [JsonDerivedType(typeof(McpServerToolApprovalRequestContent), typeDiscriminator: "mcpServerToolApprovalRequest")]
30+
// [JsonDerivedType(typeof(McpServerToolApprovalResponseContent), typeDiscriminator: "mcpServerToolApprovalResponse")]
2731

2832
public class AIContent
2933
{
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Diagnostics.CodeAnalysis;
6+
using Microsoft.Shared.Diagnostics;
7+
8+
namespace Microsoft.Extensions.AI;
9+
10+
/// <summary>
11+
/// Represents a request for user approval of an MCP server tool call.
12+
/// </summary>
13+
[Experimental("MEAI001")]
14+
public sealed class McpServerToolApprovalRequestContent : UserInputRequestContent
15+
{
16+
/// <summary>
17+
/// Initializes a new instance of the <see cref="McpServerToolApprovalRequestContent"/> class.
18+
/// </summary>
19+
/// <param name="id">The ID that uniquely identifies the MCP server tool approval request/response pair.</param>
20+
/// <param name="toolCall">The tool call that requires user approval.</param>
21+
/// <exception cref="ArgumentNullException"><paramref name="id"/> is <see langword="null"/>.</exception>
22+
/// <exception cref="ArgumentException"><paramref name="id"/> is empty or composed entirely of whitespace.</exception>
23+
/// <exception cref="ArgumentNullException"><paramref name="toolCall"/> is <see langword="null"/>.</exception>
24+
public McpServerToolApprovalRequestContent(string id, McpServerToolCallContent toolCall)
25+
: base(id)
26+
{
27+
ToolCall = Throw.IfNull(toolCall);
28+
}
29+
30+
/// <summary>
31+
/// Gets the tool call that pre-invoke approval is required for.
32+
/// </summary>
33+
public McpServerToolCallContent ToolCall { get; }
34+
35+
/// <summary>
36+
/// Creates a <see cref="McpServerToolApprovalResponseContent"/> to indicate whether the function call is approved or rejected based on the value of <paramref name="approved"/>.
37+
/// </summary>
38+
/// <param name="approved"><see langword="true"/> if the function call is approved; otherwise, <see langword="false"/>.</param>
39+
/// <returns>The <see cref="FunctionApprovalResponseContent"/> representing the approval response.</returns>
40+
public McpServerToolApprovalResponseContent CreateResponse(bool approved) => new(Id, approved);
41+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Diagnostics.CodeAnalysis;
6+
7+
namespace Microsoft.Extensions.AI;
8+
9+
/// <summary>
10+
/// Represents a response to an MCP server tool approval request.
11+
/// </summary>
12+
[Experimental("MEAI001")]
13+
public sealed class McpServerToolApprovalResponseContent : UserInputResponseContent
14+
{
15+
/// <summary>
16+
/// Initializes a new instance of the <see cref="McpServerToolApprovalResponseContent"/> class.
17+
/// </summary>
18+
/// <param name="id">The ID that uniquely identifies the MCP server tool approval request/response pair.</param>
19+
/// <param name="approved"><see langword="true"/> if the MCP server tool call is approved; otherwise, <see langword="false"/>.</param>
20+
/// <exception cref="ArgumentNullException"><paramref name="id"/> is <see langword="null"/>.</exception>
21+
/// <exception cref="ArgumentException"><paramref name="id"/> is empty or composed entirely of whitespace.</exception>
22+
public McpServerToolApprovalResponseContent(string id, bool approved)
23+
: base(id)
24+
{
25+
Approved = approved;
26+
}
27+
28+
/// <summary>
29+
/// Gets a value indicating whether the user approved the request.
30+
/// </summary>
31+
public bool Approved { get; }
32+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Diagnostics.CodeAnalysis;
7+
using Microsoft.Shared.Diagnostics;
8+
9+
namespace Microsoft.Extensions.AI;
10+
11+
/// <summary>
12+
/// Represents a tool call request to a MCP server.
13+
/// </summary>
14+
/// <remarks>
15+
/// This content type is used to represent an invocation of an MCP server tool by a hosted service.
16+
/// It is informational only.
17+
/// </remarks>
18+
[Experimental("MEAI001")]
19+
public sealed class McpServerToolCallContent : AIContent
20+
{
21+
/// <summary>
22+
/// Initializes a new instance of the <see cref="McpServerToolCallContent"/> class.
23+
/// </summary>
24+
/// <param name="callId">The tool call ID.</param>
25+
/// <param name="toolName">The tool name.</param>
26+
/// <param name="serverName">The MCP server name.</param>
27+
/// <exception cref="ArgumentNullException"><paramref name="callId"/>, <paramref name="toolName"/>, or <paramref name="serverName"/> are <see langword="null"/>.</exception>
28+
/// <exception cref="ArgumentException"><paramref name="callId"/>, <paramref name="toolName"/>, or <paramref name="serverName"/> are empty or composed entirely of whitespace.</exception>
29+
public McpServerToolCallContent(string callId, string toolName, string serverName)
30+
{
31+
CallId = Throw.IfNullOrWhitespace(callId);
32+
ToolName = Throw.IfNullOrWhitespace(toolName);
33+
ServerName = Throw.IfNullOrWhitespace(serverName);
34+
}
35+
36+
/// <summary>
37+
/// Gets the tool call ID.
38+
/// </summary>
39+
public string CallId { get; }
40+
41+
/// <summary>
42+
/// Gets the name of the tool called.
43+
/// </summary>
44+
public string ToolName { get; }
45+
46+
/// <summary>
47+
/// Gets the name of the MCP server.
48+
/// </summary>
49+
public string ServerName { get; }
50+
51+
/// <summary>
52+
/// Gets or sets the arguments used for the tool call.
53+
/// </summary>
54+
public IReadOnlyDictionary<string, object?>? Arguments { get; set; }
55+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Diagnostics.CodeAnalysis;
7+
using Microsoft.Shared.Diagnostics;
8+
9+
namespace Microsoft.Extensions.AI;
10+
11+
/// <summary>
12+
/// Represents the result of a MCP server tool call.
13+
/// </summary>
14+
/// <remarks>
15+
/// This content type is used to represent the result of an invocation of an MCP server tool by a hosted service.
16+
/// It is informational only.
17+
/// </remarks>
18+
[Experimental("MEAI001")]
19+
public sealed class McpServerToolResultContent : AIContent
20+
{
21+
/// <summary>
22+
/// Initializes a new instance of the <see cref="McpServerToolResultContent"/> class.
23+
/// </summary>
24+
/// <param name="callId">The tool call ID.</param>
25+
/// <exception cref="ArgumentNullException"><paramref name="callId"/> is <see langword="null"/>.</exception>
26+
/// <exception cref="ArgumentException"><paramref name="callId"/> is empty or composed entirely of whitespace.</exception>
27+
public McpServerToolResultContent(string callId)
28+
{
29+
CallId = Throw.IfNullOrWhitespace(callId);
30+
}
31+
32+
/// <summary>
33+
/// Gets the tool call ID.
34+
/// </summary>
35+
public string CallId { get; }
36+
37+
/// <summary>
38+
/// Gets or sets the output of the tool call.
39+
/// </summary>
40+
public IList<AIContent>? Output { get; set; }
41+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics;
5+
using System.Diagnostics.CodeAnalysis;
6+
7+
namespace Microsoft.Extensions.AI;
8+
9+
/// <summary>
10+
/// Indicates that approval is always required for tool calls to a hosted MCP server.
11+
/// </summary>
12+
/// <remarks>
13+
/// Use <see cref="HostedMcpServerToolApprovalMode.AlwaysRequire"/> to get an instance of <see cref="HostedMcpServerToolAlwaysRequireApprovalMode"/>.
14+
/// </remarks>
15+
[Experimental("MEAI001")]
16+
[DebuggerDisplay(nameof(AlwaysRequire))]
17+
public sealed class HostedMcpServerToolAlwaysRequireApprovalMode : HostedMcpServerToolApprovalMode
18+
{
19+
/// <summary>Initializes a new instance of the <see cref="HostedMcpServerToolAlwaysRequireApprovalMode"/> class.</summary>
20+
/// <remarks>Use <see cref="HostedMcpServerToolApprovalMode.AlwaysRequire"/> to get an instance of <see cref="HostedMcpServerToolAlwaysRequireApprovalMode"/>.</remarks>
21+
public HostedMcpServerToolAlwaysRequireApprovalMode()
22+
{
23+
}
24+
25+
/// <inheritdoc/>
26+
public override bool Equals(object? obj) => obj is HostedMcpServerToolAlwaysRequireApprovalMode;
27+
28+
/// <inheritdoc/>
29+
public override int GetHashCode() => typeof(HostedMcpServerToolAlwaysRequireApprovalMode).GetHashCode();
30+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Generic;
5+
using System.Diagnostics.CodeAnalysis;
6+
using System.Text.Json.Serialization;
7+
8+
namespace Microsoft.Extensions.AI;
9+
10+
/// <summary>
11+
/// Describes how approval is required for tool calls to a hosted MCP server.
12+
/// </summary>
13+
/// <remarks>
14+
/// The predefined values <see cref="AlwaysRequire" />, and <see cref="NeverRequire"/> are provided to specify handling for all tools.
15+
/// To specify approval behavior for individual tool names, use <see cref="RequireSpecific(IList{string}, IList{string})"/>.
16+
/// </remarks>
17+
[Experimental("MEAI001")]
18+
[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]
19+
[JsonDerivedType(typeof(HostedMcpServerToolNeverRequireApprovalMode), typeDiscriminator: "never")]
20+
[JsonDerivedType(typeof(HostedMcpServerToolAlwaysRequireApprovalMode), typeDiscriminator: "always")]
21+
[JsonDerivedType(typeof(HostedMcpServerToolRequireSpecificApprovalMode), typeDiscriminator: "requireSpecific")]
22+
#pragma warning disable CA1052 // Static holder types should be Static or NotInheritable
23+
public class HostedMcpServerToolApprovalMode
24+
#pragma warning restore CA1052
25+
{
26+
/// <summary>
27+
/// Gets a predefined <see cref="HostedMcpServerToolApprovalMode"/> indicating that all tool calls to a hosted MCP server always require approval.
28+
/// </summary>
29+
public static HostedMcpServerToolAlwaysRequireApprovalMode AlwaysRequire { get; } = new();
30+
31+
/// <summary>
32+
/// Gets a predefined <see cref="HostedMcpServerToolApprovalMode"/> indicating that all tool calls to a hosted MCP server never require approval.
33+
/// </summary>
34+
public static HostedMcpServerToolNeverRequireApprovalMode NeverRequire { get; } = new();
35+
36+
private protected HostedMcpServerToolApprovalMode()
37+
{
38+
}
39+
40+
/// <summary>
41+
/// Instantiates a <see cref="HostedMcpServerToolApprovalMode"/> that specifies approval behavior for individual tool names.
42+
/// </summary>
43+
/// <param name="alwaysRequireApprovalToolNames">The list of tools names that always require approval.</param>
44+
/// <param name="neverRequireApprovalToolNames">The list of tools names that never require approval.</param>
45+
/// <returns>An instance of <see cref="HostedMcpServerToolRequireSpecificApprovalMode"/> for the specified tool names.</returns>
46+
public static HostedMcpServerToolRequireSpecificApprovalMode RequireSpecific(IList<string>? alwaysRequireApprovalToolNames, IList<string>? neverRequireApprovalToolNames)
47+
=> new(alwaysRequireApprovalToolNames, neverRequireApprovalToolNames);
48+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics;
5+
using System.Diagnostics.CodeAnalysis;
6+
7+
namespace Microsoft.Extensions.AI;
8+
9+
/// <summary>
10+
/// Indicates that approval is never required for tool calls to a hosted MCP server.
11+
/// </summary>
12+
/// <remarks>
13+
/// Use <see cref="HostedMcpServerToolApprovalMode.NeverRequire"/> to get an instance of <see cref="HostedMcpServerToolNeverRequireApprovalMode"/>.
14+
/// </remarks>
15+
[Experimental("MEAI001")]
16+
[DebuggerDisplay(nameof(NeverRequire))]
17+
public sealed class HostedMcpServerToolNeverRequireApprovalMode : HostedMcpServerToolApprovalMode
18+
{
19+
/// <summary>Initializes a new instance of the <see cref="HostedMcpServerToolNeverRequireApprovalMode"/> class.</summary>
20+
/// <remarks>Use <see cref="HostedMcpServerToolApprovalMode.NeverRequire"/> to get an instance of <see cref="HostedMcpServerToolNeverRequireApprovalMode"/>.</remarks>
21+
public HostedMcpServerToolNeverRequireApprovalMode()
22+
{
23+
}
24+
25+
/// <inheritdoc/>
26+
public override bool Equals(object? obj) => obj is HostedMcpServerToolNeverRequireApprovalMode;
27+
28+
/// <inheritdoc/>
29+
public override int GetHashCode() => typeof(HostedMcpServerToolNeverRequireApprovalMode).GetHashCode();
30+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Diagnostics.CodeAnalysis;
7+
using System.Linq;
8+
9+
namespace Microsoft.Extensions.AI;
10+
11+
/// <summary>
12+
/// Represents a mode where approval behavior is specified for individual tool names.
13+
/// </summary>
14+
[Experimental("MEAI001")]
15+
public sealed class HostedMcpServerToolRequireSpecificApprovalMode : HostedMcpServerToolApprovalMode
16+
{
17+
/// <summary>
18+
/// Initializes a new instance of the <see cref="HostedMcpServerToolRequireSpecificApprovalMode"/> class that specifies approval behavior for individual tool names.
19+
/// </summary>
20+
/// <param name="alwaysRequireApprovalToolNames">The list of tools names that always require approval.</param>
21+
/// <param name="neverRequireApprovalToolNames">The list of tools names that never require approval.</param>
22+
public HostedMcpServerToolRequireSpecificApprovalMode(IList<string>? alwaysRequireApprovalToolNames, IList<string>? neverRequireApprovalToolNames)
23+
{
24+
AlwaysRequireApprovalToolNames = alwaysRequireApprovalToolNames;
25+
NeverRequireApprovalToolNames = neverRequireApprovalToolNames;
26+
}
27+
28+
/// <summary>
29+
/// Gets or sets the list of tool names that always require approval.
30+
/// </summary>
31+
public IList<string>? AlwaysRequireApprovalToolNames { get; set; }
32+
33+
/// <summary>
34+
/// Gets or sets the list of tool names that never require approval.
35+
/// </summary>
36+
public IList<string>? NeverRequireApprovalToolNames { get; set; }
37+
38+
/// <inheritdoc/>
39+
public override bool Equals(object? obj) => obj is HostedMcpServerToolRequireSpecificApprovalMode other &&
40+
ListEquals(AlwaysRequireApprovalToolNames, other.AlwaysRequireApprovalToolNames) &&
41+
ListEquals(NeverRequireApprovalToolNames, other.NeverRequireApprovalToolNames);
42+
43+
/// <inheritdoc/>
44+
public override int GetHashCode() =>
45+
HashCode.Combine(GetListHashCode(AlwaysRequireApprovalToolNames), GetListHashCode(NeverRequireApprovalToolNames));
46+
47+
private static bool ListEquals(IList<string>? list1, IList<string>? list2) =>
48+
ReferenceEquals(list1, list2) ||
49+
(list1 is not null && list2 is not null && list1.SequenceEqual(list2));
50+
51+
private static int GetListHashCode(IList<string>? list)
52+
{
53+
if (list is null)
54+
{
55+
return 0;
56+
}
57+
58+
HashCode hc = default;
59+
foreach (string item in list)
60+
{
61+
hc.Add(item);
62+
}
63+
64+
return hc.ToHashCode();
65+
}
66+
}

0 commit comments

Comments
 (0)