Skip to content

Commit cb29987

Browse files
jozkeestephentoub
authored andcommitted
Add abstraction for remote MCP servers
1 parent c7edc1c commit cb29987

26 files changed

+925
-7
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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ namespace Microsoft.Extensions.AI;
1414
[JsonDerivedType(typeof(FunctionResultContent), typeDiscriminator: "functionResult")]
1515
[JsonDerivedType(typeof(HostedFileContent), typeDiscriminator: "hostedFile")]
1616
[JsonDerivedType(typeof(HostedVectorStoreContent), typeDiscriminator: "hostedVectorStore")]
17+
[JsonDerivedType(typeof(McpServerToolCallContent), typeDiscriminator: "mcpServerToolCall")]
18+
[JsonDerivedType(typeof(McpServerToolResultContent), typeDiscriminator: "mcpServerToolResult")]
1719
[JsonDerivedType(typeof(TextContent), typeDiscriminator: "text")]
1820
[JsonDerivedType(typeof(TextReasoningContent), typeDiscriminator: "reasoning")]
1921
[JsonDerivedType(typeof(UriContent), typeDiscriminator: "uri")]
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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 Microsoft.Shared.Diagnostics;
7+
8+
namespace Microsoft.Extensions.AI;
9+
10+
/// <summary>
11+
/// Represents a tool call request to a MCP server.
12+
/// </summary>
13+
public class McpServerToolCallContent : AIContent
14+
{
15+
/// <summary>
16+
/// Initializes a new instance of the <see cref="McpServerToolCallContent"/> class.
17+
/// </summary>
18+
/// <param name="callId">The tool call ID.</param>
19+
/// <param name="toolName">The tool name.</param>
20+
/// <param name="serverName">The MCP server name.</param>
21+
/// <exception cref="ArgumentNullException"><paramref name="callId"/>, <paramref name="toolName"/>, or <paramref name="serverName"/> are <see langword="null"/>.</exception>
22+
/// <exception cref="ArgumentException"><paramref name="callId"/>, <paramref name="toolName"/>, or <paramref name="serverName"/> are empty or composed entirely of whitespace.</exception>
23+
public McpServerToolCallContent(string callId, string toolName, string serverName)
24+
{
25+
CallId = Throw.IfNullOrWhitespace(callId);
26+
ToolName = Throw.IfNullOrWhitespace(toolName);
27+
ServerName = Throw.IfNullOrWhitespace(serverName);
28+
}
29+
30+
/// <summary>
31+
/// Gets the tool call ID.
32+
/// </summary>
33+
public string CallId { get; }
34+
35+
/// <summary>
36+
/// Gets the name of the tool called.
37+
/// </summary>
38+
public string ToolName { get; }
39+
40+
/// <summary>
41+
/// Gets the name of the MCP server.
42+
/// </summary>
43+
public string ServerName { get; }
44+
45+
/// <summary>
46+
/// Gets or sets the arguments used for the tool call.
47+
/// </summary>
48+
public IReadOnlyDictionary<string, object?>? Arguments { get; set; }
49+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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 Microsoft.Shared.Diagnostics;
7+
8+
namespace Microsoft.Extensions.AI;
9+
10+
/// <summary>
11+
/// Represents the result of a MCP server tool call.
12+
/// </summary>
13+
public class McpServerToolResultContent : AIContent
14+
{
15+
/// <summary>
16+
/// Initializes a new instance of the <see cref="McpServerToolResultContent"/> class.
17+
/// </summary>
18+
/// <param name="callId">The tool call ID.</param>
19+
/// <exception cref="ArgumentNullException"><paramref name="callId"/> is <see langword="null"/>.</exception>
20+
/// <exception cref="ArgumentException"><paramref name="callId"/> is empty or composed entirely of whitespace.</exception>
21+
public McpServerToolResultContent(string callId)
22+
{
23+
CallId = Throw.IfNullOrWhitespace(callId);
24+
}
25+
26+
/// <summary>
27+
/// Gets the tool call ID.
28+
/// </summary>
29+
public string CallId { get; }
30+
31+
/// <summary>
32+
/// Gets or sets the output of the tool call.
33+
/// </summary>
34+
public IList<AIContent>? Output { get; set; }
35+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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+
6+
namespace Microsoft.Extensions.AI;
7+
8+
/// <summary>
9+
/// Indicates that approval is always required for tool calls to a hosted MCP server.
10+
/// </summary>
11+
/// <remarks>
12+
/// Use <see cref="HostedMcpServerToolApprovalMode.AlwaysRequire"/> to get an instance of <see cref="HostedMcpServerToolAlwaysRequireApprovalMode"/>.
13+
/// </remarks>
14+
[DebuggerDisplay(nameof(AlwaysRequire))]
15+
public sealed class HostedMcpServerToolAlwaysRequireApprovalMode : HostedMcpServerToolApprovalMode
16+
{
17+
/// <summary>Initializes a new instance of the <see cref="HostedMcpServerToolAlwaysRequireApprovalMode"/> class.</summary>
18+
/// <remarks>Use <see cref="HostedMcpServerToolApprovalMode.AlwaysRequire"/> to get an instance of <see cref="HostedMcpServerToolAlwaysRequireApprovalMode"/>.</remarks>
19+
public HostedMcpServerToolAlwaysRequireApprovalMode()
20+
{
21+
}
22+
23+
/// <inheritdoc/>
24+
public override bool Equals(object? obj) => obj is HostedMcpServerToolAlwaysRequireApprovalMode;
25+
26+
/// <inheritdoc/>
27+
public override int GetHashCode() => typeof(HostedMcpServerToolAlwaysRequireApprovalMode).GetHashCode();
28+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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.Text.Json.Serialization;
6+
7+
namespace Microsoft.Extensions.AI;
8+
9+
/// <summary>
10+
/// Describes how approval is required for tool calls to a hosted MCP server.
11+
/// </summary>
12+
/// <remarks>
13+
/// The predefined values <see cref="AlwaysRequire" />, and <see cref="NeverRequire"/> are provided to specify handling for all tools.
14+
/// To specify approval behavior for individual tool names, use <see cref="RequireSpecific(IList{string}, IList{string})"/>.
15+
/// </remarks>
16+
[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]
17+
[JsonDerivedType(typeof(HostedMcpServerToolNeverRequireApprovalMode), typeDiscriminator: "never")]
18+
[JsonDerivedType(typeof(HostedMcpServerToolAlwaysRequireApprovalMode), typeDiscriminator: "always")]
19+
[JsonDerivedType(typeof(HostedMcpServerToolRequireSpecificApprovalMode), typeDiscriminator: "requireSpecific")]
20+
#pragma warning disable CA1052 // Static holder types should be Static or NotInheritable
21+
public class HostedMcpServerToolApprovalMode
22+
#pragma warning restore CA1052
23+
{
24+
/// <summary>
25+
/// Gets a predefined <see cref="HostedMcpServerToolApprovalMode"/> indicating that all tool calls to a hosted MCP server always require approval.
26+
/// </summary>
27+
public static HostedMcpServerToolAlwaysRequireApprovalMode AlwaysRequire { get; } = new();
28+
29+
/// <summary>
30+
/// Gets a predefined <see cref="HostedMcpServerToolApprovalMode"/> indicating that all tool calls to a hosted MCP server never require approval.
31+
/// </summary>
32+
public static HostedMcpServerToolNeverRequireApprovalMode NeverRequire { get; } = new();
33+
34+
private protected HostedMcpServerToolApprovalMode()
35+
{
36+
}
37+
38+
/// <summary>
39+
/// Instantiates a <see cref="HostedMcpServerToolApprovalMode"/> that specifies approval behavior for individual tool names.
40+
/// </summary>
41+
/// <param name="alwaysRequireApprovalToolNames">The list of tools names that always require approval.</param>
42+
/// <param name="neverRequireApprovalToolNames">The list of tools names that never require approval.</param>
43+
/// <returns>An instance of <see cref="HostedMcpServerToolRequireSpecificApprovalMode"/> for the specified tool names.</returns>
44+
public static HostedMcpServerToolRequireSpecificApprovalMode RequireSpecific(IList<string>? alwaysRequireApprovalToolNames, IList<string>? neverRequireApprovalToolNames)
45+
=> new(alwaysRequireApprovalToolNames, neverRequireApprovalToolNames);
46+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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+
6+
namespace Microsoft.Extensions.AI;
7+
8+
/// <summary>
9+
/// Indicates that approval is never required for tool calls to a hosted MCP server.
10+
/// </summary>
11+
/// <remarks>
12+
/// Use <see cref="HostedMcpServerToolApprovalMode.NeverRequire"/> to get an instance of <see cref="HostedMcpServerToolNeverRequireApprovalMode"/>.
13+
/// </remarks>
14+
[DebuggerDisplay(nameof(NeverRequire))]
15+
public sealed class HostedMcpServerToolNeverRequireApprovalMode : HostedMcpServerToolApprovalMode
16+
{
17+
/// <summary>Initializes a new instance of the <see cref="HostedMcpServerToolNeverRequireApprovalMode"/> class.</summary>
18+
/// <remarks>Use <see cref="HostedMcpServerToolApprovalMode.NeverRequire"/> to get an instance of <see cref="HostedMcpServerToolNeverRequireApprovalMode"/>.</remarks>
19+
public HostedMcpServerToolNeverRequireApprovalMode()
20+
{
21+
}
22+
23+
/// <inheritdoc/>
24+
public override bool Equals(object? obj) => obj is HostedMcpServerToolNeverRequireApprovalMode;
25+
26+
/// <inheritdoc/>
27+
public override int GetHashCode() => typeof(HostedMcpServerToolNeverRequireApprovalMode).GetHashCode();
28+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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.Linq;
7+
8+
namespace Microsoft.Extensions.AI;
9+
10+
/// <summary>
11+
/// Represents a mode where approval behavior is specified for individual tool names.
12+
/// </summary>
13+
public sealed class HostedMcpServerToolRequireSpecificApprovalMode : HostedMcpServerToolApprovalMode
14+
{
15+
/// <summary>
16+
/// Initializes a new instance of the <see cref="HostedMcpServerToolRequireSpecificApprovalMode"/> class that specifies approval behavior for individual tool names.
17+
/// </summary>
18+
/// <param name="alwaysRequireApprovalToolNames">The list of tools names that always require approval.</param>
19+
/// <param name="neverRequireApprovalToolNames">The list of tools names that never require approval.</param>
20+
public HostedMcpServerToolRequireSpecificApprovalMode(IList<string>? alwaysRequireApprovalToolNames, IList<string>? neverRequireApprovalToolNames)
21+
{
22+
AlwaysRequireApprovalToolNames = alwaysRequireApprovalToolNames;
23+
NeverRequireApprovalToolNames = neverRequireApprovalToolNames;
24+
}
25+
26+
/// <summary>
27+
/// Gets or sets the list of tool names that always require approval.
28+
/// </summary>
29+
public IList<string>? AlwaysRequireApprovalToolNames { get; set; }
30+
31+
/// <summary>
32+
/// Gets or sets the list of tool names that never require approval.
33+
/// </summary>
34+
public IList<string>? NeverRequireApprovalToolNames { get; set; }
35+
36+
/// <inheritdoc/>
37+
public override bool Equals(object? obj) => obj is HostedMcpServerToolRequireSpecificApprovalMode other &&
38+
ListEquals(AlwaysRequireApprovalToolNames, other.AlwaysRequireApprovalToolNames) &&
39+
ListEquals(NeverRequireApprovalToolNames, other.NeverRequireApprovalToolNames);
40+
41+
/// <inheritdoc/>
42+
public override int GetHashCode() =>
43+
HashCode.Combine(GetListHashCode(AlwaysRequireApprovalToolNames), GetListHashCode(NeverRequireApprovalToolNames));
44+
45+
private static bool ListEquals(IList<string>? list1, IList<string>? list2)
46+
{
47+
if (ReferenceEquals(list1, list2))
48+
{
49+
return true;
50+
}
51+
52+
if (list1 is null || list2 is null)
53+
{
54+
return false;
55+
}
56+
57+
return list1.SequenceEqual(list2);
58+
}
59+
60+
private static int GetListHashCode(IList<string>? list)
61+
{
62+
if (list is null)
63+
{
64+
return 0;
65+
}
66+
67+
var hc = default(HashCode);
68+
foreach (string item in list)
69+
{
70+
hc.Add(item);
71+
}
72+
73+
return hc.ToHashCode();
74+
}
75+
}

src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232

3333
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp'">
3434
<PackageReference Include="System.Text.Json" />
35+
<PackageReference Include="Microsoft.Bcl.HashCode" />
3536
</ItemGroup>
3637

3738
<ItemGroup Condition="'$(TargetFramework)' == 'net462'">

0 commit comments

Comments
 (0)