diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml
index 40da1294c8d..93851f5fee4 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml
@@ -169,4 +169,144 @@
lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll
true
+
+ CP0001
+ T:Microsoft.Extensions.AI.McpServerToolApprovalRequestContent
+ lib/net462/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net462/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0001
+ T:Microsoft.Extensions.AI.McpServerToolApprovalResponseContent
+ lib/net462/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net462/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0002
+ M:Microsoft.Extensions.AI.McpServerToolCallContent.get_Arguments
+ lib/net462/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net462/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0002
+ M:Microsoft.Extensions.AI.McpServerToolResultContent.get_Output
+ lib/net462/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net462/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0002
+ M:Microsoft.Extensions.AI.McpServerToolResultContent.set_Output(System.Collections.Generic.IList{Microsoft.Extensions.AI.AIContent})
+ lib/net462/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net462/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0001
+ T:Microsoft.Extensions.AI.McpServerToolApprovalRequestContent
+ lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0001
+ T:Microsoft.Extensions.AI.McpServerToolApprovalResponseContent
+ lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0002
+ M:Microsoft.Extensions.AI.McpServerToolCallContent.get_Arguments
+ lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0002
+ M:Microsoft.Extensions.AI.McpServerToolResultContent.get_Output
+ lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0002
+ M:Microsoft.Extensions.AI.McpServerToolResultContent.set_Output(System.Collections.Generic.IList{Microsoft.Extensions.AI.AIContent})
+ lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0001
+ T:Microsoft.Extensions.AI.McpServerToolApprovalRequestContent
+ lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0001
+ T:Microsoft.Extensions.AI.McpServerToolApprovalResponseContent
+ lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0002
+ M:Microsoft.Extensions.AI.McpServerToolCallContent.get_Arguments
+ lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0002
+ M:Microsoft.Extensions.AI.McpServerToolResultContent.get_Output
+ lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0002
+ M:Microsoft.Extensions.AI.McpServerToolResultContent.set_Output(System.Collections.Generic.IList{Microsoft.Extensions.AI.AIContent})
+ lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0001
+ T:Microsoft.Extensions.AI.McpServerToolApprovalRequestContent
+ lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0001
+ T:Microsoft.Extensions.AI.McpServerToolApprovalResponseContent
+ lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0002
+ M:Microsoft.Extensions.AI.McpServerToolCallContent.get_Arguments
+ lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0002
+ M:Microsoft.Extensions.AI.McpServerToolResultContent.get_Output
+ lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0002
+ M:Microsoft.Extensions.AI.McpServerToolResultContent.set_Output(System.Collections.Generic.IList{Microsoft.Extensions.AI.AIContent})
+ lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs
index af8b19c8d84..ffdf33f7645 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs
@@ -28,8 +28,6 @@ namespace Microsoft.Extensions.AI;
// [JsonDerivedType(typeof(FunctionApprovalResponseContent), typeDiscriminator: "functionApprovalResponse")]
// [JsonDerivedType(typeof(McpServerToolCallContent), typeDiscriminator: "mcpServerToolCall")]
// [JsonDerivedType(typeof(McpServerToolResultContent), typeDiscriminator: "mcpServerToolResult")]
-// [JsonDerivedType(typeof(McpServerToolApprovalRequestContent), typeDiscriminator: "mcpServerToolApprovalRequest")]
-// [JsonDerivedType(typeof(McpServerToolApprovalResponseContent), typeDiscriminator: "mcpServerToolApprovalResponse")]
// [JsonDerivedType(typeof(CodeInterpreterToolCallContent), typeDiscriminator: "codeInterpreterToolCall")]
// [JsonDerivedType(typeof(CodeInterpreterToolResultContent), typeDiscriminator: "codeInterpreterToolResult")]
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs
index 836d5a4110b..7c506a7845b 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs
@@ -14,7 +14,7 @@ namespace Microsoft.Extensions.AI;
/// Represents a function call request.
///
[DebuggerDisplay("{DebuggerDisplay,nq}")]
-public sealed class FunctionCallContent : AIContent
+public class FunctionCallContent : AIContent
{
///
/// Initializes a new instance of the class.
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionResultContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionResultContent.cs
index 46401347b40..d5eb4884709 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionResultContent.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionResultContent.cs
@@ -13,7 +13,7 @@ namespace Microsoft.Extensions.AI;
/// Represents the result of a function call.
///
[DebuggerDisplay("{DebuggerDisplay,nq}")]
-public sealed class FunctionResultContent : AIContent
+public class FunctionResultContent : AIContent
{
///
/// Initializes a new instance of the class.
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolApprovalRequestContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolApprovalRequestContent.cs
deleted file mode 100644
index 8f302d901b4..00000000000
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolApprovalRequestContent.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System;
-using System.Diagnostics.CodeAnalysis;
-using Microsoft.Shared.Diagnostics;
-
-namespace Microsoft.Extensions.AI;
-
-///
-/// Represents a request for user approval of an MCP server tool call.
-///
-[Experimental("MEAI001")]
-public sealed class McpServerToolApprovalRequestContent : UserInputRequestContent
-{
- ///
- /// Initializes a new instance of the class.
- ///
- /// The ID that uniquely identifies the MCP server tool approval request/response pair.
- /// The tool call that requires user approval.
- /// is .
- /// is empty or composed entirely of whitespace.
- /// is .
- public McpServerToolApprovalRequestContent(string id, McpServerToolCallContent toolCall)
- : base(id)
- {
- ToolCall = Throw.IfNull(toolCall);
- }
-
- ///
- /// Gets the tool call that pre-invoke approval is required for.
- ///
- public McpServerToolCallContent ToolCall { get; }
-
- ///
- /// Creates a to indicate whether the function call is approved or rejected based on the value of .
- ///
- /// if the function call is approved; otherwise, .
- /// The representing the approval response.
- public McpServerToolApprovalResponseContent CreateResponse(bool approved) => new(Id, approved);
-}
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolApprovalResponseContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolApprovalResponseContent.cs
deleted file mode 100644
index 0e239a79d7f..00000000000
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolApprovalResponseContent.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System;
-using System.Diagnostics.CodeAnalysis;
-
-namespace Microsoft.Extensions.AI;
-
-///
-/// Represents a response to an MCP server tool approval request.
-///
-[Experimental("MEAI001")]
-public sealed class McpServerToolApprovalResponseContent : UserInputResponseContent
-{
- ///
- /// Initializes a new instance of the class.
- ///
- /// The ID that uniquely identifies the MCP server tool approval request/response pair.
- /// if the MCP server tool call is approved; otherwise, .
- /// is .
- /// is empty or composed entirely of whitespace.
- public McpServerToolApprovalResponseContent(string id, bool approved)
- : base(id)
- {
- Approved = approved;
- }
-
- ///
- /// Gets a value indicating whether the user approved the request.
- ///
- public bool Approved { get; }
-}
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs
index 3283c09a7ee..0f4053ef8b9 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs
@@ -2,8 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
-using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
+using System.Text.Json.Serialization;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Extensions.AI;
@@ -16,7 +16,7 @@ namespace Microsoft.Extensions.AI;
/// It is informational only.
///
[Experimental("MEAI001")]
-public sealed class McpServerToolCallContent : AIContent
+public sealed class McpServerToolCallContent : FunctionCallContent
{
///
/// Initializes a new instance of the class.
@@ -26,30 +26,20 @@ public sealed class McpServerToolCallContent : AIContent
/// The MCP server name that hosts the tool.
/// or is .
/// or is empty or composed entirely of whitespace.
+ [JsonConstructor]
public McpServerToolCallContent(string callId, string toolName, string? serverName)
+ : base(Throw.IfNullOrWhitespace(callId), Throw.IfNullOrWhitespace(toolName))
{
- CallId = Throw.IfNullOrWhitespace(callId);
- ToolName = Throw.IfNullOrWhitespace(toolName);
ServerName = serverName;
}
- ///
- /// Gets the tool call ID.
- ///
- public string CallId { get; }
-
///
/// Gets the name of the tool called.
///
- public string ToolName { get; }
+ public string ToolName => Name;
///
/// Gets the name of the MCP server that hosts the tool.
///
public string? ServerName { get; }
-
- ///
- /// Gets or sets the arguments used for the tool call.
- ///
- public IReadOnlyDictionary? Arguments { get; set; }
}
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs
index b8329c74d99..673a0217687 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs
@@ -2,8 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
-using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
+using System.Text.Json.Serialization;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Extensions.AI;
@@ -16,7 +16,7 @@ namespace Microsoft.Extensions.AI;
/// It is informational only.
///
[Experimental("MEAI001")]
-public sealed class McpServerToolResultContent : AIContent
+public sealed class McpServerToolResultContent : FunctionResultContent
{
///
/// Initializes a new instance of the class.
@@ -24,18 +24,9 @@ public sealed class McpServerToolResultContent : AIContent
/// The tool call ID.
/// is .
/// is empty or composed entirely of whitespace.
+ [JsonConstructor]
public McpServerToolResultContent(string callId)
+ : base(Throw.IfNullOrWhitespace(callId), result: null)
{
- CallId = Throw.IfNullOrWhitespace(callId);
}
-
- ///
- /// Gets the tool call ID.
- ///
- public string CallId { get; }
-
- ///
- /// Gets or sets the output of the tool call.
- ///
- public IList? Output { get; set; }
}
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputRequestContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputRequestContent.cs
index b2a2e0e6e95..3e77607a3fd 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputRequestContent.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputRequestContent.cs
@@ -14,7 +14,6 @@ namespace Microsoft.Extensions.AI;
[Experimental("MEAI001")]
[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]
[JsonDerivedType(typeof(FunctionApprovalRequestContent), "functionApprovalRequest")]
-[JsonDerivedType(typeof(McpServerToolApprovalRequestContent), "mcpServerToolApprovalRequest")]
public class UserInputRequestContent : AIContent
{
///
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputResponseContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputResponseContent.cs
index 6902f047282..f17ae2a964d 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputResponseContent.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputResponseContent.cs
@@ -14,7 +14,6 @@ namespace Microsoft.Extensions.AI;
[Experimental("MEAI001")]
[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]
[JsonDerivedType(typeof(FunctionApprovalResponseContent), "functionApprovalResponse")]
-[JsonDerivedType(typeof(McpServerToolApprovalResponseContent), "mcpServerToolApprovalResponse")]
public class UserInputResponseContent : AIContent
{
///
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json
index e401502d82b..7c68076c583 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json
@@ -1780,7 +1780,7 @@
]
},
{
- "Type": "sealed class Microsoft.Extensions.AI.FunctionCallContent : Microsoft.Extensions.AI.AIContent",
+ "Type": "class Microsoft.Extensions.AI.FunctionCallContent : Microsoft.Extensions.AI.AIContent",
"Stage": "Stable",
"Methods": [
{
@@ -1812,7 +1812,7 @@
]
},
{
- "Type": "sealed class Microsoft.Extensions.AI.FunctionResultContent : Microsoft.Extensions.AI.AIContent",
+ "Type": "class Microsoft.Extensions.AI.FunctionResultContent : Microsoft.Extensions.AI.AIContent",
"Stage": "Stable",
"Methods": [
{
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs
index d01294836bc..51ed08441f1 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs
@@ -55,8 +55,6 @@ private static JsonSerializerOptions CreateDefaultOptions()
AddAIContentType(options, typeof(FunctionApprovalResponseContent), typeDiscriminatorId: "functionApprovalResponse", checkBuiltIn: false);
AddAIContentType(options, typeof(McpServerToolCallContent), typeDiscriminatorId: "mcpServerToolCall", checkBuiltIn: false);
AddAIContentType(options, typeof(McpServerToolResultContent), typeDiscriminatorId: "mcpServerToolResult", checkBuiltIn: false);
- AddAIContentType(options, typeof(McpServerToolApprovalRequestContent), typeDiscriminatorId: "mcpServerToolApprovalRequest", checkBuiltIn: false);
- AddAIContentType(options, typeof(McpServerToolApprovalResponseContent), typeDiscriminatorId: "mcpServerToolApprovalResponse", checkBuiltIn: false);
AddAIContentType(options, typeof(CodeInterpreterToolCallContent), typeDiscriminatorId: "codeInterpreterToolCall", checkBuiltIn: false);
AddAIContentType(options, typeof(CodeInterpreterToolResultContent), typeDiscriminatorId: "codeInterpreterToolResult", checkBuiltIn: false);
@@ -129,8 +127,6 @@ private static JsonSerializerOptions CreateDefaultOptions()
[JsonSerializable(typeof(FunctionApprovalResponseContent))]
[JsonSerializable(typeof(McpServerToolCallContent))]
[JsonSerializable(typeof(McpServerToolResultContent))]
- [JsonSerializable(typeof(McpServerToolApprovalRequestContent))]
- [JsonSerializable(typeof(McpServerToolApprovalResponseContent))]
[JsonSerializable(typeof(CodeInterpreterToolCallContent))]
[JsonSerializable(typeof(CodeInterpreterToolResultContent))]
[JsonSerializable(typeof(ResponseContinuationToken))]
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIJsonContext.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIJsonContext.cs
index 33d17e2963e..59d287edde2 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIJsonContext.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIJsonContext.cs
@@ -14,7 +14,6 @@ namespace Microsoft.Extensions.AI;
WriteIndented = true)]
[JsonSerializable(typeof(OpenAIClientExtensions.ToolJson))]
[JsonSerializable(typeof(IDictionary))]
-[JsonSerializable(typeof(IReadOnlyDictionary))]
[JsonSerializable(typeof(string[]))]
[JsonSerializable(typeof(IEnumerable))]
[JsonSerializable(typeof(JsonElement))]
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs
index 5fb6ac1935a..59f39c4b364 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs
@@ -207,9 +207,9 @@ internal static IEnumerable ToChatMessages(IEnumerable ToChatMessages(IEnumerable
break;
case McpToolCallApprovalRequestItem mtcari:
- yield return CreateUpdate(new McpServerToolApprovalRequestContent(mtcari.Id, new(mtcari.Id, mtcari.ToolName, mtcari.ServerLabel)
+ yield return CreateUpdate(new FunctionApprovalRequestContent(mtcari.Id, new McpServerToolCallContent(mtcari.Id, mtcari.ToolName, mtcari.ServerLabel)
{
- Arguments = JsonSerializer.Deserialize(mtcari.ToolArguments.ToMemory().Span, OpenAIJsonContext.Default.IReadOnlyDictionaryStringObject)!,
+ Arguments = JsonSerializer.Deserialize(mtcari.ToolArguments, OpenAIJsonContext.Default.IDictionaryStringObject),
RawRepresentation = mtcari,
})
{
@@ -819,7 +815,7 @@ internal static IEnumerable ToOpenAIResponseItems(IEnumerable? idToContentMapping = null;
+ Dictionary? idToContentMapping = null;
foreach (ChatMessage input in inputs)
{
@@ -852,7 +848,7 @@ internal static IEnumerable ToOpenAIResponseItems(IEnumerable rawRep,
- McpServerToolApprovalResponseContent mcpResp => ResponseItem.CreateMcpApprovalResponseItem(mcpResp.Id, mcpResp.Approved),
+ FunctionApprovalResponseContent { FunctionCall: McpServerToolCallContent mcpCall } farc => ResponseItem.CreateMcpApprovalResponseItem(mcpCall.CallId, farc.Approved),
_ => null
};
@@ -1049,8 +1045,8 @@ static FunctionCallOutputResponseItem SerializeAIContent(string callId, IEnumera
}
break;
- case McpServerToolApprovalResponseContent mcpApprovalResponseContent:
- yield return ResponseItem.CreateMcpApprovalResponseItem(mcpApprovalResponseContent.Id, mcpApprovalResponseContent.Approved);
+ case FunctionApprovalResponseContent { FunctionCall: McpServerToolCallContent mcpCall } farc:
+ yield return ResponseItem.CreateMcpApprovalResponseItem(mcpCall.CallId, farc.Approved);
break;
}
}
@@ -1079,6 +1075,20 @@ static FunctionCallOutputResponseItem SerializeAIContent(string callId, IEnumera
};
break;
+ case FunctionApprovalRequestContent { FunctionCall: McpServerToolCallContent mcpCall }:
+ yield return ResponseItem.CreateMcpApprovalRequestItem(
+ mcpCall.CallId,
+ mcpCall.ServerName,
+ mcpCall.ToolName,
+ BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(
+ mcpCall.Arguments!,
+ AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IDictionary)))));
+ break;
+
+ case McpServerToolCallContent mstcc:
+ (idToContentMapping ??= [])[mstcc.CallId] = mstcc;
+ break;
+
case FunctionCallContent callContent:
yield return ResponseItem.CreateFunctionCallItem(
callContent.CallId,
@@ -1088,34 +1098,23 @@ static FunctionCallOutputResponseItem SerializeAIContent(string callId, IEnumera
AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IDictionary)))));
break;
- case McpServerToolApprovalRequestContent mcpApprovalRequestContent:
- yield return ResponseItem.CreateMcpApprovalRequestItem(
- mcpApprovalRequestContent.Id,
- mcpApprovalRequestContent.ToolCall.ServerName,
- mcpApprovalRequestContent.ToolCall.ToolName,
- BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(mcpApprovalRequestContent.ToolCall.Arguments!, OpenAIJsonContext.Default.IReadOnlyDictionaryStringObject)));
- break;
-
- case McpServerToolCallContent mstcc:
- (idToContentMapping ??= [])[mstcc.CallId] = mstcc;
- break;
-
case McpServerToolResultContent mstrc:
- if (idToContentMapping?.TryGetValue(mstrc.CallId, out AIContent? callContentFromMapping) is true &&
- callContentFromMapping is McpServerToolCallContent associatedCall)
+ if (idToContentMapping?.TryGetValue(mstrc.CallId, out McpServerToolCallContent? associatedCall) is true)
{
_ = idToContentMapping.Remove(mstrc.CallId);
McpToolCallItem mtci = ResponseItem.CreateMcpToolCallItem(
associatedCall.ServerName,
associatedCall.ToolName,
- BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(associatedCall.Arguments!, OpenAIJsonContext.Default.IReadOnlyDictionaryStringObject)));
- if (mstrc.Output?.OfType().FirstOrDefault() is ErrorContent errorContent)
+ BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(
+ associatedCall.Arguments!,
+ AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IDictionary)))));
+ if (mstrc.Result is BinaryData errorData)
{
- mtci.Error = BinaryData.FromString(errorContent.Message);
+ mtci.Error = errorData;
}
- else
+ else if (mstrc.Result is string outputString)
{
- mtci.ToolOutput = string.Concat(mstrc.Output?.OfType() ?? []);
+ mtci.ToolOutput = outputString;
}
yield return mtci;
@@ -1300,7 +1299,7 @@ private static void AddMcpToolCallContent(McpToolCallItem mtci, IList
{
contents.Add(new McpServerToolCallContent(mtci.Id, mtci.ToolName, mtci.ServerLabel)
{
- Arguments = JsonSerializer.Deserialize(mtci.ToolArguments.ToMemory().Span, OpenAIJsonContext.Default.IReadOnlyDictionaryStringObject)!,
+ Arguments = JsonSerializer.Deserialize(mtci.ToolArguments, OpenAIJsonContext.Default.IDictionaryStringObject),
// We purposefully do not set the RawRepresentation on the McpServerToolCallContent, only on the McpServerToolResultContent, to avoid
// the same McpToolCallItem being included on two different AIContent instances. When these are roundtripped, we want only one
@@ -1310,9 +1309,7 @@ private static void AddMcpToolCallContent(McpToolCallItem mtci, IList
contents.Add(new McpServerToolResultContent(mtci.Id)
{
RawRepresentation = mtci,
- Output = [mtci.Error is not null ?
- new ErrorContent(mtci.Error.ToString()) :
- new TextContent(mtci.ToolOutput)],
+ Result = mtci.Error ?? (object)mtci.ToolOutput
});
}
diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs
index cc43c192f89..1241ab1cc24 100644
--- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs
+++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs
@@ -1245,7 +1245,7 @@ FunctionResultContent CreateFunctionResultContent(FunctionInvocationResult resul
///
/// 1. Remove all and from the .
/// 2. Recreate for any that haven't been executed yet.
- /// 3. Genreate failed for any rejected .
+ /// 3. Generate failed for any rejected .
/// 4. add all the new content items to and return them as the pre-invocation history.
///
private static (List? preDownstreamCallHistory, List? approvals) ProcessFunctionApprovalResponses(
@@ -1326,13 +1326,13 @@ private static (List? approvals, List { { "param1", 123 } })),
new McpServerToolCallContent("call123", "myTool", "myServer"),
new McpServerToolResultContent("call123"),
- new McpServerToolApprovalRequestContent("request123", new McpServerToolCallContent("call123", "myTool", "myServer")),
- new McpServerToolApprovalResponseContent("request123", approved: true)
+ new FunctionApprovalRequestContent("request123", new McpServerToolCallContent("call123", "myTool", "myServer")),
+ new FunctionApprovalResponseContent("request123", approved: true, new McpServerToolCallContent("call456", "myTool2", "myServer2"))
]);
var serialized = JsonSerializer.Serialize(message, AIJsonUtilities.DefaultOptions);
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolCallContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolCallContentTests.cs
index d5c5b43ed0a..9d0e0156f5d 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolCallContentTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolCallContentTests.cs
@@ -39,7 +39,7 @@ public void Constructor_PropsRoundtrip()
Assert.Same(props, c.AdditionalProperties);
Assert.Null(c.Arguments);
- IReadOnlyDictionary args = new Dictionary();
+ IDictionary args = new Dictionary();
c.Arguments = args;
Assert.Same(args, c.Arguments);
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolResultContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolResultContentTests.cs
index 8fa6cc8a381..94776705660 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolResultContentTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolResultContentTests.cs
@@ -17,7 +17,7 @@ public void Constructor_PropsDefault()
Assert.Equal("callId", c.CallId);
Assert.Null(c.RawRepresentation);
Assert.Null(c.AdditionalProperties);
- Assert.Null(c.Output);
+ Assert.Null(c.Result);
}
[Fact]
@@ -37,10 +37,10 @@ public void Constructor_PropsRoundtrip()
Assert.Equal("callId", c.CallId);
- Assert.Null(c.Output);
+ Assert.Null(c.Result);
IList output = [];
- c.Output = output;
- Assert.Same(output, c.Output);
+ c.Result = output;
+ Assert.Same(output, c.Result);
}
[Fact]
@@ -55,7 +55,7 @@ public void Serialization_Roundtrips()
{
var content = new McpServerToolResultContent("call123")
{
- Output = new List { new TextContent("result") }
+ Result = new List { new TextContent("result") }
};
var json = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions);
@@ -63,6 +63,6 @@ public void Serialization_Roundtrips()
Assert.NotNull(deserializedContent);
Assert.Equal(content.CallId, deserializedContent.CallId);
- Assert.NotNull(deserializedContent.Output);
+ Assert.NotNull(deserializedContent.Result);
}
}
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputRequestContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputRequestContentTests.cs
index fc4dac9cabb..9661972112f 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputRequestContentTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputRequestContentTests.cs
@@ -33,16 +33,19 @@ public void Constructor_Roundtrips(string id)
[Fact]
public void Serialization_DerivedTypes_Roundtrips()
{
- UserInputRequestContent content = new FunctionApprovalRequestContent("request123", new FunctionCallContent("call123", "functionName", new Dictionary { { "param1", 123 } }));
+ FunctionApprovalRequestContent content = new FunctionApprovalRequestContent(
+ "request123", new FunctionCallContent("call123", "functionName", new Dictionary { { "param1", 123 } }));
var serializedContent = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions);
- var deserializedContent = JsonSerializer.Deserialize(serializedContent, AIJsonUtilities.DefaultOptions);
+ var deserializedContent = JsonSerializer.Deserialize(serializedContent, AIJsonUtilities.DefaultOptions);
Assert.NotNull(deserializedContent);
Assert.Equal(content.GetType(), deserializedContent.GetType());
UserInputRequestContent[] contents =
[
new FunctionApprovalRequestContent("request123", new FunctionCallContent("call123", "functionName", new Dictionary { { "param1", 123 } })),
- new McpServerToolApprovalRequestContent("request123", new McpServerToolCallContent("call123", "myTool", "myServer")),
+
+ // Uncomment once McpServerToolCallContent is no longer experimental.
+ // new FunctionApprovalRequestContent("request123", new McpServerToolCallContent("call123", "myTool", "myServer")),
];
var serializedContents = JsonSerializer.Serialize(contents, TestJsonSerializerContext.Default.UserInputRequestContentArray);
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputResponseContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputResponseContentTests.cs
index 2442e57272d..39d6307fff5 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputResponseContentTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputResponseContentTests.cs
@@ -40,7 +40,9 @@ public void Serialization_DerivedTypes_Roundtrips()
UserInputResponseContent[] contents =
[
new FunctionApprovalResponseContent("request123", true, new FunctionCallContent("call123", "functionName")),
- new McpServerToolApprovalResponseContent("request123", true),
+
+ // Uncomment once McpServerToolCallContent is no longer experimental.
+ // new FunctionApprovalResponseContent("request123", true, new McpServerToolCallContent("call123", "myTool", "myServer")),
];
var serializedContents = JsonSerializer.Serialize(contents, TestJsonSerializerContext.Default.UserInputResponseContentArray);
diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs
index 1293d14aec3..337ede2d5e5 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs
@@ -1742,6 +1742,14 @@ public Task> SelectToolsForRequestAsync(
}
}
+ protected static void SkipIfRateLimitedResponse(ChatResponse response)
+ {
+ if (response.Messages.Any(m => m.Contents.OfType().Any(e => e.ErrorCode == "rate_limit_exceeded")))
+ {
+ throw new SkipTestException("Rate limit exceeded. Test cannot continue.");
+ }
+ }
+
[MemberNotNull(nameof(ChatClient))]
protected void SkipIfNotEnabled()
{
diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs
index 1421e780dca..78233b9111a 100644
--- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs
@@ -144,79 +144,86 @@ await client.GetStreamingResponseAsync(Prompt, chatOptions).ToChatResponseAsync(
Assert.NotNull(response);
Assert.NotEmpty(response.Messages.SelectMany(m => m.Contents).OfType());
Assert.NotEmpty(response.Messages.SelectMany(m => m.Contents).OfType());
- Assert.Empty(response.Messages.SelectMany(m => m.Contents).OfType());
+ Assert.Empty(response.Messages.SelectMany(m => m.Contents).OfType());
Assert.Contains("src/Libraries/Microsoft.Extensions.AI.Abstractions/README.md", response.Text);
}
}
- [ConditionalFact]
- public async Task RemoteMCP_CallTool_ApprovalRequired()
+ [ConditionalTheory]
+ [InlineData(false, false, false)]
+ [InlineData(false, false, true)]
+ [InlineData(true, true, false)]
+ [InlineData(true, true, true)]
+ public async Task RemoteMCP_CallTool_ApprovalRequired(bool streaming, bool requireSpecific, bool useConversationId)
{
SkipIfNotEnabled();
- await RunAsync(false, false, false);
- await RunAsync(true, true, false);
- await RunAsync(false, false, true);
- await RunAsync(true, true, true);
-
- async Task RunAsync(bool streaming, bool requireSpecific, bool useConversationId)
+ ChatOptions chatOptions = new()
{
- ChatOptions chatOptions = new()
- {
- Tools = [new HostedMcpServerTool("deepwiki", new Uri("https://mcp.deepwiki.com/mcp"))
- {
- ApprovalMode = requireSpecific ?
- HostedMcpServerToolApprovalMode.RequireSpecific(["read_wiki_structure", "ask_question"], null) :
- HostedMcpServerToolApprovalMode.AlwaysRequire,
- }
- ],
- };
+ Tools = [new HostedMcpServerTool("deepwiki", new Uri("https://mcp.deepwiki.com/mcp"))
+ {
+ ApprovalMode = requireSpecific ?
+ HostedMcpServerToolApprovalMode.RequireSpecific(["read_wiki_structure", "ask_question"], null) :
+ HostedMcpServerToolApprovalMode.AlwaysRequire,
+ }
+ ],
+ };
- using var client = CreateChatClient()!;
+ using var client = CreateChatClient()!;
- // Initial request
- List input = [new ChatMessage(ChatRole.User, "Tell me the path to the README.md file for Microsoft.Extensions.AI.Abstractions in the dotnet/extensions repository")];
- ChatResponse response = streaming ?
- await client.GetStreamingResponseAsync(input, chatOptions).ToChatResponseAsync() :
- await client.GetResponseAsync(input, chatOptions);
+ // Initial request
+ List input = [new ChatMessage(ChatRole.User, "Tell me the path to the README.md file for Microsoft.Extensions.AI.Abstractions in the dotnet/extensions repository")];
+ ChatResponse response = streaming ?
+ await client.GetStreamingResponseAsync(input, chatOptions).ToChatResponseAsync() :
+ await client.GetResponseAsync(input, chatOptions);
+ SkipIfRateLimitedResponse(response);
- // Handle approvals of up to two rounds of tool calls
- int approvalsCount = 0;
- for (int i = 0; i < 2; i++)
+ // Handle approvals of up to two rounds of tool calls
+ int approvalsCount = 0;
+ for (int i = 0; i < 2; i++)
+ {
+ if (useConversationId)
{
- if (useConversationId)
- {
- chatOptions.ConversationId = response.ConversationId;
- input.Clear();
- }
- else
- {
- input.AddRange(response.Messages);
- }
-
- var approvalResponse = new ChatMessage(ChatRole.Tool,
- response.Messages
- .SelectMany(m => m.Contents)
- .OfType()
- .Select(c => new McpServerToolApprovalResponseContent(c.ToolCall.CallId, true))
- .ToArray());
- if (approvalResponse.Contents.Count == 0)
- {
- break;
- }
+ chatOptions.ConversationId = response.ConversationId;
+ input.Clear();
+ }
+ else
+ {
+ input.AddRange(response.Messages);
+ }
- approvalsCount += approvalResponse.Contents.Count;
- input.Add(approvalResponse);
- response = streaming ?
- await client.GetStreamingResponseAsync(input, chatOptions).ToChatResponseAsync() :
- await client.GetResponseAsync(input, chatOptions);
+ var approvalResponse = new ChatMessage(ChatRole.Tool,
+ response.Messages
+ .SelectMany(m => m.Contents)
+ .OfType()
+ .Select(c =>
+ {
+ var mcpCallContent = Assert.IsType(c.FunctionCall);
+ return new FunctionApprovalResponseContent(mcpCallContent.CallId, true, mcpCallContent);
+ })
+ .ToArray());
+ if (approvalResponse.Contents.Count == 0)
+ {
+ break;
}
- // Validate final response
- Assert.Equal(2, approvalsCount);
- Assert.Contains("src/Libraries/Microsoft.Extensions.AI.Abstractions/README.md", response.Text);
+ approvalsCount += approvalResponse.Contents.Count;
+ input.Add(approvalResponse);
+ response = streaming ?
+ await client.GetStreamingResponseAsync(input, chatOptions).ToChatResponseAsync() :
+ await client.GetResponseAsync(input, chatOptions);
+ SkipIfRateLimitedResponse(response);
}
+
+ if (approvalsCount == 0)
+ {
+ throw new InvalidOperationException("No approvals were requested; the test setup may be incorrect.");
+ }
+
+ // Validate final response
+ Assert.Equal(2, approvalsCount);
+ Assert.Contains("src/Libraries/Microsoft.Extensions.AI.Abstractions/README.md", response.Text);
}
[ConditionalFact]
@@ -407,8 +414,9 @@ await client.GetStreamingResponseAsync(input, chatOptions).ToChatResponseAsync()
if (approval)
{
input.AddRange(response.Messages);
- var approvalRequest = Assert.Single(response.Messages.SelectMany(m => m.Contents).OfType());
- Assert.Equal("search_events", approvalRequest.ToolCall.ToolName);
+ var approvalRequest = Assert.Single(response.Messages.SelectMany(m => m.Contents).OfType());
+ var mcpCallContent = Assert.IsType(approvalRequest.FunctionCall);
+ Assert.Equal("search_events", mcpCallContent.ToolName);
input.Add(new ChatMessage(ChatRole.Tool, [approvalRequest.CreateResponse(true)]));
response = streaming ?
@@ -421,8 +429,7 @@ await client.GetStreamingResponseAsync(input, chatOptions).ToChatResponseAsync()
Assert.Equal("search_events", toolCall.ToolName);
var toolResult = Assert.Single(response.Messages.SelectMany(m => m.Contents).OfType());
- var content = Assert.IsType(Assert.Single(toolResult.Output!));
- Assert.Equal(@"{""events"": [], ""next_page_token"": null}", content.Text);
+ Assert.Equal(@"{""events"": [], ""next_page_token"": null}", toolResult.Result);
}
}
diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs
index d1a6749450b..58f4338d7b1 100644
--- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs
@@ -1286,7 +1286,7 @@ public async Task McpToolCall_ApprovalRequired_NonStreaming(string role)
{
Tools = [new HostedMcpServerTool("deepwiki", new Uri("https://mcp.deepwiki.com/mcp"))]
};
- McpServerToolApprovalRequestContent approvalRequest;
+ FunctionApprovalRequestContent approvalRequest;
using (VerbatimHttpHandler handler = new(input, output))
using (HttpClient httpClient = new(handler))
@@ -1296,7 +1296,7 @@ public async Task McpToolCall_ApprovalRequired_NonStreaming(string role)
"Tell me the path to the README.md file for Microsoft.Extensions.AI.Abstractions in the dotnet/extensions repository",
chatOptions);
- approvalRequest = Assert.Single(response.Messages.SelectMany(m => m.Contents).OfType());
+ approvalRequest = Assert.Single(response.Messages.SelectMany(m => m.Contents).OfType());
chatOptions.ConversationId = response.ConversationId;
}
@@ -1441,8 +1441,7 @@ public async Task McpToolCall_ApprovalRequired_NonStreaming(string role)
var result = Assert.IsType(message.Contents[1]);
Assert.Equal("mcp_06ee3b1962eeb8470068e6b21cbaa081a3b5aa2a6c989f4c6f", result.CallId);
- Assert.NotNull(result.Output);
- Assert.StartsWith("The `README.md` file for `Microsoft.Extensions.AI.Abstractions` is located at", Assert.IsType(Assert.Single(result.Output)).Text);
+ Assert.StartsWith("The `README.md` file for `Microsoft.Extensions.AI.Abstractions` is located at", Assert.IsType(result.Result));
Assert.NotNull(response.Usage);
Assert.Equal(542, response.Usage.InputTokenCount);
@@ -1695,8 +1694,7 @@ public async Task McpToolCall_ApprovalNotRequired_NonStreaming(bool rawTool)
var firstResult = Assert.IsType(message.Contents[2]);
Assert.Equal("mcp_68be4166acfc8191bc5e0a751eed358b0384f747588fc3f5", firstResult.CallId);
- Assert.NotNull(firstResult.Output);
- Assert.StartsWith("Available pages for dotnet/extensions", Assert.IsType(Assert.Single(firstResult.Output)).Text);
+ Assert.StartsWith("Available pages for dotnet/extensions", Assert.IsType(firstResult.Result));
var secondCall = Assert.IsType(message.Contents[3]);
Assert.Equal("mcp_68be416900f88191837ae0718339a4ce0384f747588fc3f5", secondCall.CallId);
@@ -1708,8 +1706,7 @@ public async Task McpToolCall_ApprovalNotRequired_NonStreaming(bool rawTool)
var secondResult = Assert.IsType(message.Contents[4]);
Assert.Equal("mcp_68be416900f88191837ae0718339a4ce0384f747588fc3f5", secondResult.CallId);
- Assert.NotNull(secondResult.Output);
- Assert.StartsWith("The `README.md` file for `Microsoft.Extensions.AI.Abstractions` is located at", Assert.IsType(Assert.Single(secondResult.Output)).Text);
+ Assert.StartsWith("The `README.md` file for `Microsoft.Extensions.AI.Abstractions` is located at", Assert.IsType(secondResult.Result));
Assert.NotNull(response.Usage);
Assert.Equal(1329, response.Usage.InputTokenCount);
@@ -1717,6 +1714,208 @@ public async Task McpToolCall_ApprovalNotRequired_NonStreaming(bool rawTool)
Assert.Equal(1452, response.Usage.TotalTokenCount);
}
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task McpToolCall_ErrorResponse_NonStreaming(bool rawTool)
+ {
+ const string Input = """
+ {
+ "model": "gpt-4o-mini",
+ "tools": [
+ {
+ "type": "mcp",
+ "server_label": "mymcp",
+ "server_url": "https://mcp.example.com/mcp",
+ "require_approval": "never"
+ }
+ ],
+ "input": [
+ {
+ "type": "message",
+ "role": "user",
+ "content": [
+ {
+ "type": "input_text",
+ "text": "Test error handling"
+ }
+ ]
+ }
+ ]
+ }
+ """;
+
+ const string Output = """
+ {
+ "id": "resp_689023b0fa88819f99f48aff343d5ad50475557f6fefb5f0",
+ "object": "response",
+ "created_at": 1757299100,
+ "status": "completed",
+ "background": false,
+ "error": null,
+ "incomplete_details": null,
+ "instructions": null,
+ "max_output_tokens": null,
+ "max_tool_calls": null,
+ "model": "gpt-4o-mini-2024-07-18",
+ "output": [
+ {
+ "id": "mcpl_689023b0fa88819f99f48aff343d5ad50475557f6fefb5f0",
+ "type": "mcp_list_tools",
+ "server_label": "mymcp",
+ "tools": [
+ {
+ "annotations": {
+ "read_only": false
+ },
+ "description": "A tool that always errors",
+ "input_schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": false,
+ "$schema": "http://json-schema.org/draft-07/schema#"
+ },
+ "name": "test_error"
+ }
+ ]
+ },
+ {
+ "id": "mcp_689023b0fa88819f99f48aff343d5ad50475557f6fefb5f0",
+ "type": "mcp_call",
+ "approval_request_id": null,
+ "arguments": "{}",
+ "error": {
+ "type": "mcp_tool_execution_error",
+ "content": [
+ {
+ "type": "text",
+ "text": "An error occurred invoking 'test_error'.",
+ "annotations": null,
+ "meta": null
+ }
+ ]
+ },
+ "name": "test_error",
+ "output": null,
+ "server_label": "mymcp"
+ },
+ {
+ "id": "msg_689023b0fa88819f99f48aff343d5ad50475557f6fefb5f0",
+ "type": "message",
+ "status": "completed",
+ "content": [
+ {
+ "type": "output_text",
+ "annotations": [],
+ "logprobs": [],
+ "text": "The tool encountered an error during execution."
+ }
+ ],
+ "role": "assistant"
+ }
+ ],
+ "parallel_tool_calls": true,
+ "previous_response_id": null,
+ "prompt_cache_key": null,
+ "reasoning": {
+ "effort": null,
+ "summary": null
+ },
+ "safety_identifier": null,
+ "service_tier": "default",
+ "store": true,
+ "temperature": 1,
+ "text": {
+ "format": {
+ "type": "text"
+ },
+ "verbosity": "medium"
+ },
+ "tool_choice": "auto",
+ "tools": [
+ {
+ "type": "mcp",
+ "allowed_tools": null,
+ "headers": null,
+ "require_approval": "never",
+ "server_description": null,
+ "server_label": "mymcp",
+ "server_url": "https://mcp.example.com/"
+ }
+ ],
+ "top_logprobs": 0,
+ "top_p": 1,
+ "truncation": "disabled",
+ "usage": {
+ "input_tokens": 500,
+ "input_tokens_details": {
+ "cached_tokens": 0
+ },
+ "output_tokens": 50,
+ "output_tokens_details": {
+ "reasoning_tokens": 0
+ },
+ "total_tokens": 550
+ },
+ "user": null,
+ "metadata": {}
+ }
+ """;
+
+ using VerbatimHttpHandler handler = new(Input, Output);
+ using HttpClient httpClient = new(handler);
+ using IChatClient client = CreateResponseClient(httpClient, "gpt-4o-mini");
+
+ AITool mcpTool = rawTool ?
+ ResponseTool.CreateMcpTool("mymcp", serverUri: new("https://mcp.example.com/mcp"), toolCallApprovalPolicy: new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.NeverRequireApproval)).AsAITool() :
+ new HostedMcpServerTool("mymcp", new Uri("https://mcp.example.com/mcp"))
+ {
+ ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire,
+ };
+
+ ChatOptions chatOptions = new()
+ {
+ Tools = [mcpTool],
+ };
+
+ var response = await client.GetResponseAsync("Test error handling", chatOptions);
+ Assert.NotNull(response);
+
+ Assert.Equal("resp_689023b0fa88819f99f48aff343d5ad50475557f6fefb5f0", response.ResponseId);
+ Assert.Equal("resp_689023b0fa88819f99f48aff343d5ad50475557f6fefb5f0", response.ConversationId);
+ Assert.Equal("gpt-4o-mini-2024-07-18", response.ModelId);
+ Assert.Equal(DateTimeOffset.FromUnixTimeSeconds(1_757_299_100), response.CreatedAt);
+ Assert.Null(response.FinishReason);
+
+ var message = Assert.Single(response.Messages);
+ Assert.Equal(ChatRole.Assistant, response.Messages[0].Role);
+ Assert.Equal("The tool encountered an error during execution.", response.Messages[0].Text);
+
+ Assert.Equal(4, message.Contents.Count);
+
+ var toolCall = Assert.IsType(message.Contents[1]);
+ Assert.Equal("mcp_689023b0fa88819f99f48aff343d5ad50475557f6fefb5f0", toolCall.CallId);
+ Assert.Equal("mymcp", toolCall.ServerName);
+ Assert.Equal("test_error", toolCall.ToolName);
+ Assert.NotNull(toolCall.Arguments);
+ Assert.Empty(toolCall.Arguments);
+
+ var toolResult = Assert.IsType(message.Contents[2]);
+ Assert.Equal("mcp_689023b0fa88819f99f48aff343d5ad50475557f6fefb5f0", toolResult.CallId);
+ var errorData = Assert.IsType(toolResult.Result);
+ var errorJson = JsonDocument.Parse(errorData);
+ Assert.Equal("mcp_tool_execution_error", errorJson.RootElement.GetProperty("type").GetString());
+ var contentArray = errorJson.RootElement.GetProperty("content");
+ Assert.Equal(1, contentArray.GetArrayLength());
+ Assert.Equal("text", contentArray[0].GetProperty("type").GetString());
+ Assert.Equal("An error occurred invoking 'test_error'.", contentArray[0].GetProperty("text").GetString());
+
+ Assert.NotNull(response.Usage);
+ Assert.Equal(500, response.Usage.InputTokenCount);
+ Assert.Equal(50, response.Usage.OutputTokenCount);
+ Assert.Equal(550, response.Usage.TotalTokenCount);
+ }
+
[Fact]
public async Task McpToolCall_ApprovalNotRequired_Streaming()
{
@@ -2108,8 +2307,7 @@ public async Task McpToolCall_ApprovalNotRequired_Streaming()
var firstResult = Assert.IsType(message.Contents[2]);
Assert.Equal("mcp_68be4503d45c819e89cb574361c8eba003a2537be0e84a54", firstResult.CallId);
- Assert.NotNull(firstResult.Output);
- Assert.StartsWith("Available pages for dotnet/extensions", Assert.IsType(Assert.Single(firstResult.Output)).Text);
+ Assert.StartsWith("Available pages for dotnet/extensions", Assert.IsType(firstResult.Result));
var secondCall = Assert.IsType(message.Contents[3]);
Assert.Equal("mcp_68be4505f134819e806c002f27cce0c303a2537be0e84a54", secondCall.CallId);
@@ -2121,8 +2319,7 @@ public async Task McpToolCall_ApprovalNotRequired_Streaming()
var secondResult = Assert.IsType(message.Contents[4]);
Assert.Equal("mcp_68be4505f134819e806c002f27cce0c303a2537be0e84a54", secondResult.CallId);
- Assert.NotNull(secondResult.Output);
- Assert.StartsWith("The path to the `README.md` file", Assert.IsType(Assert.Single(secondResult.Output)).Text);
+ Assert.StartsWith("The path to the `README.md` file", Assert.IsType(secondResult.Result));
Assert.NotNull(response.Usage);
Assert.Equal(1420, response.Usage.InputTokenCount);
diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs
index 01f2e111447..abd2a5aac43 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs
@@ -9,7 +9,7 @@
using System.Threading.Tasks;
using Xunit;
-namespace Microsoft.Extensions.AI.ChatCompletion;
+namespace Microsoft.Extensions.AI;
public class FunctionInvokingChatClientApprovalsTests
{
@@ -1113,6 +1113,219 @@ async IAsyncEnumerable YieldInnerClientUpdates(
}
}
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task FunctionCallReplacedWithApproval_MixedWithMcpApprovalAsync(bool useAdditionalTools)
+ {
+ AITool[] tools =
+ [
+ new ApprovalRequiredAIFunction(AIFunctionFactory.Create(() => "Result 1", "Func")),
+ new HostedMcpServerTool("myServer", "https://localhost/mcp")
+ ];
+
+ var options = new ChatOptions
+ {
+ Tools = useAdditionalTools ? null : tools
+ };
+
+ List input =
+ [
+ new ChatMessage(ChatRole.User, "hello"),
+ ];
+
+ List downstreamClientOutput =
+ [
+ new ChatMessage(ChatRole.Assistant,
+ [
+ new FunctionCallContent("callId1", "Func"),
+ new FunctionApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer"))
+ ])
+ ];
+
+ List expectedOutput =
+ [
+ new ChatMessage(ChatRole.Assistant,
+ [
+ new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func")),
+ new FunctionApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer"))
+ ])
+ ];
+
+ await InvokeAndAssertAsync(options, input, downstreamClientOutput, expectedOutput, additionalTools: useAdditionalTools ? tools : null);
+ await InvokeAndAssertStreamingAsync(options, input, downstreamClientOutput, expectedOutput, additionalTools: useAdditionalTools ? tools : null);
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task ApprovedApprovalResponseIsExecuted_MixedWithMcpApprovalAsync(bool useAdditionalTools)
+ {
+ AITool[] tools =
+ [
+ new ApprovalRequiredAIFunction(AIFunctionFactory.Create(() => "Result 1", "Func")),
+ new HostedMcpServerTool("myServer", "https://localhost/mcp")
+ ];
+
+ var options = new ChatOptions
+ {
+ Tools = useAdditionalTools ? null : tools
+ };
+
+ List input =
+ [
+ new ChatMessage(ChatRole.User, "hello"),
+ new ChatMessage(ChatRole.Assistant,
+ [
+ new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func")),
+ new FunctionApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer"))
+ ]) { MessageId = "resp1" },
+ new ChatMessage(ChatRole.User,
+ [
+ new FunctionApprovalResponseContent("callId1", true, new FunctionCallContent("callId1", "Func")),
+ new FunctionApprovalResponseContent("callId2", true, new McpServerToolCallContent("callId2", "McpCall", "myServer"))
+ ]),
+ ];
+
+ List expectedDownstreamClientInput =
+ [
+ new ChatMessage(ChatRole.User, "hello"),
+ new ChatMessage(ChatRole.Assistant,
+ [
+ new FunctionApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer"))
+ ]),
+ new ChatMessage(ChatRole.User,
+ [
+ new FunctionApprovalResponseContent("callId2", true, new McpServerToolCallContent("callId2", "McpCall", "myServer"))
+ ]),
+ new ChatMessage(ChatRole.Assistant,
+ [
+ new FunctionCallContent("callId1", "Func")
+ ]),
+ new ChatMessage(ChatRole.Tool,
+ [
+ new FunctionResultContent("callId1", result: "Result 1")
+ ]),
+ ];
+
+ List downstreamClientOutput =
+ [
+ new ChatMessage(ChatRole.Assistant, [
+ new McpServerToolResultContent("callId2") { Result = new List { new TextContent("Result 2") } },
+ new TextContent("world")
+ ])
+ ];
+
+ List output =
+ [
+ new ChatMessage(ChatRole.Assistant,
+ [
+ new FunctionCallContent("callId1", "Func")
+ ]),
+ new ChatMessage(ChatRole.Tool,
+ [
+ new FunctionResultContent("callId1", result: "Result 1")
+ ]),
+ new ChatMessage(ChatRole.Assistant, [
+ new McpServerToolResultContent("callId2") { Result = new List { new TextContent("Result 2") } },
+ new TextContent("world")
+ ])
+ ];
+
+ await InvokeAndAssertAsync(options, input, downstreamClientOutput, output, expectedDownstreamClientInput, additionalTools: useAdditionalTools ? tools : null);
+ await InvokeAndAssertStreamingAsync(options, input, downstreamClientOutput, output, expectedDownstreamClientInput, additionalTools: useAdditionalTools ? tools : null);
+ }
+
+ [Theory]
+ [InlineData(false, true, false)]
+ [InlineData(false, false, true)]
+ [InlineData(true, true, false)]
+ [InlineData(true, false, true)]
+ public async Task RejectedApprovalResponses_MixedWithMcpApprovalAsync(bool useAdditionalTools, bool approveFuncCall, bool approveMcpCall)
+ {
+ Assert.NotEqual(approveFuncCall, approveMcpCall);
+
+ AITool[] tools =
+ [
+ new ApprovalRequiredAIFunction(AIFunctionFactory.Create(() => "Result 1", "Func")),
+ new HostedMcpServerTool("myServer", "https://localhost/mcp")
+ ];
+
+ var options = new ChatOptions
+ {
+ Tools = useAdditionalTools ? null : tools
+ };
+
+ List input =
+ [
+ new ChatMessage(ChatRole.User, "hello"),
+ new ChatMessage(ChatRole.Assistant,
+ [
+ new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func")),
+ new FunctionApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer"))
+ ]) { MessageId = "resp1" },
+ new ChatMessage(ChatRole.User,
+ [
+ new FunctionApprovalResponseContent("callId1", approveFuncCall, new FunctionCallContent("callId1", "Func")),
+ new FunctionApprovalResponseContent("callId2", approveMcpCall, new McpServerToolCallContent("callId2", "McpCall", "myServer"))
+ ]),
+ ];
+
+ List expectedDownstreamClientInput = [
+ new ChatMessage(ChatRole.User, "hello"),
+ new ChatMessage(ChatRole.Assistant,
+ [
+ new FunctionApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer"))
+ ]),
+ new ChatMessage(ChatRole.User,
+ [
+ new FunctionApprovalResponseContent("callId2", approveMcpCall, new McpServerToolCallContent("callId2", "McpCall", "myServer"))
+ ]),
+ new ChatMessage(ChatRole.Assistant,
+ [
+ new FunctionCallContent("callId1", "Func")
+ ]),
+ new ChatMessage(ChatRole.Tool,
+ [
+ approveFuncCall ?
+ new FunctionResultContent("callId1", result: "Result 1") :
+ new FunctionResultContent("callId1", result: "Tool call invocation rejected.")
+ ]),
+ ];
+
+ List downstreamClientOutput =
+ [
+ new ChatMessage(ChatRole.Assistant, [
+ new TextContent("world"),
+ .. approveMcpCall ?
+ [new McpServerToolResultContent("callId2") { Result = new List { new TextContent("Result 2") } }] :
+ Array.Empty()
+ ])
+ ];
+
+ List output = [
+ new ChatMessage(ChatRole.Assistant,
+ [
+ new FunctionCallContent("callId1", "Func"),
+ ]),
+ new ChatMessage(ChatRole.Tool,
+ [
+ approveFuncCall ?
+ new FunctionResultContent("callId1", result: "Result 1") :
+ new FunctionResultContent("callId1", result: "Tool call invocation rejected.")
+ ]),
+ new ChatMessage(ChatRole.Assistant, [
+ new TextContent("world"),
+ .. approveMcpCall ?
+ [new McpServerToolResultContent("callId2") { Result = new List { new TextContent("Result 2") } }] :
+ Array.Empty()
+ ])
+ ];
+
+ await InvokeAndAssertAsync(options, input, downstreamClientOutput, output, expectedDownstreamClientInput, additionalTools: useAdditionalTools ? tools : null);
+ await InvokeAndAssertStreamingAsync(options, input, downstreamClientOutput, output, expectedDownstreamClientInput, additionalTools: useAdditionalTools ? tools : null);
+ }
+
private static Task> InvokeAndAssertAsync(
ChatOptions? options,
List input,