diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml
new file mode 100644
index 00000000000..d266b9d2d4e
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml
@@ -0,0 +1,429 @@
+
+
+
+
+
+ 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
+
+
+ CP0001
+ T:Microsoft.Extensions.AI.UserInputRequestContent
+ lib/net462/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net462/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0001
+ T:Microsoft.Extensions.AI.UserInputResponseContent
+ lib/net462/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net462/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0002
+ M:Microsoft.Extensions.AI.HostedMcpServerTool.get_AuthorizationToken
+ lib/net462/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net462/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0002
+ M:Microsoft.Extensions.AI.HostedMcpServerTool.set_AuthorizationToken(System.String)
+ 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.McpServerToolCallContent.get_ToolName
+ 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
+
+
+ CP0007
+ T:Microsoft.Extensions.AI.FunctionApprovalRequestContent
+ lib/net462/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net462/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0007
+ T:Microsoft.Extensions.AI.FunctionApprovalResponseContent
+ 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
+
+
+ CP0001
+ T:Microsoft.Extensions.AI.UserInputRequestContent
+ lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0001
+ T:Microsoft.Extensions.AI.UserInputResponseContent
+ lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0002
+ M:Microsoft.Extensions.AI.HostedMcpServerTool.get_AuthorizationToken
+ lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0002
+ M:Microsoft.Extensions.AI.HostedMcpServerTool.set_AuthorizationToken(System.String)
+ 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.McpServerToolCallContent.get_ToolName
+ 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
+
+
+ CP0007
+ T:Microsoft.Extensions.AI.FunctionApprovalRequestContent
+ lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0007
+ T:Microsoft.Extensions.AI.FunctionApprovalResponseContent
+ 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
+
+
+ CP0001
+ T:Microsoft.Extensions.AI.UserInputRequestContent
+ lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0001
+ T:Microsoft.Extensions.AI.UserInputResponseContent
+ lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0002
+ M:Microsoft.Extensions.AI.HostedMcpServerTool.get_AuthorizationToken
+ lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0002
+ M:Microsoft.Extensions.AI.HostedMcpServerTool.set_AuthorizationToken(System.String)
+ 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.McpServerToolCallContent.get_ToolName
+ 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
+
+
+ CP0007
+ T:Microsoft.Extensions.AI.FunctionApprovalRequestContent
+ lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0007
+ T:Microsoft.Extensions.AI.FunctionApprovalResponseContent
+ 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
+
+
+ CP0001
+ T:Microsoft.Extensions.AI.UserInputRequestContent
+ lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0001
+ T:Microsoft.Extensions.AI.UserInputResponseContent
+ lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0002
+ M:Microsoft.Extensions.AI.HostedMcpServerTool.get_AuthorizationToken
+ lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0002
+ M:Microsoft.Extensions.AI.HostedMcpServerTool.set_AuthorizationToken(System.String)
+ 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.McpServerToolCallContent.get_ToolName
+ 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
+
+
+ CP0007
+ T:Microsoft.Extensions.AI.FunctionApprovalRequestContent
+ lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0007
+ T:Microsoft.Extensions.AI.FunctionApprovalResponseContent
+ lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+
+ CP0001
+ T:Microsoft.Extensions.AI.McpServerToolApprovalRequestContent
+ lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0001
+ T:Microsoft.Extensions.AI.McpServerToolApprovalResponseContent
+ lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0001
+ T:Microsoft.Extensions.AI.UserInputRequestContent
+ lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0001
+ T:Microsoft.Extensions.AI.UserInputResponseContent
+ lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0002
+ M:Microsoft.Extensions.AI.HostedMcpServerTool.get_AuthorizationToken
+ lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0002
+ M:Microsoft.Extensions.AI.HostedMcpServerTool.set_AuthorizationToken(System.String)
+ lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0002
+ M:Microsoft.Extensions.AI.McpServerToolCallContent.get_Arguments
+ lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0002
+ M:Microsoft.Extensions.AI.McpServerToolCallContent.get_ToolName
+ lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0002
+ M:Microsoft.Extensions.AI.McpServerToolResultContent.get_Output
+ lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0002
+ M:Microsoft.Extensions.AI.McpServerToolResultContent.set_Output(System.Collections.Generic.IList{Microsoft.Extensions.AI.AIContent})
+ lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0007
+ T:Microsoft.Extensions.AI.FunctionApprovalRequestContent
+ lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll
+ true
+
+
+ CP0007
+ T:Microsoft.Extensions.AI.FunctionApprovalResponseContent
+ lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll
+ lib/net10.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..55d01b2d310 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs
@@ -18,18 +18,16 @@ namespace Microsoft.Extensions.AI;
[JsonDerivedType(typeof(TextReasoningContent), typeDiscriminator: "reasoning")]
[JsonDerivedType(typeof(UriContent), typeDiscriminator: "uri")]
[JsonDerivedType(typeof(UsageContent), typeDiscriminator: "usage")]
+[JsonDerivedType(typeof(FunctionApprovalRequestContent), typeDiscriminator: "functionApprovalRequest")]
+[JsonDerivedType(typeof(FunctionApprovalResponseContent), typeDiscriminator: "functionApprovalResponse")]
+[JsonDerivedType(typeof(McpServerToolCallContent), typeDiscriminator: "mcpServerToolCall")]
+[JsonDerivedType(typeof(McpServerToolResultContent), typeDiscriminator: "mcpServerToolResult")]
// These should be added in once they're no longer [Experimental]. If they're included while still
// experimental, any JsonSerializerContext that includes AIContent will incur errors about using
// experimental types in its source generated files. When [Experimental] is removed from these types,
// these lines should be uncommented and the corresponding lines in AIJsonUtilities.CreateDefaultOptions
// as well as the [JsonSerializable] attributes for them on the JsonContext should be removed.
-// [JsonDerivedType(typeof(FunctionApprovalRequestContent), typeDiscriminator: "functionApprovalRequest")]
-// [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/FunctionApprovalRequestContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalRequestContent.cs
index f5a394cd63d..5e9cbcc3d04 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalRequestContent.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalRequestContent.cs
@@ -2,42 +2,39 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
-using System.Diagnostics.CodeAnalysis;
-using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Extensions.AI;
///
-/// Represents a request for user approval of a function call.
+/// Represents a request for approval before invoking a function call.
///
-[Experimental(DiagnosticIds.Experiments.AIFunctionApprovals, UrlFormat = DiagnosticIds.UrlFormat)]
-public sealed class FunctionApprovalRequestContent : UserInputRequestContent
+public sealed class FunctionApprovalRequestContent : InputRequestContent
{
///
/// Initializes a new instance of the class.
///
- /// The ID that uniquely identifies the function approval request/response pair.
- /// The function call that requires user approval.
- /// is .
- /// is empty or composed entirely of whitespace.
+ /// The unique identifier that correlates this request with its corresponding response. This may differ from the of the specified .
+ /// The function call that requires approval before execution.
+ /// is .
+ /// is empty or composed entirely of whitespace.
/// is .
- public FunctionApprovalRequestContent(string id, FunctionCallContent functionCall)
- : base(id)
+ public FunctionApprovalRequestContent(string requestId, FunctionCallContent functionCall)
+ : base(requestId)
{
FunctionCall = Throw.IfNull(functionCall);
}
///
- /// Gets the function call that pre-invoke approval is required for.
+ /// Gets the function call that requires approval before execution.
///
public FunctionCallContent FunctionCall { get; }
///
- /// Creates a to indicate whether the function call is approved or rejected based on the value of .
+ /// Creates a indicating whether the function call is approved or rejected.
///
/// if the function call is approved; otherwise, .
/// An optional reason for the approval or rejection.
- /// The representing the approval response.
- public FunctionApprovalResponseContent CreateResponse(bool approved, string? reason = null) => new(Id, approved, FunctionCall) { Reason = reason };
+ /// The correlated with this request.
+ public FunctionApprovalResponseContent CreateResponse(bool approved, string? reason = null) => new(RequestId, approved, FunctionCall) { Reason = reason };
}
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalResponseContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalResponseContent.cs
index 5cc04c61442..04f12309b37 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalResponseContent.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalResponseContent.cs
@@ -2,41 +2,38 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
-using System.Diagnostics.CodeAnalysis;
-using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Extensions.AI;
///
-/// Represents a response to a function approval request.
+/// Represents a response to a , indicating whether the function call was approved.
///
-[Experimental(DiagnosticIds.Experiments.AIFunctionApprovals, UrlFormat = DiagnosticIds.UrlFormat)]
-public sealed class FunctionApprovalResponseContent : UserInputResponseContent
+public sealed class FunctionApprovalResponseContent : InputResponseContent
{
///
/// Initializes a new instance of the class.
///
- /// The ID that uniquely identifies the function approval request/response pair.
+ /// The unique identifier of the associated with this response.
/// if the function call is approved; otherwise, .
- /// The function call that requires user approval.
- /// is .
- /// is empty or composed entirely of whitespace.
+ /// The function call that was subject to approval.
+ /// is .
+ /// is empty or composed entirely of whitespace.
/// is .
- public FunctionApprovalResponseContent(string id, bool approved, FunctionCallContent functionCall)
- : base(id)
+ public FunctionApprovalResponseContent(string requestId, bool approved, FunctionCallContent functionCall)
+ : base(requestId)
{
Approved = approved;
FunctionCall = Throw.IfNull(functionCall);
}
///
- /// Gets a value indicating whether the user approved the request.
+ /// Gets a value indicating whether the function call was approved for execution.
///
public bool Approved { get; }
///
- /// Gets the function call for which approval was requested.
+ /// Gets the function call that was subject to approval.
///
public FunctionCallContent FunctionCall { get; }
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs
index 6ec7febc486..042e862f986 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs
@@ -14,6 +14,7 @@ namespace Microsoft.Extensions.AI;
/// Represents a function call request.
///
[DebuggerDisplay("{DebuggerDisplay,nq}")]
+[JsonDerivedType(typeof(McpServerToolCallContent), "mcpServerToolCall")]
public class FunctionCallContent : AIContent
{
///
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionResultContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionResultContent.cs
index d5eb4884709..9ffdb11472d 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionResultContent.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionResultContent.cs
@@ -13,6 +13,7 @@ namespace Microsoft.Extensions.AI;
/// Represents the result of a function call.
///
[DebuggerDisplay("{DebuggerDisplay,nq}")]
+[JsonDerivedType(typeof(McpServerToolResultContent), "mcpServerToolResult")]
public class FunctionResultContent : AIContent
{
///
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputRequestContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputRequestContent.cs
new file mode 100644
index 00000000000..80fdf292d50
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputRequestContent.cs
@@ -0,0 +1,31 @@
+// 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.Text.Json.Serialization;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Extensions.AI;
+
+///
+/// Represents a request for input from the user or application.
+///
+[JsonDerivedType(typeof(FunctionApprovalRequestContent), "functionApprovalRequest")]
+public class InputRequestContent : AIContent
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The unique identifier that correlates this request with its corresponding response.
+ /// is .
+ /// is empty or composed entirely of whitespace.
+ protected InputRequestContent(string requestId)
+ {
+ RequestId = Throw.IfNullOrWhitespace(requestId);
+ }
+
+ ///
+ /// Gets the unique identifier that correlates this request with its corresponding .
+ ///
+ public string RequestId { get; }
+}
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputResponseContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputResponseContent.cs
new file mode 100644
index 00000000000..086427dbab5
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputResponseContent.cs
@@ -0,0 +1,31 @@
+// 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.Text.Json.Serialization;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Extensions.AI;
+
+///
+/// Represents the response to an .
+///
+[JsonDerivedType(typeof(FunctionApprovalResponseContent), "functionApprovalResponse")]
+public class InputResponseContent : AIContent
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The unique identifier that correlates this response with its corresponding request.
+ /// is .
+ /// is empty or composed entirely of whitespace.
+ protected InputResponseContent(string requestId)
+ {
+ RequestId = Throw.IfNullOrWhitespace(requestId);
+ }
+
+ ///
+ /// Gets the unique identifier that correlates this response with its corresponding .
+ ///
+ public string RequestId { get; }
+}
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 8a611b1fc82..00000000000
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolApprovalRequestContent.cs
+++ /dev/null
@@ -1,42 +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.DiagnosticIds;
-using Microsoft.Shared.Diagnostics;
-
-namespace Microsoft.Extensions.AI;
-
-///
-/// Represents a request for user approval of an MCP server tool call.
-///
-[Experimental(DiagnosticIds.Experiments.AIMcpServers, UrlFormat = DiagnosticIds.UrlFormat)]
-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 4eaab83e0db..00000000000
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolApprovalResponseContent.cs
+++ /dev/null
@@ -1,33 +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.DiagnosticIds;
-
-namespace Microsoft.Extensions.AI;
-
-///
-/// Represents a response to an MCP server tool approval request.
-///
-[Experimental(DiagnosticIds.Experiments.AIMcpServers, UrlFormat = DiagnosticIds.UrlFormat)]
-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 ef050f69ea2..62b7b25f065 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs
@@ -2,9 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Extensions.AI;
@@ -13,44 +10,35 @@ namespace Microsoft.Extensions.AI;
/// Represents a tool call request to a MCP server.
///
///
+///
/// This content type is used to represent an invocation of an MCP server tool by a hosted service.
-/// It is informational only.
+/// It is informational only and may appear as part of an approval request
+/// to convey what is being approved, or as a record of which MCP server tool was invoked.
+///
///
-[Experimental(DiagnosticIds.Experiments.AIMcpServers, UrlFormat = DiagnosticIds.UrlFormat)]
-public sealed class McpServerToolCallContent : AIContent
+public sealed class McpServerToolCallContent : FunctionCallContent
{
///
/// Initializes a new instance of the class.
///
/// The tool call ID.
- /// The tool name.
+ /// The tool name.
/// The MCP server name that hosts the tool.
- /// or is .
- /// or is empty or composed entirely of whitespace.
- public McpServerToolCallContent(string callId, string toolName, string? serverName)
+ /// or is .
+ /// or is empty or composed entirely of whitespace.
+ ///
+ /// This content is informational only and may appear as part of an approval request
+ /// to convey what is being approved, or as a record of which MCP server tool was invoked.
+ ///
+ public McpServerToolCallContent(string callId, string name, string? serverName)
+ : base(Throw.IfNullOrWhitespace(callId), Throw.IfNullOrWhitespace(name))
{
- CallId = Throw.IfNullOrWhitespace(callId);
- ToolName = Throw.IfNullOrWhitespace(toolName);
ServerName = serverName;
+ InformationalOnly = true;
}
- ///
- /// Gets the tool call ID.
- ///
- public string CallId { get; }
-
- ///
- /// Gets the name of the tool called.
- ///
- public string ToolName { get; }
-
///
/// 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 a8798328019..694b86d4e43 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs
@@ -2,9 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Extensions.AI;
@@ -16,8 +13,7 @@ namespace Microsoft.Extensions.AI;
/// This content type is used to represent the result of an invocation of an MCP server tool by a hosted service.
/// It is informational only.
///
-[Experimental(DiagnosticIds.Experiments.AIMcpServers, UrlFormat = DiagnosticIds.UrlFormat)]
-public sealed class McpServerToolResultContent : AIContent
+public sealed class McpServerToolResultContent : FunctionResultContent
{
///
/// Initializes a new instance of the class.
@@ -26,17 +22,7 @@ public sealed class McpServerToolResultContent : AIContent
/// is .
/// is empty or composed entirely of whitespace.
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
deleted file mode 100644
index 9f40b33253c..00000000000
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputRequestContent.cs
+++ /dev/null
@@ -1,36 +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 System.Text.Json.Serialization;
-using Microsoft.Shared.DiagnosticIds;
-using Microsoft.Shared.Diagnostics;
-
-namespace Microsoft.Extensions.AI;
-
-///
-/// Represents a request for user input.
-///
-[Experimental(DiagnosticIds.Experiments.AIFunctionApprovals, UrlFormat = DiagnosticIds.UrlFormat)]
-[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]
-[JsonDerivedType(typeof(FunctionApprovalRequestContent), "functionApprovalRequest")]
-[JsonDerivedType(typeof(McpServerToolApprovalRequestContent), "mcpServerToolApprovalRequest")]
-public class UserInputRequestContent : AIContent
-{
- ///
- /// Initializes a new instance of the class.
- ///
- /// The ID that uniquely identifies the user input request/response pair.
- /// is .
- /// is empty or composed entirely of whitespace.
- protected UserInputRequestContent(string id)
- {
- Id = Throw.IfNullOrWhitespace(id);
- }
-
- ///
- /// Gets the ID that uniquely identifies the user input request/response pair.
- ///
- public string Id { get; }
-}
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputResponseContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputResponseContent.cs
deleted file mode 100644
index eaddd46f920..00000000000
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputResponseContent.cs
+++ /dev/null
@@ -1,36 +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 System.Text.Json.Serialization;
-using Microsoft.Shared.DiagnosticIds;
-using Microsoft.Shared.Diagnostics;
-
-namespace Microsoft.Extensions.AI;
-
-///
-/// Represents the response to a request for user input.
-///
-[Experimental(DiagnosticIds.Experiments.AIFunctionApprovals, UrlFormat = DiagnosticIds.UrlFormat)]
-[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]
-[JsonDerivedType(typeof(FunctionApprovalResponseContent), "functionApprovalResponse")]
-[JsonDerivedType(typeof(McpServerToolApprovalResponseContent), "mcpServerToolApprovalResponse")]
-public class UserInputResponseContent : AIContent
-{
- ///
- /// Initializes a new instance of the class.
- ///
- /// The ID that uniquely identifies the user input request/response pair.
- /// is .
- /// is empty or composed entirely of whitespace.
- protected UserInputResponseContent(string id)
- {
- Id = Throw.IfNullOrWhitespace(id);
- }
-
- ///
- /// Gets the ID that uniquely identifies the user input request/response pair.
- ///
- public string Id { get; }
-}
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/ApprovalRequiredAIFunction.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/ApprovalRequiredAIFunction.cs
index 77a93342784..1f51cd16c9d 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/ApprovalRequiredAIFunction.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/ApprovalRequiredAIFunction.cs
@@ -2,8 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
-using System.Diagnostics.CodeAnalysis;
-using Microsoft.Shared.DiagnosticIds;
namespace Microsoft.Extensions.AI;
@@ -15,7 +13,6 @@ namespace Microsoft.Extensions.AI;
/// This class simply augments an with an indication that approval is required before invocation.
/// It does not enforce the requirement for user approval; it is the responsibility of the invoker to obtain that approval before invoking the function.
///
-[Experimental(DiagnosticIds.Experiments.AIFunctionApprovals, UrlFormat = DiagnosticIds.UrlFormat)]
public sealed class ApprovalRequiredAIFunction : DelegatingAIFunction
{
///
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolAlwaysRequireApprovalMode.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolAlwaysRequireApprovalMode.cs
index 608839b116b..dfb48583360 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolAlwaysRequireApprovalMode.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolAlwaysRequireApprovalMode.cs
@@ -2,8 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
-using System.Diagnostics.CodeAnalysis;
-using Microsoft.Shared.DiagnosticIds;
namespace Microsoft.Extensions.AI;
@@ -13,7 +11,6 @@ namespace Microsoft.Extensions.AI;
///
/// Use to get an instance of .
///
-[Experimental(DiagnosticIds.Experiments.AIMcpServers, UrlFormat = DiagnosticIds.UrlFormat)]
[DebuggerDisplay(nameof(AlwaysRequire))]
public sealed class HostedMcpServerToolAlwaysRequireApprovalMode : HostedMcpServerToolApprovalMode
{
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolApprovalMode.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolApprovalMode.cs
index 2d81f4c924f..9c4ca705238 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolApprovalMode.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolApprovalMode.cs
@@ -2,9 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
-using Microsoft.Shared.DiagnosticIds;
namespace Microsoft.Extensions.AI;
@@ -15,8 +13,6 @@ namespace Microsoft.Extensions.AI;
/// The predefined values , and are provided to specify handling for all tools.
/// To specify approval behavior for individual tool names, use .
///
-[Experimental(DiagnosticIds.Experiments.AIMcpServers, UrlFormat = DiagnosticIds.UrlFormat)]
-[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]
[JsonDerivedType(typeof(HostedMcpServerToolNeverRequireApprovalMode), typeDiscriminator: "never")]
[JsonDerivedType(typeof(HostedMcpServerToolAlwaysRequireApprovalMode), typeDiscriminator: "always")]
[JsonDerivedType(typeof(HostedMcpServerToolRequireSpecificApprovalMode), typeDiscriminator: "requireSpecific")]
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolNeverRequireApprovalMode.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolNeverRequireApprovalMode.cs
index b21e3a61352..ede9f0f1309 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolNeverRequireApprovalMode.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolNeverRequireApprovalMode.cs
@@ -2,8 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
-using System.Diagnostics.CodeAnalysis;
-using Microsoft.Shared.DiagnosticIds;
namespace Microsoft.Extensions.AI;
@@ -13,7 +11,6 @@ namespace Microsoft.Extensions.AI;
///
/// Use to get an instance of .
///
-[Experimental(DiagnosticIds.Experiments.AIMcpServers, UrlFormat = DiagnosticIds.UrlFormat)]
[DebuggerDisplay(nameof(NeverRequire))]
public sealed class HostedMcpServerToolNeverRequireApprovalMode : HostedMcpServerToolApprovalMode
{
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolRequireSpecificApprovalMode.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolRequireSpecificApprovalMode.cs
index 84d1233a357..526c49bc027 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolRequireSpecificApprovalMode.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolRequireSpecificApprovalMode.cs
@@ -3,16 +3,13 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
using System.Linq;
-using Microsoft.Shared.DiagnosticIds;
namespace Microsoft.Extensions.AI;
///
/// Represents a mode where approval behavior is specified for individual tool names.
///
-[Experimental(DiagnosticIds.Experiments.AIMcpServers, UrlFormat = DiagnosticIds.UrlFormat)]
public sealed class HostedMcpServerToolRequireSpecificApprovalMode : HostedMcpServerToolApprovalMode
{
///
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 d7bfcc15bcf..95cfc015c86 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
@@ -729,11 +729,11 @@
},
{
"Type": "sealed class Microsoft.Extensions.AI.ApprovalRequiredAIFunction : Microsoft.Extensions.AI.DelegatingAIFunction",
- "Stage": "Experimental",
+ "Stage": "Stable",
"Methods": [
{
"Member": "Microsoft.Extensions.AI.ApprovalRequiredAIFunction.ApprovalRequiredAIFunction(Microsoft.Extensions.AI.AIFunction innerFunction);",
- "Stage": "Experimental"
+ "Stage": "Stable"
}
]
},
@@ -1930,46 +1930,46 @@
]
},
{
- "Type": "sealed class Microsoft.Extensions.AI.FunctionApprovalRequestContent : Microsoft.Extensions.AI.UserInputRequestContent",
- "Stage": "Experimental",
+ "Type": "sealed class Microsoft.Extensions.AI.FunctionApprovalRequestContent : Microsoft.Extensions.AI.InputRequestContent",
+ "Stage": "Stable",
"Methods": [
{
- "Member": "Microsoft.Extensions.AI.FunctionApprovalRequestContent.FunctionApprovalRequestContent(string id, Microsoft.Extensions.AI.FunctionCallContent functionCall);",
- "Stage": "Experimental"
+ "Member": "Microsoft.Extensions.AI.FunctionApprovalRequestContent.FunctionApprovalRequestContent(string requestId, Microsoft.Extensions.AI.FunctionCallContent functionCall);",
+ "Stage": "Stable"
},
{
"Member": "Microsoft.Extensions.AI.FunctionApprovalResponseContent Microsoft.Extensions.AI.FunctionApprovalRequestContent.CreateResponse(bool approved, string? reason = null);",
- "Stage": "Experimental"
+ "Stage": "Stable"
}
],
"Properties": [
{
"Member": "Microsoft.Extensions.AI.FunctionCallContent Microsoft.Extensions.AI.FunctionApprovalRequestContent.FunctionCall { get; }",
- "Stage": "Experimental"
+ "Stage": "Stable"
}
]
},
{
- "Type": "sealed class Microsoft.Extensions.AI.FunctionApprovalResponseContent : Microsoft.Extensions.AI.UserInputResponseContent",
- "Stage": "Experimental",
+ "Type": "sealed class Microsoft.Extensions.AI.FunctionApprovalResponseContent : Microsoft.Extensions.AI.InputResponseContent",
+ "Stage": "Stable",
"Methods": [
{
- "Member": "Microsoft.Extensions.AI.FunctionApprovalResponseContent.FunctionApprovalResponseContent(string id, bool approved, Microsoft.Extensions.AI.FunctionCallContent functionCall);",
- "Stage": "Experimental"
+ "Member": "Microsoft.Extensions.AI.FunctionApprovalResponseContent.FunctionApprovalResponseContent(string requestId, bool approved, Microsoft.Extensions.AI.FunctionCallContent functionCall);",
+ "Stage": "Stable"
}
],
"Properties": [
{
"Member": "bool Microsoft.Extensions.AI.FunctionApprovalResponseContent.Approved { get; }",
- "Stage": "Experimental"
+ "Stage": "Stable"
},
{
"Member": "Microsoft.Extensions.AI.FunctionCallContent Microsoft.Extensions.AI.FunctionApprovalResponseContent.FunctionCall { get; }",
- "Stage": "Experimental"
+ "Stage": "Stable"
},
{
"Member": "string? Microsoft.Extensions.AI.FunctionApprovalResponseContent.Reason { get; set; }",
- "Stage": "Experimental"
+ "Stage": "Stable"
}
]
},
@@ -2227,145 +2227,141 @@
},
{
"Type": "class Microsoft.Extensions.AI.HostedMcpServerTool : Microsoft.Extensions.AI.AITool",
- "Stage": "Experimental",
+ "Stage": "Stable",
"Methods": [
{
"Member": "Microsoft.Extensions.AI.HostedMcpServerTool.HostedMcpServerTool(string serverName, string serverAddress);",
- "Stage": "Experimental"
+ "Stage": "Stable"
},
{
"Member": "Microsoft.Extensions.AI.HostedMcpServerTool.HostedMcpServerTool(string serverName, string serverAddress, System.Collections.Generic.IReadOnlyDictionary? additionalProperties);",
- "Stage": "Experimental"
+ "Stage": "Stable"
},
{
- "Member": "Microsoft.Extensions.AI.HostedMcpServerTool.HostedMcpServerTool(string serverName, System.Uri serverUrl);",
- "Stage": "Experimental"
+ "Member": "Microsoft.Extensions.AI.HostedMcpServerTool.HostedMcpServerTool(string serverName, System.Uri serverAddress);",
+ "Stage": "Stable"
},
{
- "Member": "Microsoft.Extensions.AI.HostedMcpServerTool.HostedMcpServerTool(string serverName, System.Uri serverUrl, System.Collections.Generic.IReadOnlyDictionary? additionalProperties);",
- "Stage": "Experimental"
+ "Member": "Microsoft.Extensions.AI.HostedMcpServerTool.HostedMcpServerTool(string serverName, System.Uri serverAddress, System.Collections.Generic.IReadOnlyDictionary? additionalProperties);",
+ "Stage": "Stable"
}
],
"Properties": [
{
"Member": "override System.Collections.Generic.IReadOnlyDictionary Microsoft.Extensions.AI.HostedMcpServerTool.AdditionalProperties { get; }",
- "Stage": "Experimental"
+ "Stage": "Stable"
},
{
"Member": "System.Collections.Generic.IList? Microsoft.Extensions.AI.HostedMcpServerTool.AllowedTools { get; set; }",
- "Stage": "Experimental"
+ "Stage": "Stable"
},
{
"Member": "Microsoft.Extensions.AI.HostedMcpServerToolApprovalMode? Microsoft.Extensions.AI.HostedMcpServerTool.ApprovalMode { get; set; }",
- "Stage": "Experimental"
- },
- {
- "Member": "string? Microsoft.Extensions.AI.HostedMcpServerTool.AuthorizationToken { get; set; }",
- "Stage": "Experimental"
+ "Stage": "Stable"
},
{
- "Member": "System.Collections.Generic.IDictionary Microsoft.Extensions.AI.HostedMcpServerTool.Headers { get; }",
- "Stage": "Experimental"
+ "Member": "System.Collections.Generic.IDictionary? Microsoft.Extensions.AI.HostedMcpServerTool.Headers { get; set; }",
+ "Stage": "Stable"
},
{
"Member": "override string Microsoft.Extensions.AI.HostedMcpServerTool.Name { get; }",
- "Stage": "Experimental"
+ "Stage": "Stable"
},
{
"Member": "string Microsoft.Extensions.AI.HostedMcpServerTool.ServerAddress { get; }",
- "Stage": "Experimental"
+ "Stage": "Stable"
},
{
"Member": "string? Microsoft.Extensions.AI.HostedMcpServerTool.ServerDescription { get; set; }",
- "Stage": "Experimental"
+ "Stage": "Stable"
},
{
"Member": "string Microsoft.Extensions.AI.HostedMcpServerTool.ServerName { get; }",
- "Stage": "Experimental"
+ "Stage": "Stable"
}
]
},
{
"Type": "sealed class Microsoft.Extensions.AI.HostedMcpServerToolAlwaysRequireApprovalMode : Microsoft.Extensions.AI.HostedMcpServerToolApprovalMode",
- "Stage": "Experimental",
+ "Stage": "Stable",
"Methods": [
{
"Member": "Microsoft.Extensions.AI.HostedMcpServerToolAlwaysRequireApprovalMode.HostedMcpServerToolAlwaysRequireApprovalMode();",
- "Stage": "Experimental"
+ "Stage": "Stable"
},
{
"Member": "override bool Microsoft.Extensions.AI.HostedMcpServerToolAlwaysRequireApprovalMode.Equals(object? obj);",
- "Stage": "Experimental"
+ "Stage": "Stable"
},
{
"Member": "override int Microsoft.Extensions.AI.HostedMcpServerToolAlwaysRequireApprovalMode.GetHashCode();",
- "Stage": "Experimental"
+ "Stage": "Stable"
}
]
},
{
"Type": "class Microsoft.Extensions.AI.HostedMcpServerToolApprovalMode",
- "Stage": "Experimental",
+ "Stage": "Stable",
"Methods": [
{
"Member": "static Microsoft.Extensions.AI.HostedMcpServerToolRequireSpecificApprovalMode Microsoft.Extensions.AI.HostedMcpServerToolApprovalMode.RequireSpecific(System.Collections.Generic.IList? alwaysRequireApprovalToolNames, System.Collections.Generic.IList? neverRequireApprovalToolNames);",
- "Stage": "Experimental"
+ "Stage": "Stable"
}
],
"Properties": [
{
"Member": "static Microsoft.Extensions.AI.HostedMcpServerToolAlwaysRequireApprovalMode Microsoft.Extensions.AI.HostedMcpServerToolApprovalMode.AlwaysRequire { get; }",
- "Stage": "Experimental"
+ "Stage": "Stable"
},
{
"Member": "static Microsoft.Extensions.AI.HostedMcpServerToolNeverRequireApprovalMode Microsoft.Extensions.AI.HostedMcpServerToolApprovalMode.NeverRequire { get; }",
- "Stage": "Experimental"
+ "Stage": "Stable"
}
]
},
{
"Type": "sealed class Microsoft.Extensions.AI.HostedMcpServerToolNeverRequireApprovalMode : Microsoft.Extensions.AI.HostedMcpServerToolApprovalMode",
- "Stage": "Experimental",
+ "Stage": "Stable",
"Methods": [
{
"Member": "Microsoft.Extensions.AI.HostedMcpServerToolNeverRequireApprovalMode.HostedMcpServerToolNeverRequireApprovalMode();",
- "Stage": "Experimental"
+ "Stage": "Stable"
},
{
"Member": "override bool Microsoft.Extensions.AI.HostedMcpServerToolNeverRequireApprovalMode.Equals(object? obj);",
- "Stage": "Experimental"
+ "Stage": "Stable"
},
{
"Member": "override int Microsoft.Extensions.AI.HostedMcpServerToolNeverRequireApprovalMode.GetHashCode();",
- "Stage": "Experimental"
+ "Stage": "Stable"
}
]
},
{
"Type": "sealed class Microsoft.Extensions.AI.HostedMcpServerToolRequireSpecificApprovalMode : Microsoft.Extensions.AI.HostedMcpServerToolApprovalMode",
- "Stage": "Experimental",
+ "Stage": "Stable",
"Methods": [
{
"Member": "Microsoft.Extensions.AI.HostedMcpServerToolRequireSpecificApprovalMode.HostedMcpServerToolRequireSpecificApprovalMode(System.Collections.Generic.IList? alwaysRequireApprovalToolNames, System.Collections.Generic.IList? neverRequireApprovalToolNames);",
- "Stage": "Experimental"
+ "Stage": "Stable"
},
{
"Member": "override bool Microsoft.Extensions.AI.HostedMcpServerToolRequireSpecificApprovalMode.Equals(object? obj);",
- "Stage": "Experimental"
+ "Stage": "Stable"
},
{
"Member": "override int Microsoft.Extensions.AI.HostedMcpServerToolRequireSpecificApprovalMode.GetHashCode();",
- "Stage": "Experimental"
+ "Stage": "Stable"
}
],
"Properties": [
{
"Member": "System.Collections.Generic.IList? Microsoft.Extensions.AI.HostedMcpServerToolRequireSpecificApprovalMode.AlwaysRequireApprovalToolNames { get; set; }",
- "Stage": "Experimental"
+ "Stage": "Stable"
},
{
"Member": "System.Collections.Generic.IList? Microsoft.Extensions.AI.HostedMcpServerToolRequireSpecificApprovalMode.NeverRequireApprovalToolNames { get; set; }",
- "Stage": "Experimental"
+ "Stage": "Stable"
}
]
},
@@ -2429,11 +2425,11 @@
},
{
"Type": "interface Microsoft.Extensions.AI.IChatReducer",
- "Stage": "Stable",
+ "Stage": "Experimental",
"Methods": [
{
"Member": "System.Threading.Tasks.Task> Microsoft.Extensions.AI.IChatReducer.ReduceAsync(System.Collections.Generic.IEnumerable messages, System.Threading.CancellationToken cancellationToken);",
- "Stage": "Stable"
+ "Stage": "Experimental"
}
]
},
@@ -2701,114 +2697,88 @@
]
},
{
- "Type": "interface Microsoft.Extensions.AI.ISpeechToTextClient : System.IDisposable",
- "Stage": "Experimental",
+ "Type": "class Microsoft.Extensions.AI.InputRequestContent : Microsoft.Extensions.AI.AIContent",
+ "Stage": "Stable",
"Methods": [
{
- "Member": "object? Microsoft.Extensions.AI.ISpeechToTextClient.GetService(System.Type serviceType, object? serviceKey = null);",
- "Stage": "Experimental"
- },
- {
- "Member": "System.Collections.Generic.IAsyncEnumerable Microsoft.Extensions.AI.ISpeechToTextClient.GetStreamingTextAsync(System.IO.Stream audioSpeechStream, Microsoft.Extensions.AI.SpeechToTextOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));",
- "Stage": "Experimental"
- },
+ "Member": "Microsoft.Extensions.AI.InputRequestContent.InputRequestContent(string requestId);",
+ "Stage": "Stable"
+ }
+ ],
+ "Properties": [
{
- "Member": "System.Threading.Tasks.Task Microsoft.Extensions.AI.ISpeechToTextClient.GetTextAsync(System.IO.Stream audioSpeechStream, Microsoft.Extensions.AI.SpeechToTextOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));",
- "Stage": "Experimental"
+ "Member": "string Microsoft.Extensions.AI.InputRequestContent.RequestId { get; }",
+ "Stage": "Stable"
}
]
},
{
- "Type": "interface Microsoft.Extensions.AI.IToolReductionStrategy",
- "Stage": "Experimental",
+ "Type": "class Microsoft.Extensions.AI.InputResponseContent : Microsoft.Extensions.AI.AIContent",
+ "Stage": "Stable",
"Methods": [
{
- "Member": "System.Threading.Tasks.Task> Microsoft.Extensions.AI.IToolReductionStrategy.SelectToolsForRequestAsync(System.Collections.Generic.IEnumerable messages, Microsoft.Extensions.AI.ChatOptions? options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));",
- "Stage": "Experimental"
+ "Member": "Microsoft.Extensions.AI.InputResponseContent.InputResponseContent(string requestId);",
+ "Stage": "Stable"
+ }
+ ],
+ "Properties": [
+ {
+ "Member": "string Microsoft.Extensions.AI.InputResponseContent.RequestId { get; }",
+ "Stage": "Stable"
}
]
},
{
- "Type": "sealed class Microsoft.Extensions.AI.McpServerToolApprovalRequestContent : Microsoft.Extensions.AI.UserInputRequestContent",
+ "Type": "interface Microsoft.Extensions.AI.ISpeechToTextClient : System.IDisposable",
"Stage": "Experimental",
"Methods": [
{
- "Member": "Microsoft.Extensions.AI.McpServerToolApprovalRequestContent.McpServerToolApprovalRequestContent(string id, Microsoft.Extensions.AI.McpServerToolCallContent toolCall);",
+ "Member": "object? Microsoft.Extensions.AI.ISpeechToTextClient.GetService(System.Type serviceType, object? serviceKey = null);",
"Stage": "Experimental"
},
{
- "Member": "Microsoft.Extensions.AI.McpServerToolApprovalResponseContent Microsoft.Extensions.AI.McpServerToolApprovalRequestContent.CreateResponse(bool approved);",
+ "Member": "System.Collections.Generic.IAsyncEnumerable Microsoft.Extensions.AI.ISpeechToTextClient.GetStreamingTextAsync(System.IO.Stream audioSpeechStream, Microsoft.Extensions.AI.SpeechToTextOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));",
"Stage": "Experimental"
- }
- ],
- "Properties": [
+ },
{
- "Member": "Microsoft.Extensions.AI.McpServerToolCallContent Microsoft.Extensions.AI.McpServerToolApprovalRequestContent.ToolCall { get; }",
+ "Member": "System.Threading.Tasks.Task Microsoft.Extensions.AI.ISpeechToTextClient.GetTextAsync(System.IO.Stream audioSpeechStream, Microsoft.Extensions.AI.SpeechToTextOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));",
"Stage": "Experimental"
}
]
},
{
- "Type": "sealed class Microsoft.Extensions.AI.McpServerToolApprovalResponseContent : Microsoft.Extensions.AI.UserInputResponseContent",
+ "Type": "interface Microsoft.Extensions.AI.IToolReductionStrategy",
"Stage": "Experimental",
"Methods": [
{
- "Member": "Microsoft.Extensions.AI.McpServerToolApprovalResponseContent.McpServerToolApprovalResponseContent(string id, bool approved);",
- "Stage": "Experimental"
- }
- ],
- "Properties": [
- {
- "Member": "bool Microsoft.Extensions.AI.McpServerToolApprovalResponseContent.Approved { get; }",
+ "Member": "System.Threading.Tasks.Task> Microsoft.Extensions.AI.IToolReductionStrategy.SelectToolsForRequestAsync(System.Collections.Generic.IEnumerable messages, Microsoft.Extensions.AI.ChatOptions? options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));",
"Stage": "Experimental"
}
]
},
{
- "Type": "sealed class Microsoft.Extensions.AI.McpServerToolCallContent : Microsoft.Extensions.AI.AIContent",
- "Stage": "Experimental",
+ "Type": "sealed class Microsoft.Extensions.AI.McpServerToolCallContent : Microsoft.Extensions.AI.FunctionCallContent",
+ "Stage": "Stable",
"Methods": [
{
- "Member": "Microsoft.Extensions.AI.McpServerToolCallContent.McpServerToolCallContent(string callId, string toolName, string? serverName);",
- "Stage": "Experimental"
+ "Member": "Microsoft.Extensions.AI.McpServerToolCallContent.McpServerToolCallContent(string callId, string name, string? serverName);",
+ "Stage": "Stable"
}
],
"Properties": [
- {
- "Member": "System.Collections.Generic.IReadOnlyDictionary? Microsoft.Extensions.AI.McpServerToolCallContent.Arguments { get; set; }",
- "Stage": "Experimental"
- },
- {
- "Member": "string Microsoft.Extensions.AI.McpServerToolCallContent.CallId { get; }",
- "Stage": "Experimental"
- },
{
"Member": "string? Microsoft.Extensions.AI.McpServerToolCallContent.ServerName { get; }",
- "Stage": "Experimental"
- },
- {
- "Member": "string Microsoft.Extensions.AI.McpServerToolCallContent.ToolName { get; }",
- "Stage": "Experimental"
+ "Stage": "Stable"
}
]
},
{
- "Type": "sealed class Microsoft.Extensions.AI.McpServerToolResultContent : Microsoft.Extensions.AI.AIContent",
- "Stage": "Experimental",
+ "Type": "sealed class Microsoft.Extensions.AI.McpServerToolResultContent : Microsoft.Extensions.AI.FunctionResultContent",
+ "Stage": "Stable",
"Methods": [
{
"Member": "Microsoft.Extensions.AI.McpServerToolResultContent.McpServerToolResultContent(string callId);",
- "Stage": "Experimental"
- }
- ],
- "Properties": [
- {
- "Member": "string Microsoft.Extensions.AI.McpServerToolResultContent.CallId { get; }",
- "Stage": "Experimental"
- },
- {
- "Member": "System.Collections.Generic.IList? Microsoft.Extensions.AI.McpServerToolResultContent.Output { get; set; }",
- "Stage": "Experimental"
+ "Stage": "Stable"
}
]
},
@@ -3435,38 +3405,6 @@
"Stage": "Stable"
}
]
- },
- {
- "Type": "class Microsoft.Extensions.AI.UserInputRequestContent : Microsoft.Extensions.AI.AIContent",
- "Stage": "Experimental",
- "Methods": [
- {
- "Member": "Microsoft.Extensions.AI.UserInputRequestContent.UserInputRequestContent(string id);",
- "Stage": "Experimental"
- }
- ],
- "Properties": [
- {
- "Member": "string Microsoft.Extensions.AI.UserInputRequestContent.Id { get; }",
- "Stage": "Experimental"
- }
- ]
- },
- {
- "Type": "class Microsoft.Extensions.AI.UserInputResponseContent : Microsoft.Extensions.AI.AIContent",
- "Stage": "Experimental",
- "Methods": [
- {
- "Member": "Microsoft.Extensions.AI.UserInputResponseContent.UserInputResponseContent(string id);",
- "Stage": "Experimental"
- }
- ],
- "Properties": [
- {
- "Member": "string Microsoft.Extensions.AI.UserInputResponseContent.Id { get; }",
- "Stage": "Experimental"
- }
- ]
}
]
}
\ No newline at end of file
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs
index ef95d68031c..682933df7f7 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs
@@ -3,8 +3,6 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Extensions.AI;
@@ -12,18 +10,11 @@ namespace Microsoft.Extensions.AI;
///
/// Represents a hosted MCP server tool that can be specified to an AI service.
///
-[Experimental(DiagnosticIds.Experiments.AIMcpServers, UrlFormat = DiagnosticIds.UrlFormat)]
public class HostedMcpServerTool : AITool
{
- /// The name of the Authorization header.
- private const string AuthorizationHeaderName = "Authorization";
-
/// Any additional properties associated with the tool.
private IReadOnlyDictionary? _additionalProperties;
- /// Lazily-initialized collection of headers to include when calling the remote MCP server.
- private Dictionary? _headers;
-
///
/// Initializes a new instance of the class.
///
@@ -55,12 +46,12 @@ public HostedMcpServerTool(string serverName, string serverAddress, IReadOnlyDic
/// Initializes a new instance of the class.
///
/// The name of the remote MCP server.
- /// The URL of the remote MCP server.
- /// or is .
+ /// The URL of the remote MCP server.
+ /// or is .
/// is empty or composed entirely of whitespace.
- /// is not an absolute URL.
- public HostedMcpServerTool(string serverName, Uri serverUrl)
- : this(serverName, ValidateUrl(serverUrl))
+ /// is not an absolute URL.
+ public HostedMcpServerTool(string serverName, Uri serverAddress)
+ : this(serverName, ValidateUrl(serverAddress))
{
}
@@ -68,27 +59,27 @@ public HostedMcpServerTool(string serverName, Uri serverUrl)
/// Initializes a new instance of the class.
///
/// The name of the remote MCP server.
- /// The URL of the remote MCP server.
+ /// The URL of the remote MCP server.
/// Any additional properties associated with the tool.
- /// or is .
+ /// or is .
/// is empty or composed entirely of whitespace.
- /// is not an absolute URL.
- public HostedMcpServerTool(string serverName, Uri serverUrl, IReadOnlyDictionary? additionalProperties)
- : this(serverName, ValidateUrl(serverUrl))
+ /// is not an absolute URL.
+ public HostedMcpServerTool(string serverName, Uri serverAddress, IReadOnlyDictionary? additionalProperties)
+ : this(serverName, ValidateUrl(serverAddress))
{
_additionalProperties = additionalProperties;
}
- private static string ValidateUrl(Uri serverUrl)
+ private static string ValidateUrl(Uri serverAddress)
{
- _ = Throw.IfNull(serverUrl);
+ _ = Throw.IfNull(serverAddress);
- if (!serverUrl.IsAbsoluteUri)
+ if (!serverAddress.IsAbsoluteUri)
{
- Throw.ArgumentException(nameof(serverUrl), "The provided URL is not absolute.");
+ Throw.ArgumentException(nameof(serverAddress), "The provided URL is not absolute.");
}
- return serverUrl.AbsoluteUri;
+ return serverAddress.AbsoluteUri;
}
///
@@ -107,39 +98,6 @@ private static string ValidateUrl(Uri serverUrl)
///
public string ServerAddress { get; }
- ///
- /// Gets or sets the OAuth authorization token that the AI service should use when calling the remote MCP server.
- ///
- ///
- /// When set, this value is automatically added to the dictionary with the key "Authorization"
- /// and the value "Bearer {token}". Setting this property will overwrite any existing "Authorization" header in .
- /// Setting this property to will remove the "Authorization" header from .
- ///
- public string? AuthorizationToken
- {
- get
- {
- if (_headers?.TryGetValue(AuthorizationHeaderName, out string? value) is true &&
- value?.StartsWith("Bearer ", StringComparison.Ordinal) is true)
- {
- return value.Substring("Bearer ".Length);
- }
-
- return null;
- }
- set
- {
- if (value is not null)
- {
- Headers[AuthorizationHeaderName] = $"Bearer {value}";
- }
- else if (_headers is not null)
- {
- _ = _headers.Remove(AuthorizationHeaderName);
- }
- }
- }
-
///
/// Gets or sets the description of the remote MCP server, used to provide more context to the AI service.
///
@@ -171,12 +129,15 @@ public string? AuthorizationToken
public HostedMcpServerToolApprovalMode? ApprovalMode { get; set; }
///
- /// Gets a mutable dictionary of HTTP headers to include when calling the remote MCP server.
+ /// Gets or sets a mutable dictionary of HTTP headers to include when calling the remote MCP server.
///
///
///
/// The underlying provider is not guaranteed to support or honor the headers.
///
+ ///
+ /// This property is useful for specifying the authentication header or other headers required by the MCP server.
+ ///
///
- public IDictionary Headers => _headers ??= new Dictionary();
+ public IDictionary? Headers { get; set; }
}
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 0f2e4340358..8c9ffeaa54a 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs
@@ -51,12 +51,6 @@ private static JsonSerializerOptions CreateDefaultOptions()
// Temporary workaround: these types are [Experimental] and can't be added as [JsonDerivedType] on AIContent yet,
// or else consuming assemblies that used source generation with AIContent would implicitly reference them.
// Once they're no longer [Experimental] and added as [JsonDerivedType] on AIContent, these lines should be removed.
- AddAIContentType(options, typeof(FunctionApprovalRequestContent), typeDiscriminatorId: "functionApprovalRequest", checkBuiltIn: false);
- 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);
AddAIContentType(options, typeof(ImageGenerationToolCallContent), typeDiscriminatorId: "imageGenerationToolCall", checkBuiltIn: false);
@@ -123,16 +117,15 @@ private static JsonSerializerOptions CreateDefaultOptions()
[JsonSerializable(typeof(AIContent))]
[JsonSerializable(typeof(IEnumerable))]
+ // InputRequestContent and InputResponseContent are polymorphic base types that may be
+ // serialized as root types (not just as AIContent). They have protected constructors so
+ // can't be instantiated directly, but we still need metadata when serializing derived
+ // types (e.g., FunctionApprovalRequestContent) as InputRequestContent.
+ [JsonSerializable(typeof(InputRequestContent))]
+ [JsonSerializable(typeof(InputResponseContent))]
+
// Temporary workaround: These should be implicitly added in once they're no longer [Experimental]
// and are included via [JsonDerivedType] on AIContent.
- [JsonSerializable(typeof(UserInputRequestContent))]
- [JsonSerializable(typeof(UserInputResponseContent))]
- [JsonSerializable(typeof(FunctionApprovalRequestContent))]
- [JsonSerializable(typeof(FunctionApprovalResponseContent))]
- [JsonSerializable(typeof(McpServerToolCallContent))]
- [JsonSerializable(typeof(McpServerToolResultContent))]
- [JsonSerializable(typeof(McpServerToolApprovalRequestContent))]
- [JsonSerializable(typeof(McpServerToolApprovalResponseContent))]
[JsonSerializable(typeof(CodeInterpreterToolCallContent))]
[JsonSerializable(typeof(CodeInterpreterToolResultContent))]
[JsonSerializable(typeof(ImageGenerationToolCallContent))]
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIJsonContext.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIJsonContext.cs
index 8f42edb24d6..9a040864613 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIJsonContext.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIJsonContext.cs
@@ -17,7 +17,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 af13a52328d..eedcf3b79df 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs
@@ -167,6 +167,7 @@ internal static ChatResponse FromOpenAIResponse(ResponseResult responseResult, C
internal static IEnumerable ToChatMessages(IEnumerable items, CreateResponseOptions? options = null)
{
ChatMessage? message = null;
+ Dictionary? mcpApprovalRequests = null;
foreach (ResponseItem outputItem in items)
{
@@ -210,9 +211,13 @@ internal static IEnumerable ToChatMessages(IEnumerable ToChatMessages(IEnumerable FromOpenAIStreamingRe
ChatRole? lastRole = null;
bool anyFunctions = false;
ResponseStatus? latestResponseStatus = null;
+ Dictionary? mcpApprovalRequests = null;
UpdateConversationId(resumeResponseId);
@@ -447,9 +467,13 @@ ChatResponseUpdate CreateUpdate(AIContent? content = null) =>
break;
case McpToolCallApprovalRequestItem mtcari:
- yield return CreateUpdate(new McpServerToolApprovalRequestContent(mtcari.Id, new(mtcari.Id, mtcari.ToolName, mtcari.ServerLabel)
+ // Store for correlation with responses.
+ (mcpApprovalRequests ??= new())[mtcari.Id] = mtcari;
+
+ // We are reusing the mtcari.Id as the McpServerToolCallContent.CallId since we don't have one yet.
+ 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,
})
{
@@ -457,6 +481,24 @@ ChatResponseUpdate CreateUpdate(AIContent? content = null) =>
});
break;
+ case McpToolCallApprovalResponseItem mtcari
+ when mcpApprovalRequests?.TryGetValue(mtcari.ApprovalRequestId, out McpToolCallApprovalRequestItem? request) is true:
+ _ = mcpApprovalRequests.Remove(mtcari.ApprovalRequestId);
+
+ // Correlate with the original request to get tool details.
+ // McpToolCallApprovalResponseItem without a correlated request falls through to default.
+ yield return CreateUpdate(new FunctionApprovalResponseContent(
+ mtcari.ApprovalRequestId,
+ mtcari.Approved,
+ new McpServerToolCallContent(mtcari.ApprovalRequestId, request.ToolName, request.ServerLabel)
+ {
+ Arguments = JsonSerializer.Deserialize(request.ToolArguments, OpenAIJsonContext.Default.IDictionaryStringObject),
+ })
+ {
+ RawRepresentation = mtcari,
+ });
+ break;
+
case CodeInterpreterCallResponseItem cicri:
// The CodeInterpreterToolCallContent has already been yielded as part of delta updates.
// Only yield the CodeInterpreterToolResultContent here for the outputs.
@@ -635,16 +677,22 @@ void IDisposable.Dispose()
if (isUrl)
{
- // For http: favor headers over authorization token.
- if (mcpTool.Headers.Count > 0)
+ if (mcpTool.Headers is { Count: > 0 })
{
responsesMcpTool.Headers = mcpTool.Headers;
}
}
else
{
- // For connectors: Only set AuthorizationToken, do not include headers.
- responsesMcpTool.AuthorizationToken = mcpTool.AuthorizationToken;
+ // For connectors: extract Bearer token from Headers and set as AuthorizationToken.
+ // Use case-insensitive comparison since auth scheme is case-insensitive per RFC 7235.
+ // Allow flexible whitespace in the header value.
+ if (mcpTool.Headers?.TryGetValue("Authorization", out string? authHeader) is true &&
+ authHeader.AsSpan().Trim() is { Length: > 0 } trimmedAuthHeader &&
+ trimmedAuthHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
+ {
+ responsesMcpTool.AuthorizationToken = trimmedAuthHeader.Slice("Bearer ".Length).TrimStart().ToString();
+ }
}
if (mcpTool.AllowedTools is not null)
@@ -886,7 +934,7 @@ internal static IEnumerable ToOpenAIResponseItems(IEnumerable? idToContentMapping = null;
+ Dictionary? idToContentMapping = null;
foreach (ChatMessage input in inputs)
{
@@ -919,7 +967,7 @@ internal static IEnumerable ToOpenAIResponseItems(IEnumerable rawRep,
- McpServerToolApprovalResponseContent mcpResp => ResponseItem.CreateMcpApprovalResponseItem(mcpResp.Id, mcpResp.Approved),
+ FunctionApprovalResponseContent { FunctionCall: McpServerToolCallContent } funcResp => ResponseItem.CreateMcpApprovalResponseItem(funcResp.RequestId, funcResp.Approved),
_ => null
};
@@ -1001,6 +1049,10 @@ internal static IEnumerable ToOpenAIResponseItems(IEnumerable contents)
{
@@ -1115,10 +1167,6 @@ static FunctionCallOutputResponseItem SerializeAIContent(string callId, IEnumera
break;
}
break;
-
- case McpServerToolApprovalResponseContent mcpApprovalResponseContent:
- yield return ResponseItem.CreateMcpApprovalResponseItem(mcpApprovalResponseContent.Id, mcpApprovalResponseContent.Approved);
- break;
}
}
@@ -1146,6 +1194,10 @@ static FunctionCallOutputResponseItem SerializeAIContent(string callId, IEnumera
};
break;
+ case McpServerToolCallContent mstcc:
+ (idToContentMapping ??= [])[mstcc.CallId] = mstcc;
+ break;
+
case FunctionCallContent callContent:
yield return ResponseItem.CreateFunctionCallItem(
callContent.CallId,
@@ -1155,34 +1207,33 @@ static FunctionCallOutputResponseItem SerializeAIContent(string callId, IEnumera
AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IDictionary)))));
break;
- case McpServerToolApprovalRequestContent mcpApprovalRequestContent:
+ case FunctionApprovalRequestContent funcReq when funcReq.FunctionCall is McpServerToolCallContent mcpToolCall:
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;
+ funcReq.RequestId,
+ mcpToolCall.ServerName,
+ mcpToolCall.Name,
+ BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(
+ mcpToolCall.Arguments!,
+ AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IDictionary)))));
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)
+ associatedCall.Name,
+ BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(
+ associatedCall.Arguments!,
+ AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IDictionary)))));
+ if (mstrc.Result is ErrorContent errorContent)
{
mtci.Error = BinaryData.FromString(errorContent.Message);
}
- else
+ else if (mstrc.Result is TextContent textContent)
{
- mtci.ToolOutput = string.Concat(mstrc.Output?.OfType() ?? []);
+ mtci.ToolOutput = textContent.Text;
}
yield return mtci;
@@ -1367,7 +1418,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
@@ -1377,9 +1428,9 @@ private static void AddMcpToolCallContent(McpToolCallItem mtci, IList
contents.Add(new McpServerToolResultContent(mtci.Id)
{
RawRepresentation = mtci,
- Output = [mtci.Error is not null ?
+ Result = mtci.Error is not null ?
new ErrorContent(mtci.Error.ToString()) :
- new TextContent(mtci.ToolOutput)],
+ new TextContent(mtci.ToolOutput),
});
}
diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs
index 9fd95df0424..7fcf5b770de 100644
--- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs
+++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs
@@ -1464,13 +1464,13 @@ private static bool CurrentActivityIsInvokeAgent
var content = message.Contents[j];
switch (content)
{
- case FunctionApprovalRequestContent farc:
+ case FunctionApprovalRequestContent farc when !farc.FunctionCall.InformationalOnly:
// Validation: Capture each call id for each approval request to ensure later we have a matching response.
_ = (approvalRequestCallIds ??= []).Add(farc.FunctionCall.CallId);
- (allApprovalRequestsMessages ??= []).Add(farc.Id, message);
+ (allApprovalRequestsMessages ??= []).Add(farc.RequestId, message);
break;
- case FunctionApprovalResponseContent farc:
+ case FunctionApprovalResponseContent farc when !farc.FunctionCall.InformationalOnly:
// Validation: Remove the call id for each approval response, to check it off the list of requests we need responses for.
_ = approvalRequestCallIds?.Remove(farc.FunctionCall.CallId);
(allApprovalResponses ??= []).Add(farc);
@@ -1542,7 +1542,7 @@ private static bool CurrentActivityIsInvokeAgent
ref List? targetList = ref approvalResponse.Approved ? ref approvedFunctionCalls : ref rejectedFunctionCalls;
ChatMessage? requestMessage = null;
- _ = allApprovalRequestsMessages?.TryGetValue(approvalResponse.Id, out requestMessage);
+ _ = allApprovalRequestsMessages?.TryGetValue(approvalResponse.RequestId, out requestMessage);
(targetList ??= []).Add(new() { Response = approvalResponse, RequestMessage = requestMessage });
}
@@ -1711,7 +1711,7 @@ private static bool TryReplaceFunctionCallsWithApprovalRequests(IList
if (content[i] is FunctionCallContent fcc && !fcc.InformationalOnly)
{
updatedContent ??= [.. content]; // Clone the list if we haven't already
- updatedContent[i] = new FunctionApprovalRequestContent(fcc.CallId, fcc);
+ updatedContent[i] = new FunctionApprovalRequestContent(ComposeApprovalRequestId(fcc.CallId), fcc);
}
}
}
@@ -1766,7 +1766,7 @@ private IList ReplaceFunctionCallsWithApprovalRequests(
var functionCall = (FunctionCallContent)message.Contents[contentIndex];
LogFunctionRequiresApproval(functionCall.Name);
- message.Contents[contentIndex] = new FunctionApprovalRequestContent(functionCall.CallId, functionCall);
+ message.Contents[contentIndex] = new FunctionApprovalRequestContent(ComposeApprovalRequestId(functionCall.CallId), functionCall);
outputMessages[messageIndex] = message;
lastMessageIndex = messageIndex;
@@ -1783,6 +1783,9 @@ private static TimeSpan GetElapsedTime(long startingTimestamp) =>
new((long)((Stopwatch.GetTimestamp() - startingTimestamp) * ((double)TimeSpan.TicksPerSecond / Stopwatch.Frequency)));
#endif
+ /// Composes an approval request ID from a function call ID.
+ private static string ComposeApprovalRequestId(string callId) => $"ficc_{callId}";
+
///
/// Execute the provided and return the resulting
/// wrapped in objects.
diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs
index bcb4fb2a241..ad3cf4feae0 100644
--- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs
+++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs
@@ -258,6 +258,30 @@ internal static string SerializeChatMessages(
m.Parts.Add(new OtelGenericPart { Type = "reasoning", Content = trc.Text });
break;
+ case McpServerToolCallContent mstcc:
+ m.Parts.Add(new OtelServerToolCallPart
+ {
+ Id = mstcc.CallId,
+ Name = mstcc.Name,
+ ServerToolCall = new OtelMcpToolCall
+ {
+ Arguments = mstcc.Arguments,
+ ServerName = mstcc.ServerName,
+ },
+ });
+ break;
+
+ case McpServerToolResultContent mstrc:
+ m.Parts.Add(new OtelServerToolCallResponsePart
+ {
+ Id = mstrc.CallId,
+ ServerToolCallResponse = new OtelMcpToolCallResponse
+ {
+ Output = mstrc.Result,
+ },
+ });
+ break;
+
case FunctionCallContent fcc:
m.Parts.Add(new OtelToolCallRequestPart
{
@@ -357,50 +381,26 @@ internal static string SerializeChatMessages(
});
break;
- case McpServerToolCallContent mstcc:
- m.Parts.Add(new OtelServerToolCallPart
- {
- Id = mstcc.CallId,
- Name = mstcc.ToolName,
- ServerToolCall = new OtelMcpToolCall
- {
- Arguments = mstcc.Arguments,
- ServerName = mstcc.ServerName,
- },
- });
- break;
-
- case McpServerToolResultContent mstrc:
- m.Parts.Add(new OtelServerToolCallResponsePart
- {
- Id = mstrc.CallId,
- ServerToolCallResponse = new OtelMcpToolCallResponse
- {
- Output = mstrc.Output,
- },
- });
- break;
-
- case McpServerToolApprovalRequestContent mstarc:
+ case FunctionApprovalRequestContent fareqc when fareqc.FunctionCall is McpServerToolCallContent mcpToolCall:
m.Parts.Add(new OtelServerToolCallPart
{
- Id = mstarc.Id,
- Name = mstarc.ToolCall.ToolName,
+ Id = fareqc.RequestId,
+ Name = fareqc.FunctionCall.Name,
ServerToolCall = new OtelMcpApprovalRequest
{
- Arguments = mstarc.ToolCall.Arguments,
- ServerName = mstarc.ToolCall.ServerName,
+ Arguments = mcpToolCall.Arguments,
+ ServerName = mcpToolCall.ServerName,
},
});
break;
- case McpServerToolApprovalResponseContent mstaresp:
+ case FunctionApprovalResponseContent farespc when farespc.FunctionCall is McpServerToolCallContent:
m.Parts.Add(new OtelServerToolCallResponsePart
{
- Id = mstaresp.Id,
+ Id = farespc.RequestId,
ServerToolCallResponse = new OtelMcpApprovalResponse
{
- Approved = mstaresp.Approved,
+ Approved = farespc.Approved,
},
});
break;
@@ -858,7 +858,7 @@ private sealed class OtelMcpToolCall
{
public string Type { get; set; } = "mcp";
public string? ServerName { get; set; }
- public IReadOnlyDictionary? Arguments { get; set; }
+ public IDictionary? Arguments { get; set; }
}
private sealed class OtelMcpToolCallResponse
@@ -871,7 +871,7 @@ private sealed class OtelMcpApprovalRequest
{
public string Type { get; set; } = "mcp_approval_request";
public string? ServerName { get; set; }
- public IReadOnlyDictionary? Arguments { get; set; }
+ public IDictionary? Arguments { get; set; }
}
private sealed class OtelMcpApprovalResponse
diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatReduction/SummarizingChatReducer.cs b/src/Libraries/Microsoft.Extensions.AI/ChatReduction/SummarizingChatReducer.cs
index a3b38ca14cd..7121f61cd8f 100644
--- a/src/Libraries/Microsoft.Extensions.AI/ChatReduction/SummarizingChatReducer.cs
+++ b/src/Libraries/Microsoft.Extensions.AI/ChatReduction/SummarizingChatReducer.cs
@@ -208,8 +208,8 @@ public IEnumerable ToChatMessages()
private static bool IsToolRelatedContent(AIContent content) => content
is FunctionCallContent
or FunctionResultContent
- or UserInputRequestContent
- or UserInputResponseContent;
+ or InputRequestContent
+ or InputResponseContent;
/// Builds the list of messages to send to the chat client for summarization.
private IEnumerable ToSummarizerChatMessages(int indexOfFirstMessageToKeep, string summarizationPrompt)
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/AssertExtensions.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/AssertExtensions.cs
index 546b93a7f20..975ccb26230 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/AssertExtensions.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/AssertExtensions.cs
@@ -46,6 +46,23 @@ public static void EqualMessageLists(List expectedMessages, 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")),
new ImageGenerationToolCallContent { ImageId = "img123" },
new ImageGenerationToolResultContent { ImageId = "img456", Outputs = [new DataContent(new byte[] { 4, 5, 6 }, "image/png")] }
]);
+ // Verify each element roundtrips individually
+ foreach (AIContent content in message.Contents)
+ {
+ var serializedElement = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions);
+ var deserializedElement = JsonSerializer.Deserialize(serializedElement, AIJsonUtilities.DefaultOptions);
+ Assert.NotNull(deserializedElement);
+ Assert.Equal(content.GetType(), deserializedElement.GetType());
+ }
+
var serialized = JsonSerializer.Serialize(message, AIJsonUtilities.DefaultOptions);
ChatMessage? deserialized = JsonSerializer.Deserialize(serialized, AIJsonUtilities.DefaultOptions);
Assert.NotNull(deserialized);
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalRequestContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalRequestContentTests.cs
index cc5cc1dd8d9..8028167d8ad 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalRequestContentTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalRequestContentTests.cs
@@ -15,9 +15,9 @@ public void Constructor_InvalidArguments_Throws()
{
FunctionCallContent functionCall = new("FCC1", "TestFunction");
- Assert.Throws("id", () => new FunctionApprovalRequestContent(null!, functionCall));
- Assert.Throws("id", () => new FunctionApprovalRequestContent("", functionCall));
- Assert.Throws("id", () => new FunctionApprovalRequestContent("\r\t\n ", functionCall));
+ Assert.Throws("requestId", () => new FunctionApprovalRequestContent(null!, functionCall));
+ Assert.Throws("requestId", () => new FunctionApprovalRequestContent("", functionCall));
+ Assert.Throws("requestId", () => new FunctionApprovalRequestContent("\r\t\n ", functionCall));
Assert.Throws("functionCall", () => new FunctionApprovalRequestContent("id", null!));
}
@@ -32,7 +32,7 @@ public void Constructor_Roundtrips(string id)
FunctionApprovalRequestContent content = new(id, functionCall);
- Assert.Same(id, content.Id);
+ Assert.Same(id, content.RequestId);
Assert.Same(functionCall, content.FunctionCall);
}
@@ -49,7 +49,7 @@ public void CreateResponse_ReturnsExpectedResponse(bool approved)
var response = content.CreateResponse(approved);
Assert.NotNull(response);
- Assert.Same(id, response.Id);
+ Assert.Same(id, response.RequestId);
Assert.Equal(approved, response.Approved);
Assert.Same(functionCall, response.FunctionCall);
Assert.Null(response.Reason);
@@ -70,7 +70,7 @@ public void CreateResponse_WithReason_ReturnsExpectedResponse(bool approved, str
var response = content.CreateResponse(approved, reason);
Assert.NotNull(response);
- Assert.Same(id, response.Id);
+ Assert.Same(id, response.RequestId);
Assert.Equal(approved, response.Approved);
Assert.Same(functionCall, response.FunctionCall);
Assert.Equal(reason, response.Reason);
@@ -81,13 +81,22 @@ public void Serialization_Roundtrips()
{
var content = new FunctionApprovalRequestContent("request123", new FunctionCallContent("call123", "functionName", new Dictionary { { "param1", 123 } }));
- var json = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions);
- var deserializedContent = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions);
-
- Assert.NotNull(deserializedContent);
- Assert.Equal(content.Id, deserializedContent.Id);
- Assert.NotNull(deserializedContent.FunctionCall);
- Assert.Equal(content.FunctionCall.CallId, deserializedContent.FunctionCall.CallId);
- Assert.Equal(content.FunctionCall.Name, deserializedContent.FunctionCall.Name);
+ AssertSerializationRoundtrips(content);
+ AssertSerializationRoundtrips(content);
+ AssertSerializationRoundtrips(content);
+
+ static void AssertSerializationRoundtrips(FunctionApprovalRequestContent content)
+ where T : AIContent
+ {
+ T contentAsT = (T)(object)content;
+ string json = JsonSerializer.Serialize(contentAsT, AIJsonUtilities.DefaultOptions);
+ T? deserialized = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions);
+ Assert.NotNull(deserialized);
+ var deserializedContent = Assert.IsType(deserialized);
+ Assert.Equal(content.RequestId, deserializedContent.RequestId);
+ Assert.NotNull(deserializedContent.FunctionCall);
+ Assert.Equal(content.FunctionCall.CallId, deserializedContent.FunctionCall.CallId);
+ Assert.Equal(content.FunctionCall.Name, deserializedContent.FunctionCall.Name);
+ }
}
}
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalResponseContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalResponseContentTests.cs
index 405955463a1..036ef83b65a 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalResponseContentTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalResponseContentTests.cs
@@ -14,9 +14,9 @@ public void Constructor_InvalidArguments_Throws()
{
FunctionCallContent functionCall = new("FCC1", "TestFunction");
- Assert.Throws("id", () => new FunctionApprovalResponseContent(null!, true, functionCall));
- Assert.Throws("id", () => new FunctionApprovalResponseContent("", true, functionCall));
- Assert.Throws("id", () => new FunctionApprovalResponseContent("\r\t\n ", true, functionCall));
+ Assert.Throws("requestId", () => new FunctionApprovalResponseContent(null!, true, functionCall));
+ Assert.Throws("requestId", () => new FunctionApprovalResponseContent("", true, functionCall));
+ Assert.Throws("requestId", () => new FunctionApprovalResponseContent("\r\t\n ", true, functionCall));
Assert.Throws("functionCall", () => new FunctionApprovalResponseContent("id", true, null!));
}
@@ -30,15 +30,44 @@ public void Constructor_Roundtrips(string id, bool approved)
FunctionCallContent functionCall = new("FCC1", "TestFunction");
FunctionApprovalResponseContent content = new(id, approved, functionCall);
- Assert.Same(id, content.Id);
+ Assert.Same(id, content.RequestId);
Assert.Equal(approved, content.Approved);
Assert.Same(functionCall, content.FunctionCall);
}
+ [Fact]
+ public void Serialization_Roundtrips()
+ {
+ var content = new FunctionApprovalResponseContent("request123", true, new FunctionCallContent("call123", "functionName"))
+ {
+ Reason = "Approved for testing"
+ };
+
+ AssertSerializationRoundtrips(content);
+ AssertSerializationRoundtrips(content);
+ AssertSerializationRoundtrips(content);
+
+ static void AssertSerializationRoundtrips(FunctionApprovalResponseContent content)
+ where T : AIContent
+ {
+ T contentAsT = (T)(object)content;
+ string json = JsonSerializer.Serialize(contentAsT, AIJsonUtilities.DefaultOptions);
+ T? deserialized = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions);
+ Assert.NotNull(deserialized);
+ var deserializedContent = Assert.IsType(deserialized);
+ Assert.Equal(content.RequestId, deserializedContent.RequestId);
+ Assert.Equal(content.Approved, deserializedContent.Approved);
+ Assert.Equal(content.Reason, deserializedContent.Reason);
+ Assert.NotNull(deserializedContent.FunctionCall);
+ Assert.Equal(content.FunctionCall.CallId, deserializedContent.FunctionCall.CallId);
+ Assert.Equal(content.FunctionCall.Name, deserializedContent.FunctionCall.Name);
+ }
+ }
+
[Theory]
[InlineData(null)]
[InlineData("Custom rejection reason")]
- public void Serialization_Roundtrips(string? reason)
+ public void Serialization_WithReason_Roundtrips(string? reason)
{
var content = new FunctionApprovalResponseContent("request123", true, new FunctionCallContent("call123", "functionName"))
{
@@ -49,7 +78,7 @@ public void Serialization_Roundtrips(string? reason)
var deserializedContent = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions);
Assert.NotNull(deserializedContent);
- Assert.Equal(content.Id, deserializedContent.Id);
+ Assert.Equal(content.RequestId, deserializedContent.RequestId);
Assert.Equal(content.Approved, deserializedContent.Approved);
Assert.Equal(content.Reason, deserializedContent.Reason);
Assert.NotNull(deserializedContent.FunctionCall);
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests.cs
index 9fde659d65f..0ca4db7a123 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests.cs
@@ -405,4 +405,60 @@ public static void CreateFromParsedArguments_NullInput_ThrowsArgumentNullExcepti
Assert.Throws("name", () => FunctionCallContent.CreateFromParsedArguments("{}", "callId", null!, _ => null));
Assert.Throws("argumentParser", () => FunctionCallContent.CreateFromParsedArguments("{}", "callId", "functionName", null!));
}
+
+ [Fact]
+ public void Serialization_Roundtrips()
+ {
+ var content = new FunctionCallContent("call123", "myFunction")
+ {
+ Arguments = new Dictionary { { "arg1", "value1" } }
+ };
+
+ AssertSerializationRoundtrips(content);
+ AssertSerializationRoundtrips(content);
+
+ static void AssertSerializationRoundtrips(FunctionCallContent content)
+ where T : AIContent
+ {
+ T contentAsT = (T)(object)content;
+ string json = JsonSerializer.Serialize(contentAsT, AIJsonUtilities.DefaultOptions);
+ T? deserialized = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions);
+ Assert.NotNull(deserialized);
+ var deserializedContent = Assert.IsType(deserialized);
+ Assert.Equal(content.CallId, deserializedContent.CallId);
+ Assert.Equal(content.Name, deserializedContent.Name);
+ Assert.NotNull(deserializedContent.Arguments);
+ Assert.Equal("value1", deserializedContent.Arguments["arg1"]?.ToString());
+ }
+ }
+
+ [Fact]
+ public void Serialization_DerivedTypes_Roundtrips()
+ {
+ FunctionCallContent[] contents =
+ [
+ new FunctionCallContent("call1", "function1", new Dictionary { { "param1", 123 } }),
+ new McpServerToolCallContent("call2", "myTool", "myServer"),
+ ];
+
+ // Verify each element roundtrips individually
+ foreach (var content in contents)
+ {
+ var serialized = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions);
+ var deserialized = JsonSerializer.Deserialize(serialized, AIJsonUtilities.DefaultOptions);
+ Assert.NotNull(deserialized);
+ Assert.Equal(content.GetType(), deserialized.GetType());
+ }
+
+ // Verify the array roundtrips
+ var serializedContents = JsonSerializer.Serialize(contents, TestJsonSerializerContext.Default.FunctionCallContentArray);
+ var deserializedContents = JsonSerializer.Deserialize(serializedContents, TestJsonSerializerContext.Default.FunctionCallContentArray);
+ Assert.NotNull(deserializedContents);
+ Assert.Equal(contents.Length, deserializedContents.Length);
+ for (int i = 0; i < deserializedContents.Length; i++)
+ {
+ Assert.NotNull(deserializedContents[i]);
+ Assert.Equal(contents[i].GetType(), deserializedContents[i].GetType());
+ }
+ }
}
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionResultContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionResultContentTests.cs
index 1542a4b823a..7bf2cdf9d6f 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionResultContentTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionResultContentTests.cs
@@ -91,4 +91,55 @@ public void ItShouldBeSerializableAndDeserializableWithException()
Assert.Equal(sut.Result, deserializedSut.Result?.ToString());
Assert.Null(deserializedSut.Exception);
}
+
+ [Fact]
+ public void Serialization_Roundtrips()
+ {
+ var content = new FunctionResultContent("call123", "result");
+
+ AssertSerializationRoundtrips(content);
+ AssertSerializationRoundtrips(content);
+
+ static void AssertSerializationRoundtrips(FunctionResultContent content)
+ where T : AIContent
+ {
+ T contentAsT = (T)(object)content;
+ string json = JsonSerializer.Serialize(contentAsT, AIJsonUtilities.DefaultOptions);
+ T? deserialized = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions);
+ Assert.NotNull(deserialized);
+ var deserializedContent = Assert.IsType(deserialized);
+ Assert.Equal(content.CallId, deserializedContent.CallId);
+ Assert.Equal("result", deserializedContent.Result?.ToString());
+ }
+ }
+
+ [Fact]
+ public void Serialization_DerivedTypes_Roundtrips()
+ {
+ FunctionResultContent[] contents =
+ [
+ new FunctionResultContent("call1", "result1"),
+ new McpServerToolResultContent("call2"),
+ ];
+
+ // Verify each element roundtrips individually
+ foreach (var content in contents)
+ {
+ var serialized = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions);
+ var deserialized = JsonSerializer.Deserialize(serialized, AIJsonUtilities.DefaultOptions);
+ Assert.NotNull(deserialized);
+ Assert.Equal(content.GetType(), deserialized.GetType());
+ }
+
+ // Verify the array roundtrips
+ var serializedContents = JsonSerializer.Serialize(contents, TestJsonSerializerContext.Default.FunctionResultContentArray);
+ var deserializedContents = JsonSerializer.Deserialize(serializedContents, TestJsonSerializerContext.Default.FunctionResultContentArray);
+ Assert.NotNull(deserializedContents);
+ Assert.Equal(contents.Length, deserializedContents.Length);
+ for (int i = 0; i < deserializedContents.Length; i++)
+ {
+ Assert.NotNull(deserializedContents[i]);
+ Assert.Equal(contents[i].GetType(), deserializedContents[i].GetType());
+ }
+ }
}
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputRequestContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputRequestContentTests.cs
new file mode 100644
index 00000000000..76636a5e879
--- /dev/null
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputRequestContentTests.cs
@@ -0,0 +1,69 @@
+// 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.Collections.Generic;
+using System.Text.Json;
+using Xunit;
+
+namespace Microsoft.Extensions.AI;
+
+public class InputRequestContentTests
+{
+ [Fact]
+ public void Constructor_InvalidArguments_Throws()
+ {
+ Assert.Throws("requestId", () => new TestInputRequestContent(null!));
+ Assert.Throws("requestId", () => new TestInputRequestContent(""));
+ Assert.Throws("requestId", () => new TestInputRequestContent("\r\t\n "));
+ }
+
+ [Theory]
+ [InlineData("abc")]
+ [InlineData("123")]
+ [InlineData("!@#")]
+ public void Constructor_Roundtrips(string id)
+ {
+ TestInputRequestContent content = new(id);
+
+ Assert.Equal(id, content.RequestId);
+ }
+
+ [Fact]
+ public void Serialization_DerivedTypes_Roundtrips()
+ {
+ InputRequestContent[] contents =
+ [
+ new FunctionApprovalRequestContent("request123", new FunctionCallContent("call123", "functionName", new Dictionary { { "param1", 123 } })),
+ new FunctionApprovalRequestContent("request456", new McpServerToolCallContent("call456", "myTool", "myServer")),
+ ];
+
+ // Verify each element roundtrips individually
+ foreach (var content in contents)
+ {
+ var serialized = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions);
+ var deserialized = JsonSerializer.Deserialize(serialized, AIJsonUtilities.DefaultOptions);
+ Assert.NotNull(deserialized);
+ Assert.Equal(content.GetType(), deserialized.GetType());
+ }
+
+ // Verify the array roundtrips
+ var serializedContents = JsonSerializer.Serialize(contents, TestJsonSerializerContext.Default.InputRequestContentArray);
+ var deserializedContents = JsonSerializer.Deserialize(serializedContents, TestJsonSerializerContext.Default.InputRequestContentArray);
+ Assert.NotNull(deserializedContents);
+ Assert.Equal(contents.Length, deserializedContents.Length);
+ for (int i = 0; i < deserializedContents.Length; i++)
+ {
+ Assert.NotNull(deserializedContents[i]);
+ Assert.Equal(contents[i].GetType(), deserializedContents[i].GetType());
+ }
+ }
+
+ private sealed class TestInputRequestContent : InputRequestContent
+ {
+ public TestInputRequestContent(string requestId)
+ : base(requestId)
+ {
+ }
+ }
+}
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputResponseContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputResponseContentTests.cs
new file mode 100644
index 00000000000..3a2b6411538
--- /dev/null
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputResponseContentTests.cs
@@ -0,0 +1,68 @@
+// 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.Text.Json;
+using Xunit;
+
+namespace Microsoft.Extensions.AI;
+
+public class InputResponseContentTests
+{
+ [Fact]
+ public void Constructor_InvalidArguments_Throws()
+ {
+ Assert.Throws("requestId", () => new TestInputResponseContent(null!));
+ Assert.Throws("requestId", () => new TestInputResponseContent(""));
+ Assert.Throws("requestId", () => new TestInputResponseContent("\r\t\n "));
+ }
+
+ [Theory]
+ [InlineData("abc")]
+ [InlineData("123")]
+ [InlineData("!@#")]
+ public void Constructor_Roundtrips(string id)
+ {
+ TestInputResponseContent content = new(id);
+
+ Assert.Equal(id, content.RequestId);
+ }
+
+ [Fact]
+ public void Serialization_DerivedTypes_Roundtrips()
+ {
+ InputResponseContent[] contents =
+ [
+ new FunctionApprovalResponseContent("request123", true, new FunctionCallContent("call123", "functionName")),
+ new FunctionApprovalResponseContent("request456", true, new McpServerToolCallContent("call456", "myTool", "myServer")),
+ ];
+
+ // Verify each element roundtrips individually
+ foreach (var content in contents)
+ {
+ var serialized = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions);
+ var deserialized = JsonSerializer.Deserialize(serialized, AIJsonUtilities.DefaultOptions);
+ Assert.NotNull(deserialized);
+ Assert.Equal(content.GetType(), deserialized.GetType());
+ }
+
+ // Verify the array roundtrips
+ var serializedContents = JsonSerializer.Serialize(contents, TestJsonSerializerContext.Default.InputResponseContentArray);
+ var deserializedContents = JsonSerializer.Deserialize(serializedContents, TestJsonSerializerContext.Default.InputResponseContentArray);
+ Assert.NotNull(deserializedContents);
+ Assert.Equal(contents.Length, deserializedContents.Length);
+ for (int i = 0; i < deserializedContents.Length; i++)
+ {
+ Assert.NotNull(deserializedContents[i]);
+ Assert.Equal(contents[i].GetType(), deserializedContents[i].GetType());
+ }
+ }
+
+ private class TestInputResponseContent : InputResponseContent
+ {
+ public TestInputResponseContent(string requestId)
+ : base(requestId)
+ {
+ }
+ }
+}
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..cc696618257 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolCallContentTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolCallContentTests.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Text.Json;
using Xunit;
namespace Microsoft.Extensions.AI;
@@ -18,9 +19,10 @@ public void Constructor_PropsDefault()
Assert.Null(c.AdditionalProperties);
Assert.Equal("callId1", c.CallId);
- Assert.Equal("toolName", c.ToolName);
+ Assert.Equal("toolName", c.Name);
Assert.Null(c.ServerName);
Assert.Null(c.Arguments);
+ Assert.True(c.InformationalOnly);
}
[Fact]
@@ -39,12 +41,16 @@ 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);
+ Assert.True(c.InformationalOnly);
+ c.InformationalOnly = false;
+ Assert.False(c.InformationalOnly);
+
Assert.Equal("callId1", c.CallId);
- Assert.Equal("toolName", c.ToolName);
+ Assert.Equal("toolName", c.Name);
Assert.Equal("serverName", c.ServerName);
}
@@ -52,9 +58,37 @@ public void Constructor_PropsRoundtrip()
public void Constructor_Throws()
{
Assert.Throws("callId", () => new McpServerToolCallContent(string.Empty, "name", null));
- Assert.Throws("toolName", () => new McpServerToolCallContent("callId1", string.Empty, null));
+ Assert.Throws("name", () => new McpServerToolCallContent("callId1", string.Empty, null));
Assert.Throws("callId", () => new McpServerToolCallContent(null!, "name", null));
- Assert.Throws("toolName", () => new McpServerToolCallContent("callId1", null!, null));
+ Assert.Throws("name", () => new McpServerToolCallContent("callId1", null!, null));
+ }
+
+ [Fact]
+ public void Serialization_Roundtrips()
+ {
+ var content = new McpServerToolCallContent("call123", "myTool", "myServer")
+ {
+ Arguments = new Dictionary { { "arg1", "value1" } }
+ };
+
+ AssertSerializationRoundtrips(content);
+ AssertSerializationRoundtrips(content);
+ AssertSerializationRoundtrips(content);
+
+ static void AssertSerializationRoundtrips(McpServerToolCallContent content)
+ where T : AIContent
+ {
+ T contentAsT = (T)(object)content;
+ string json = JsonSerializer.Serialize(contentAsT, AIJsonUtilities.DefaultOptions);
+ T? deserialized = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions);
+ Assert.NotNull(deserialized);
+ var deserializedContent = Assert.IsType(deserialized);
+ Assert.Equal(content.CallId, deserializedContent.CallId);
+ Assert.Equal(content.Name, deserializedContent.Name);
+ Assert.Equal(content.ServerName, deserializedContent.ServerName);
+ Assert.NotNull(deserializedContent.Arguments);
+ Assert.Equal("value1", deserializedContent.Arguments["arg1"]?.ToString());
+ }
}
}
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..d71a1a6243f 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolResultContentTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolResultContentTests.cs
@@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
-using System.Collections.Generic;
using System.Text.Json;
using Xunit;
@@ -17,7 +16,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 +36,10 @@ public void Constructor_PropsRoundtrip()
Assert.Equal("callId", c.CallId);
- Assert.Null(c.Output);
- IList output = [];
- c.Output = output;
- Assert.Same(output, c.Output);
+ Assert.Null(c.Result);
+ object result = "test result";
+ c.Result = result;
+ Assert.Same(result, c.Result);
}
[Fact]
@@ -55,14 +54,23 @@ public void Serialization_Roundtrips()
{
var content = new McpServerToolResultContent("call123")
{
- Output = new List { new TextContent("result") }
+ Result = "result"
};
- var json = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions);
- var deserializedContent = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions);
+ AssertSerializationRoundtrips(content);
+ AssertSerializationRoundtrips(content);
+ AssertSerializationRoundtrips(content);
- Assert.NotNull(deserializedContent);
- Assert.Equal(content.CallId, deserializedContent.CallId);
- Assert.NotNull(deserializedContent.Output);
+ static void AssertSerializationRoundtrips(McpServerToolResultContent content)
+ where T : AIContent
+ {
+ T contentAsT = (T)(object)content;
+ string json = JsonSerializer.Serialize(contentAsT, AIJsonUtilities.DefaultOptions);
+ T? deserialized = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions);
+ Assert.NotNull(deserialized);
+ var deserializedContent = Assert.IsType(deserialized);
+ Assert.Equal(content.CallId, deserializedContent.CallId);
+ Assert.Equal("result", ((JsonElement)deserializedContent.Result!).GetString());
+ }
}
}
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputRequestContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputRequestContentTests.cs
deleted file mode 100644
index fc4dac9cabb..00000000000
--- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputRequestContentTests.cs
+++ /dev/null
@@ -1,67 +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.Collections.Generic;
-using System.Linq;
-using System.Text.Json;
-using Xunit;
-
-namespace Microsoft.Extensions.AI.Contents;
-
-public class UserInputRequestContentTests
-{
- [Fact]
- public void Constructor_InvalidArguments_Throws()
- {
- Assert.Throws("id", () => new TestUserInputRequestContent(null!));
- Assert.Throws("id", () => new TestUserInputRequestContent(""));
- Assert.Throws("id", () => new TestUserInputRequestContent("\r\t\n "));
- }
-
- [Theory]
- [InlineData("abc")]
- [InlineData("123")]
- [InlineData("!@#")]
- public void Constructor_Roundtrips(string id)
- {
- TestUserInputRequestContent content = new(id);
-
- Assert.Equal(id, content.Id);
- }
-
- [Fact]
- public void Serialization_DerivedTypes_Roundtrips()
- {
- UserInputRequestContent 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);
- 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")),
- ];
-
- var serializedContents = JsonSerializer.Serialize(contents, TestJsonSerializerContext.Default.UserInputRequestContentArray);
- var deserializedContents = JsonSerializer.Deserialize(serializedContents, TestJsonSerializerContext.Default.UserInputRequestContentArray);
- Assert.NotNull(deserializedContents);
-
- Assert.Equal(contents.Count(), deserializedContents.Length);
- for (int i = 0; i < deserializedContents.Length; i++)
- {
- Assert.NotNull(contents.ElementAt(i));
- Assert.Equal(contents.ElementAt(i).GetType(), deserializedContents[i].GetType());
- }
- }
-
- private sealed class TestUserInputRequestContent : UserInputRequestContent
- {
- public TestUserInputRequestContent(string id)
- : base(id)
- {
- }
- }
-}
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputResponseContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputResponseContentTests.cs
deleted file mode 100644
index 2442e57272d..00000000000
--- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputResponseContentTests.cs
+++ /dev/null
@@ -1,65 +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.Text.Json;
-using Xunit;
-
-namespace Microsoft.Extensions.AI.Contents;
-
-public class UserInputResponseContentTests
-{
- [Fact]
- public void Constructor_InvalidArguments_Throws()
- {
- Assert.Throws("id", () => new TestUserInputResponseContent(null!));
- Assert.Throws("id", () => new TestUserInputResponseContent(""));
- Assert.Throws("id", () => new TestUserInputResponseContent("\r\t\n "));
- }
-
- [Theory]
- [InlineData("abc")]
- [InlineData("123")]
- [InlineData("!@#")]
- public void Constructor_Roundtrips(string id)
- {
- TestUserInputResponseContent content = new(id);
-
- Assert.Equal(id, content.Id);
- }
-
- [Fact]
- public void Serialization_DerivedTypes_Roundtrips()
- {
- UserInputResponseContent content = new FunctionApprovalResponseContent("request123", true, new FunctionCallContent("call123", "functionName"));
- var serializedContent = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions);
- var deserializedContent = JsonSerializer.Deserialize(serializedContent, AIJsonUtilities.DefaultOptions);
- Assert.NotNull(deserializedContent);
- Assert.Equal(content.GetType(), deserializedContent.GetType());
-
- UserInputResponseContent[] contents =
- [
- new FunctionApprovalResponseContent("request123", true, new FunctionCallContent("call123", "functionName")),
- new McpServerToolApprovalResponseContent("request123", true),
- ];
-
- var serializedContents = JsonSerializer.Serialize(contents, TestJsonSerializerContext.Default.UserInputResponseContentArray);
- var deserializedContents = JsonSerializer.Deserialize(serializedContents, TestJsonSerializerContext.Default.UserInputResponseContentArray);
- Assert.NotNull(deserializedContents);
-
- Assert.Equal(contents.Length, deserializedContents.Length);
- for (int i = 0; i < deserializedContents.Length; i++)
- {
- Assert.NotNull(contents[i]);
- Assert.Equal(contents[i].GetType(), deserializedContents[i].GetType());
- }
- }
-
- private class TestUserInputResponseContent : UserInputResponseContent
- {
- public TestUserInputResponseContent(string id)
- : base(id)
- {
- }
- }
-}
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs
index 6c448d0efb1..0437c47eb1c 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs
@@ -39,8 +39,10 @@ namespace Microsoft.Extensions.AI;
[JsonSerializable(typeof(ChatResponseFormatTests.SomeType))]
[JsonSerializable(typeof(ChatResponseFormatTests.TypeWithDisplayName))]
[JsonSerializable(typeof(ResponseContinuationToken))]
-[JsonSerializable(typeof(UserInputRequestContent[]))]
-[JsonSerializable(typeof(UserInputResponseContent[]))]
+[JsonSerializable(typeof(InputRequestContent[]))]
+[JsonSerializable(typeof(InputResponseContent[]))]
+[JsonSerializable(typeof(FunctionCallContent[]))]
+[JsonSerializable(typeof(FunctionResultContent[]))]
[JsonSerializable(typeof(ReasoningOptions))]
[JsonSerializable(typeof(ReasoningEffort))]
[JsonSerializable(typeof(ReasoningOutput))]
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs
index 56c04ce1dfa..454fd74a731 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs
@@ -20,12 +20,10 @@ public void Constructor_PropsDefault()
Assert.Equal("https://localhost/", tool.ServerAddress);
Assert.Empty(tool.Description);
- Assert.Null(tool.AuthorizationToken);
Assert.Null(tool.ServerDescription);
Assert.Null(tool.AllowedTools);
Assert.Null(tool.ApprovalMode);
- Assert.NotNull(tool.Headers);
- Assert.Empty(tool.Headers);
+ Assert.Null(tool.Headers);
}
[Fact]
@@ -72,11 +70,6 @@ public void Constructor_Roundtrips()
Assert.Equal("connector_id", tool.ServerAddress);
Assert.Empty(tool.Description);
- Assert.Null(tool.AuthorizationToken);
- string authToken = "Bearer token123";
- tool.AuthorizationToken = authToken;
- Assert.Equal(authToken, tool.AuthorizationToken);
-
Assert.Null(tool.ServerDescription);
string serverDescription = "This is a test server";
tool.ServerDescription = serverDescription;
@@ -98,92 +91,33 @@ public void Constructor_Roundtrips()
tool.ApprovalMode = customApprovalMode;
Assert.Same(customApprovalMode, tool.ApprovalMode);
+ Assert.Null(tool.Headers);
+ tool.Headers = new Dictionary { ["X-Custom-Header"] = "value1" };
Assert.NotNull(tool.Headers);
Assert.Single(tool.Headers);
- tool.Headers["X-Custom-Header"] = "value1";
- Assert.True(tool.Headers.Count == 2);
Assert.Equal("value1", tool.Headers["X-Custom-Header"]);
}
[Fact]
public void Constructor_WithHeaders_Uri_Roundtrips()
{
- var headers = new Dictionary
+ HostedMcpServerTool tool = new("serverName", new Uri("https://localhost/"))
{
- ["Authorization"] = "Bearer token456",
- ["X-Custom"] = "value2"
+ Headers = new Dictionary
+ {
+ ["Authorization"] = "Bearer token456",
+ ["X-Custom"] = "value2"
+ }
};
- HostedMcpServerTool tool = new("serverName", new Uri("https://localhost/"));
- foreach (KeyValuePair keyValuePair in headers)
- {
- tool.Headers[keyValuePair.Key] = keyValuePair.Value;
- }
Assert.Equal("serverName", tool.ServerName);
Assert.Equal("https://localhost/", tool.ServerAddress);
+ Assert.NotNull(tool.Headers);
Assert.Equal(2, tool.Headers.Count);
Assert.Equal("Bearer token456", tool.Headers["Authorization"]);
- Assert.Equal("token456", tool.AuthorizationToken);
Assert.Equal("value2", tool.Headers["X-Custom"]);
}
- [Fact]
- public void Constructor_WithNullHeaders_CreatesEmptyDictionary()
- {
- HostedMcpServerTool tool1 = new("serverName", "connector_id");
- Assert.NotNull(tool1.Headers);
- Assert.Empty(tool1.Headers);
-
- HostedMcpServerTool tool2 = new("serverName", new Uri("https://localhost/"));
- Assert.NotNull(tool2.Headers);
- Assert.Empty(tool2.Headers);
- }
-
- [Fact]
- public void AuthorizationToken_And_Headers_NoOrderingIssues()
- {
- // Verify that setting AuthorizationToken followed by adding to Headers works
- var tool1 = new HostedMcpServerTool("server", "https://localhost/")
- {
- AuthorizationToken = "token123"
- };
- tool1.Headers["X-Custom"] = "value1";
-
- Assert.Equal(2, tool1.Headers.Count);
- Assert.Equal("Bearer token123", tool1.Headers["Authorization"]);
- Assert.Equal("token123", tool1.AuthorizationToken);
- Assert.Equal("value1", tool1.Headers["X-Custom"]);
-
- // Verify that adding to Headers followed by setting AuthorizationToken works the same
- var tool2 = new HostedMcpServerTool("server", "https://localhost/");
- tool2.Headers["X-Custom"] = "value1";
- tool2.AuthorizationToken = "token123";
-
- Assert.Equal(2, tool2.Headers.Count);
- Assert.Equal("Bearer token123", tool2.Headers["Authorization"]);
- Assert.Equal("token123", tool2.AuthorizationToken);
- Assert.Equal("value1", tool2.Headers["X-Custom"]);
-
- // Verify setting AuthorizationToken to null removes only Authorization header
- tool2.AuthorizationToken = null;
- Assert.Single(tool2.Headers);
- Assert.False(tool2.Headers.ContainsKey("Authorization"));
- Assert.Null(tool2.AuthorizationToken);
- Assert.Equal("value1", tool2.Headers["X-Custom"]);
- }
-
- [Fact]
- public void Headers_WithNullAuthorization()
- {
- var tool = new HostedMcpServerTool("server", "https://localhost/");
- tool.Headers["Authorization"] = null!;
- tool.Headers["X-Custom"] = "value1";
- Assert.Equal(2, tool.Headers.Count);
- Assert.Null(tool.Headers["Authorization"]);
- Assert.Null(tool.AuthorizationToken);
- Assert.Equal("value1", tool.Headers["X-Custom"]);
- }
-
[Fact]
public void Constructor_Throws()
{
@@ -193,9 +127,9 @@ public void Constructor_Throws()
Assert.Throws("serverName", () => new HostedMcpServerTool(null!, new Uri("https://localhost/")));
Assert.Throws("serverAddress", () => new HostedMcpServerTool("name", string.Empty));
- Assert.Throws("serverUrl", () => new HostedMcpServerTool("name", new Uri("/api/mcp", UriKind.Relative)));
+ Assert.Throws("serverAddress", () => new HostedMcpServerTool("name", new Uri("/api/mcp", UriKind.Relative)));
Assert.Throws("serverAddress", () => new HostedMcpServerTool("name", (string)null!));
- Assert.Throws("serverUrl", () => new HostedMcpServerTool("name", (Uri)null!));
+ Assert.Throws("serverAddress", () => new HostedMcpServerTool("name", (Uri)null!));
}
}
diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs
index d2e4dd39867..3daf4796bad 100644
--- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs
@@ -431,7 +431,7 @@ public void AsOpenAIResponseTool_WithHostedMcpServerToolWithAuthToken_ProducesVa
{
var mcpTool = new HostedMcpServerTool("test-server", "http://localhost:8000")
{
- AuthorizationToken = "test-token"
+ Headers = new Dictionary { ["Authorization"] = "Bearer test-token" }
};
var result = mcpTool.AsOpenAIResponseTool();
@@ -449,9 +449,12 @@ public void AsOpenAIResponseTool_WithHostedMcpServerToolWithAuthTokenAndCustomHe
{
var mcpTool = new HostedMcpServerTool("test-server", "http://localhost:8000")
{
- AuthorizationToken = "test-token"
+ Headers = new Dictionary
+ {
+ ["Authorization"] = "Bearer test-token",
+ ["X-Custom-Header"] = "custom-value"
+ }
};
- mcpTool.Headers["X-Custom-Header"] = "custom-value";
var result = mcpTool.AsOpenAIResponseTool();
@@ -559,11 +562,11 @@ public void AsOpenAIResponseTool_WithHostedMcpServerToolWithRequireSpecificAppro
}
[Fact]
- public void AsOpenAIResponseTool_WithHostedMcpServerToolConnector_OnlySetsAuthToken()
+ public void AsOpenAIResponseTool_WithHostedMcpServerToolConnector_ExtractsAuthToken()
{
var mcpTool = new HostedMcpServerTool("calendar", "connector_googlecalendar")
{
- AuthorizationToken = "connector-token"
+ Headers = new Dictionary { ["Authorization"] = "Bearer connector-token" }
};
var result = mcpTool.AsOpenAIResponseTool();
@@ -572,7 +575,7 @@ public void AsOpenAIResponseTool_WithHostedMcpServerToolConnector_OnlySetsAuthTo
var tool = Assert.IsType(result);
Assert.Equal("connector-token", tool.AuthorizationToken);
- // For connectors, headers should not be set even though AuthorizationToken adds to Headers internally
+ // For connectors, headers should not be set - only AuthorizationToken
Assert.Empty(tool.Headers);
}
@@ -880,13 +883,126 @@ public void AsChatResponse_ConvertsOpenAIResponse()
// as all constructors/factory methods currently are internal. Update this test when such functionality is available.
}
+ ///
+ /// Derived type to allow creating StreamingResponseOutputItemDoneUpdate instances for testing.
+ /// The base class has internal constructors, but we can derive and set the Item property.
+ ///
+ private sealed class TestableStreamingResponseOutputItemDoneUpdate : StreamingResponseOutputItemDoneUpdate
+ {
+ }
+
[Fact]
- public void AsChatResponseUpdatesAsync_ConvertsOpenAIStreamingResponseUpdates()
+ public async Task AsChatResponseUpdatesAsync_ConvertsOpenAIStreamingResponseUpdates()
{
Assert.Throws("responseUpdates", () => ((IAsyncEnumerable)null!).AsChatResponseUpdatesAsync());
- // The OpenAI library currently doesn't provide any way to create a StreamingResponseUpdate instance,
- // as all constructors/factory methods currently are internal. Update this test when such functionality is available.
+ // Create streaming updates with various ResponseItem types
+ FunctionCallResponseItem functionCall = ResponseItem.CreateFunctionCallItem("call_abc", "MyFunction", BinaryData.FromString("""{"arg":"value"}"""));
+ McpToolCallItem mcpToolCall = ResponseItem.CreateMcpToolCallItem("deepwiki", "ask_question", BinaryData.FromString("""{"query":"hello"}"""));
+ mcpToolCall.Id = "mcp_call_123";
+ mcpToolCall.ToolOutput = "The answer is 42";
+ McpToolCallApprovalRequestItem mcpApprovalRequest = ResponseItem.CreateMcpApprovalRequestItem(
+ "mcpr_123",
+ "deepwiki",
+ "ask_question",
+ BinaryData.FromString("""{"repo":"dotnet/extensions"}"""));
+ McpToolCallApprovalResponseItem mcpApprovalResponse = ResponseItem.CreateMcpApprovalResponseItem("mcpr_123", approved: true);
+
+ List updates = [];
+ await foreach (ChatResponseUpdate update in CreateStreamingUpdates().AsChatResponseUpdatesAsync())
+ {
+ updates.Add(update);
+ }
+
+ // Verify we got the expected updates
+ Assert.Equal(4, updates.Count);
+
+ // First update should be FunctionCallContent
+ FunctionCallContent? fcc = updates[0].Contents.OfType().FirstOrDefault();
+ Assert.NotNull(fcc);
+ Assert.Equal("call_abc", fcc.CallId);
+ Assert.Equal("MyFunction", fcc.Name);
+
+ // Second update should be McpServerToolCallContent + McpServerToolResultContent
+ McpServerToolCallContent? mcpToolCallContent = updates[1].Contents.OfType().FirstOrDefault();
+ Assert.NotNull(mcpToolCallContent);
+ Assert.Equal("mcp_call_123", mcpToolCallContent.CallId);
+ Assert.Equal("ask_question", mcpToolCallContent.Name);
+ Assert.Equal("deepwiki", mcpToolCallContent.ServerName);
+ Assert.Null(mcpToolCallContent.RawRepresentation); // Intentionally null to avoid duplication during roundtrip
+
+ McpServerToolResultContent? mcpToolResultContent = updates[1].Contents.OfType().FirstOrDefault();
+ Assert.NotNull(mcpToolResultContent);
+ Assert.Equal("mcp_call_123", mcpToolResultContent.CallId);
+ Assert.NotNull(mcpToolResultContent.RawRepresentation);
+ Assert.Same(mcpToolCall, mcpToolResultContent.RawRepresentation);
+
+ // Third update should be FunctionApprovalRequestContent with McpServerToolCallContent
+ FunctionApprovalRequestContent? approvalRequest = updates[2].Contents.OfType().FirstOrDefault();
+ Assert.NotNull(approvalRequest);
+ Assert.Equal("mcpr_123", approvalRequest.RequestId);
+ Assert.NotNull(approvalRequest.RawRepresentation);
+ Assert.Same(mcpApprovalRequest, approvalRequest.RawRepresentation);
+
+ McpServerToolCallContent nestedMcpCall = Assert.IsType(approvalRequest.FunctionCall);
+ Assert.Equal("ask_question", nestedMcpCall.Name);
+ Assert.Equal("deepwiki", nestedMcpCall.ServerName);
+
+ // Fourth update should be FunctionApprovalResponseContent correlated with request
+ FunctionApprovalResponseContent? approvalResponse = updates[3].Contents.OfType().FirstOrDefault();
+ Assert.NotNull(approvalResponse);
+ Assert.Equal("mcpr_123", approvalResponse.RequestId);
+ Assert.True(approvalResponse.Approved);
+ Assert.NotNull(approvalResponse.RawRepresentation);
+ Assert.Same(mcpApprovalResponse, approvalResponse.RawRepresentation);
+
+ // The correlated FunctionCall should be McpServerToolCallContent with tool details from the request
+ McpServerToolCallContent correlatedMcpCall = Assert.IsType(approvalResponse.FunctionCall);
+ Assert.Equal("mcpr_123", correlatedMcpCall.CallId);
+ Assert.Equal("ask_question", correlatedMcpCall.Name);
+ Assert.Equal("deepwiki", correlatedMcpCall.ServerName);
+ Assert.NotNull(correlatedMcpCall.Arguments);
+ Assert.Equal("dotnet/extensions", correlatedMcpCall.Arguments["repo"]?.ToString());
+
+ async IAsyncEnumerable CreateStreamingUpdates()
+ {
+ await Task.Yield();
+
+ yield return new TestableStreamingResponseOutputItemDoneUpdate { Item = functionCall };
+ yield return new TestableStreamingResponseOutputItemDoneUpdate { Item = mcpToolCall };
+ yield return new TestableStreamingResponseOutputItemDoneUpdate { Item = mcpApprovalRequest };
+ yield return new TestableStreamingResponseOutputItemDoneUpdate { Item = mcpApprovalResponse };
+ }
+ }
+
+ [Fact]
+ public async Task AsChatResponseUpdatesAsync_McpToolCallApprovalResponseItem_WithoutCorrelatedRequest_FallsBackToAIContent()
+ {
+ // Create an approval response without a matching request in the stream.
+ McpToolCallApprovalResponseItem mcpApprovalResponse = ResponseItem.CreateMcpApprovalResponseItem("unknown_request_id", approved: true);
+
+ List updates = [];
+ await foreach (ChatResponseUpdate update in CreateStreamingUpdates().AsChatResponseUpdatesAsync())
+ {
+ updates.Add(update);
+ }
+
+ Assert.Single(updates);
+
+ // Should NOT have a FunctionApprovalResponseContent since there was no correlated request
+ Assert.Empty(updates[0].Contents.OfType());
+
+ // Should have a generic AIContent with RawRepresentation set to the response item
+ AIContent? genericContent = updates[0].Contents.FirstOrDefault(c => c.RawRepresentation == mcpApprovalResponse);
+ Assert.NotNull(genericContent);
+ Assert.IsNotType(genericContent);
+ Assert.Same(mcpApprovalResponse, genericContent.RawRepresentation);
+
+ async IAsyncEnumerable CreateStreamingUpdates()
+ {
+ await Task.Yield();
+ yield return new TestableStreamingResponseOutputItemDoneUpdate { Item = mcpApprovalResponse };
+ }
}
[Fact]
@@ -976,6 +1092,171 @@ public void AsChatMessages_FromResponseItems_WithFunctionCall_HandlesCorrectly()
Assert.Equal("value", functionCall.Arguments!["param"]?.ToString());
}
+ [Fact]
+ public void AsChatMessages_FromResponseItems_AllContentTypes_SetsRawRepresentation()
+ {
+ // Create ResponseItems of various types that ToChatMessages handles.
+ // Each type should roundtrip with RawRepresentation set.
+ MessageResponseItem assistantItem = ResponseItem.CreateAssistantMessageItem("Hello from the assistant!");
+ ReasoningResponseItem reasoningItem = ResponseItem.CreateReasoningItem("This is reasoning text");
+ FunctionCallResponseItem functionCallItem = ResponseItem.CreateFunctionCallItem("call_abc", "MyFunction", BinaryData.FromString("""{"arg": "value"}"""));
+ FunctionCallOutputResponseItem functionOutputItem = ResponseItem.CreateFunctionCallOutputItem("call_abc", "function result output");
+ McpToolCallItem mcpToolCallItem = ResponseItem.CreateMcpToolCallItem("deepwiki", "ask_question", BinaryData.FromString("""{"query":"hello"}"""));
+ mcpToolCallItem.Id = "mcp_call_123";
+ mcpToolCallItem.ToolOutput = "The answer is 42";
+ McpToolCallApprovalRequestItem mcpApprovalRequestItem = ResponseItem.CreateMcpApprovalRequestItem(
+ "mcpr_123",
+ "deepwiki",
+ "ask_question",
+ BinaryData.FromString("""{"repoName":"dotnet/extensions"}"""));
+
+ // Use matching ID so response can correlate with the request
+ McpToolCallApprovalResponseItem mcpApprovalResponseItem = ResponseItem.CreateMcpApprovalResponseItem("mcpr_123", approved: true);
+
+ ResponseItem[] items = [assistantItem, reasoningItem, functionCallItem, functionOutputItem, mcpToolCallItem, mcpApprovalRequestItem, mcpApprovalResponseItem];
+
+ // Convert to ChatMessages
+ ChatMessage[] messages = items.AsChatMessages().ToArray();
+
+ // All items should be grouped into a single assistant message
+ Assert.Single(messages);
+ ChatMessage message = messages[0];
+ Assert.Equal(ChatRole.Assistant, message.Role);
+
+ // The message itself should have RawRepresentation from MessageResponseItem
+ Assert.NotNull(message.RawRepresentation);
+ Assert.Same(assistantItem, message.RawRepresentation);
+
+ // Verify each content type has RawRepresentation set
+
+ // 1. MessageResponseItem -> TextContent with ResponseContentPart as RawRepresentation
+ TextContent? textContent = message.Contents.OfType().FirstOrDefault();
+ Assert.NotNull(textContent);
+ Assert.Equal("Hello from the assistant!", textContent.Text);
+ Assert.NotNull(textContent.RawRepresentation);
+ Assert.IsAssignableFrom(textContent.RawRepresentation);
+
+ // 2. ReasoningResponseItem -> TextReasoningContent
+ TextReasoningContent? reasoningContent = message.Contents.OfType().FirstOrDefault();
+ Assert.NotNull(reasoningContent);
+ Assert.Equal("This is reasoning text", reasoningContent.Text);
+ Assert.NotNull(reasoningContent.RawRepresentation);
+ Assert.Same(reasoningItem, reasoningContent.RawRepresentation);
+
+ // 3. FunctionCallResponseItem -> FunctionCallContent
+ FunctionCallContent? functionCallContent = message.Contents.OfType().FirstOrDefault();
+ Assert.NotNull(functionCallContent);
+ Assert.Equal("call_abc", functionCallContent.CallId);
+ Assert.Equal("MyFunction", functionCallContent.Name);
+ Assert.NotNull(functionCallContent.RawRepresentation);
+ Assert.Same(functionCallItem, functionCallContent.RawRepresentation);
+
+ // 4. FunctionCallOutputResponseItem -> FunctionResultContent
+ FunctionResultContent? functionResultContent = message.Contents.OfType().FirstOrDefault();
+ Assert.NotNull(functionResultContent);
+ Assert.Equal("call_abc", functionResultContent.CallId);
+ Assert.Equal("function result output", functionResultContent.Result);
+ Assert.NotNull(functionResultContent.RawRepresentation);
+ Assert.Same(functionOutputItem, functionResultContent.RawRepresentation);
+
+ // 5. McpToolCallItem -> McpServerToolCallContent + McpServerToolResultContent
+ // Note: AddMcpToolCallContent creates both contents; RawRepresentation is only on the result, not the call
+ McpServerToolCallContent? mcpToolCall = message.Contents.OfType().FirstOrDefault(c => c.CallId == "mcp_call_123");
+ Assert.NotNull(mcpToolCall);
+ Assert.Equal("mcp_call_123", mcpToolCall.CallId);
+ Assert.Equal("ask_question", mcpToolCall.Name);
+ Assert.Equal("deepwiki", mcpToolCall.ServerName);
+ Assert.Null(mcpToolCall.RawRepresentation); // Intentionally null to avoid duplication during roundtrip
+
+ McpServerToolResultContent? mcpToolResult = message.Contents.OfType().FirstOrDefault(c => c.CallId == "mcp_call_123");
+ Assert.NotNull(mcpToolResult);
+ Assert.Equal("mcp_call_123", mcpToolResult.CallId);
+ Assert.NotNull(mcpToolResult.RawRepresentation);
+ Assert.Same(mcpToolCallItem, mcpToolResult.RawRepresentation);
+
+ // 6. McpToolCallApprovalRequestItem -> FunctionApprovalRequestContent
+ FunctionApprovalRequestContent? approvalRequestContent = message.Contents.OfType().FirstOrDefault();
+ Assert.NotNull(approvalRequestContent);
+ Assert.Equal("mcpr_123", approvalRequestContent.RequestId);
+ Assert.NotNull(approvalRequestContent.RawRepresentation);
+ Assert.Same(mcpApprovalRequestItem, approvalRequestContent.RawRepresentation);
+
+ // The nested FunctionCall should be McpServerToolCallContent
+ McpServerToolCallContent nestedMcpCall = Assert.IsType(approvalRequestContent.FunctionCall);
+ Assert.Equal("ask_question", nestedMcpCall.Name);
+ Assert.Equal("deepwiki", nestedMcpCall.ServerName);
+ Assert.NotNull(nestedMcpCall.RawRepresentation);
+ Assert.Same(mcpApprovalRequestItem, nestedMcpCall.RawRepresentation);
+
+ // 7. McpToolCallApprovalResponseItem -> FunctionApprovalResponseContent (correlated with request)
+ FunctionApprovalResponseContent? approvalResponseContent = message.Contents.OfType().FirstOrDefault();
+ Assert.NotNull(approvalResponseContent);
+ Assert.Equal("mcpr_123", approvalResponseContent.RequestId);
+ Assert.True(approvalResponseContent.Approved);
+ Assert.NotNull(approvalResponseContent.RawRepresentation);
+ Assert.Same(mcpApprovalResponseItem, approvalResponseContent.RawRepresentation);
+
+ // The correlated FunctionCall should be McpServerToolCallContent with tool details from the request
+ McpServerToolCallContent correlatedMcpCall = Assert.IsType(approvalResponseContent.FunctionCall);
+ Assert.Equal("mcpr_123", correlatedMcpCall.CallId);
+ Assert.Equal("ask_question", correlatedMcpCall.Name);
+ Assert.Equal("deepwiki", correlatedMcpCall.ServerName);
+ Assert.NotNull(correlatedMcpCall.Arguments);
+ Assert.Equal("dotnet/extensions", correlatedMcpCall.Arguments["repoName"]?.ToString());
+ }
+
+ [Fact]
+ public void AsChatMessages_McpToolCallApprovalResponseItem_WithoutCorrelatedRequest_FallsBackToAIContent()
+ {
+ // Create an approval response without a matching request in the batch.
+ // This simulates receiving a response item when we don't have the original request.
+ MessageResponseItem assistantItem = ResponseItem.CreateAssistantMessageItem("Hello");
+ McpToolCallApprovalResponseItem mcpApprovalResponseItem = ResponseItem.CreateMcpApprovalResponseItem("unknown_request_id", approved: true);
+
+ ResponseItem[] items = [assistantItem, mcpApprovalResponseItem];
+
+ // Convert to ChatMessages
+ ChatMessage[] messages = items.AsChatMessages().ToArray();
+
+ Assert.Single(messages);
+ ChatMessage message = messages[0];
+
+ // Should NOT have a FunctionApprovalResponseContent since there was no correlated request
+ Assert.Empty(message.Contents.OfType());
+
+ // Should have a generic AIContent with RawRepresentation set to the response item
+ AIContent? genericContent = message.Contents.FirstOrDefault(c => c.RawRepresentation == mcpApprovalResponseItem);
+ Assert.NotNull(genericContent);
+ Assert.IsNotType(genericContent);
+ Assert.Same(mcpApprovalResponseItem, genericContent.RawRepresentation);
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void AsOpenAIResponseItems_McpServerToolContents_RoundtripsToolOutputAndError(bool isError)
+ {
+ var mcpToolCall = new McpServerToolCallContent("call_123", "get_weather", "weather_server") { Arguments = new Dictionary { ["city"] = "Seattle" } };
+ var mcpToolResult = new McpServerToolResultContent("call_123") { Result = isError ? new ErrorContent("error") : new TextContent("sunny") };
+
+ var items = new ChatMessage[] { new(ChatRole.Assistant, [mcpToolCall, mcpToolResult]) }.AsOpenAIResponseItems().ToArray();
+
+ McpToolCallItem mtci = Assert.IsType(Assert.Single(items));
+ Assert.Equal("get_weather", mtci.ToolName);
+ Assert.Equal("weather_server", mtci.ServerLabel);
+
+ if (isError)
+ {
+ Assert.Contains("error", mtci.Error?.ToString());
+ Assert.Null(mtci.ToolOutput);
+ }
+ else
+ {
+ Assert.Equal("sunny", mtci.ToolOutput);
+ Assert.Null(mtci.Error);
+ }
+ }
+
[Fact]
public void AsOpenAIChatCompletion_WithNullArgument_ThrowsArgumentNullException()
{
diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs
index 0bf3f7fcf0f..e1965468cde 100644
--- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs
@@ -149,7 +149,7 @@ 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);
}
@@ -203,8 +203,8 @@ await client.GetStreamingResponseAsync(input, chatOptions).ToChatResponseAsync()
var approvalResponse = new ChatMessage(ChatRole.Tool,
response.Messages
.SelectMany(m => m.Contents)
- .OfType()
- .Select(c => new McpServerToolApprovalResponseContent(c.ToolCall.CallId, true))
+ .OfType()
+ .Select(c => c.CreateResponse(true))
.ToArray());
if (approvalResponse.Contents.Count == 0)
{
@@ -377,7 +377,7 @@ public async Task RemoteMCP_Connector()
{
SkipIfNotEnabled();
- if (TestRunnerConfiguration.Instance["RemoteMCP:ConnectorAccessToken"] is not string accessToken)
+ if (TestRunnerConfiguration.Instance["RemoteMCP:ConnectorAccessToken"] is not string { Length: > 0 } accessToken)
{
throw new SkipTestException(
"To run this test, set a value for RemoteMCP:ConnectorAccessToken. " +
@@ -394,9 +394,9 @@ async Task RunAsync(bool streaming, bool approval)
Tools = [new HostedMcpServerTool("calendar", "connector_googlecalendar")
{
ApprovalMode = approval ?
- HostedMcpServerToolApprovalMode.AlwaysRequire :
- HostedMcpServerToolApprovalMode.NeverRequire,
- AuthorizationToken = accessToken
+ HostedMcpServerToolApprovalMode.AlwaysRequire :
+ HostedMcpServerToolApprovalMode.NeverRequire,
+ Headers = new Dictionary { ["Authorization"] = $"Bearer {accessToken}" },
}
],
};
@@ -412,8 +412,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.Name);
input.Add(new ChatMessage(ChatRole.Tool, [approvalRequest.CreateResponse(true)]));
response = streaming ?
@@ -423,10 +424,10 @@ await client.GetStreamingResponseAsync(input, chatOptions).ToChatResponseAsync()
Assert.NotNull(response);
var toolCall = Assert.Single(response.Messages.SelectMany(m => m.Contents).OfType());
- Assert.Equal("search_events", toolCall.ToolName);
+ Assert.Equal("search_events", toolCall.Name);
var toolResult = Assert.Single(response.Messages.SelectMany(m => m.Contents).OfType());
- var content = Assert.IsType(Assert.Single(toolResult.Output!));
+ var content = Assert.IsType(toolResult.Result);
Assert.Equal(@"{""events"": [], ""next_page_token"": null}", content.Text);
}
}
diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs
index abd1414d85f..6193d58b291 100644
--- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs
@@ -1396,7 +1396,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))
@@ -1406,7 +1406,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;
}
@@ -1543,7 +1543,7 @@ public async Task McpToolCall_ApprovalRequired_NonStreaming(string role)
var call = Assert.IsType(message.Contents[0]);
Assert.Equal("mcp_06ee3b1962eeb8470068e6b21cbaa081a3b5aa2a6c989f4c6f", call.CallId);
Assert.Equal("deepwiki", call.ServerName);
- Assert.Equal("ask_question", call.ToolName);
+ Assert.Equal("ask_question", call.Name);
Assert.NotNull(call.Arguments);
Assert.Equal(2, call.Arguments.Count);
Assert.Equal("dotnet/extensions", ((JsonElement)call.Arguments["repoName"]!).GetString());
@@ -1551,8 +1551,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).Text);
Assert.NotNull(response.Usage);
Assert.Equal(542, response.Usage.InputTokenCount);
@@ -1798,28 +1797,26 @@ public async Task McpToolCall_ApprovalNotRequired_NonStreaming(bool rawTool)
var firstCall = Assert.IsType(message.Contents[1]);
Assert.Equal("mcp_68be4166acfc8191bc5e0a751eed358b0384f747588fc3f5", firstCall.CallId);
Assert.Equal("deepwiki", firstCall.ServerName);
- Assert.Equal("read_wiki_structure", firstCall.ToolName);
+ Assert.Equal("read_wiki_structure", firstCall.Name);
Assert.NotNull(firstCall.Arguments);
Assert.Single(firstCall.Arguments);
Assert.Equal("dotnet/extensions", ((JsonElement)firstCall.Arguments["repoName"]!).GetString());
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).Text);
var secondCall = Assert.IsType(message.Contents[3]);
Assert.Equal("mcp_68be416900f88191837ae0718339a4ce0384f747588fc3f5", secondCall.CallId);
Assert.Equal("deepwiki", secondCall.ServerName);
- Assert.Equal("ask_question", secondCall.ToolName);
+ Assert.Equal("ask_question", secondCall.Name);
Assert.NotNull(secondCall.Arguments);
Assert.Equal("dotnet/extensions", ((JsonElement)secondCall.Arguments["repoName"]!).GetString());
Assert.Equal("What is the path to the README.md file for Microsoft.Extensions.AI.Abstractions?", ((JsonElement)secondCall.Arguments["question"]!).GetString());
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).Text);
Assert.NotNull(response.Usage);
Assert.Equal(1329, response.Usage.InputTokenCount);
@@ -2211,28 +2208,26 @@ public async Task McpToolCall_ApprovalNotRequired_Streaming()
var firstCall = Assert.IsType(message.Contents[1]);
Assert.Equal("mcp_68be4503d45c819e89cb574361c8eba003a2537be0e84a54", firstCall.CallId);
Assert.Equal("deepwiki", firstCall.ServerName);
- Assert.Equal("read_wiki_structure", firstCall.ToolName);
+ Assert.Equal("read_wiki_structure", firstCall.Name);
Assert.NotNull(firstCall.Arguments);
Assert.Single(firstCall.Arguments);
Assert.Equal("dotnet/extensions", ((JsonElement)firstCall.Arguments["repoName"]!).GetString());
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).Text);
var secondCall = Assert.IsType(message.Contents[3]);
Assert.Equal("mcp_68be4505f134819e806c002f27cce0c303a2537be0e84a54", secondCall.CallId);
Assert.Equal("deepwiki", secondCall.ServerName);
- Assert.Equal("ask_question", secondCall.ToolName);
+ Assert.Equal("ask_question", secondCall.Name);
Assert.NotNull(secondCall.Arguments);
Assert.Equal("dotnet/extensions", ((JsonElement)secondCall.Arguments["repoName"]!).GetString());
Assert.Equal("What is the path to the README.md file for Microsoft.Extensions.AI.Abstractions?", ((JsonElement)secondCall.Arguments["question"]!).GetString());
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).Text);
Assert.NotNull(response.Usage);
Assert.Equal(1420, response.Usage.InputTokenCount);
@@ -2240,6 +2235,203 @@ public async Task McpToolCall_ApprovalNotRequired_Streaming()
Assert.Equal(1569, 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.Name);
+ Assert.NotNull(toolCall.Arguments);
+ Assert.Empty(toolCall.Arguments);
+
+ var toolResult = Assert.IsType(message.Contents[2]);
+ Assert.Equal("mcp_689023b0fa88819f99f48aff343d5ad50475557f6fefb5f0", toolResult.CallId);
+ var errorContent = Assert.IsType(toolResult.Result);
+ Assert.Contains("An error occurred invoking 'test_error'.", errorContent.Message);
+
+ 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_WithAuthorizationTokenAndCustomHeaders_IncludesInRequest()
{
@@ -2309,11 +2501,13 @@ public async Task McpToolCall_WithAuthorizationTokenAndCustomHeaders_IncludesInR
var mcpTool = new HostedMcpServerTool("deepwiki", new Uri("https://mcp.deepwiki.com/mcp"))
{
ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire,
- AuthorizationToken = "test-auth-token-12345"
+ Headers = new Dictionary
+ {
+ ["Authorization"] = "Bearer test-auth-token-12345",
+ ["X-Custom-Header"] = "custom-value"
+ }
};
- mcpTool.Headers!["X-Custom-Header"] = "custom-value";
-
var response = await client.GetResponseAsync("hello", new ChatOptions { Tools = [mcpTool] });
Assert.NotNull(response);
@@ -2322,6 +2516,94 @@ public async Task McpToolCall_WithAuthorizationTokenAndCustomHeaders_IncludesInR
Assert.Equal("Hi!", message.Text);
}
+ [Theory]
+ [InlineData("bearer test-auth-token-12345")]
+ [InlineData("BEARER test-auth-token-12345")]
+ [InlineData("BeArEr test-auth-token-12345")]
+ [InlineData(" Bearer test-auth-token-12345")]
+ [InlineData("Bearer test-auth-token-12345 ")]
+ [InlineData(" Bearer test-auth-token-12345 ")]
+ [InlineData("Bearer test-auth-token-12345")]
+ public async Task McpToolCall_WithCaseInsensitiveBearerToken_ExtractsToken(string authHeaderValue)
+ {
+ // Use a connector ID (non-URL) to trigger the Bearer token extraction code path
+ string expectedToken = "test-auth-token-12345";
+ string expectedInput = $$"""
+ {
+ "model": "gpt-4o-mini",
+ "tools": [
+ {
+ "type": "mcp",
+ "server_label": "my-connector",
+ "connector_id": "connector-id-123",
+ "authorization": "{{expectedToken}}",
+ "require_approval": "never"
+ }
+ ],
+ "input": [
+ {
+ "type": "message",
+ "role": "user",
+ "content": [
+ {
+ "type": "input_text",
+ "text": "hello"
+ }
+ ]
+ }
+ ]
+ }
+ """;
+
+ const string Output = """
+ {
+ "id": "resp_bearer01",
+ "object": "response",
+ "created_at": 1757299043,
+ "status": "completed",
+ "model": "gpt-4o-mini-2024-07-18",
+ "output": [
+ {
+ "id": "msg_bearer01",
+ "type": "message",
+ "status": "completed",
+ "role": "assistant",
+ "content": [
+ {
+ "type": "output_text",
+ "text": "Hi!"
+ }
+ ]
+ }
+ ],
+ "usage": {
+ "input_tokens": 10,
+ "output_tokens": 2,
+ "total_tokens": 12
+ }
+ }
+ """;
+
+ using VerbatimHttpHandler handler = new(expectedInput, Output);
+ using HttpClient httpClient = new(handler);
+ using IChatClient client = CreateResponseClient(httpClient, "gpt-4o-mini");
+
+ // Use string constructor with non-URL to trigger connector ID path
+ var mcpTool = new HostedMcpServerTool("my-connector", "connector-id-123")
+ {
+ ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire,
+ Headers = new Dictionary
+ {
+ ["Authorization"] = authHeaderValue,
+ }
+ };
+
+ var response = await client.GetResponseAsync("hello", new ChatOptions { Tools = [mcpTool] });
+
+ Assert.NotNull(response);
+ Assert.Equal("resp_bearer01", response.ResponseId);
+ }
+
[Fact]
public async Task GetResponseAsync_BackgroundResponses_FirstCall()
{
diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs
index 0d2fd351c30..120646134c7 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs
@@ -1,4 +1,4 @@
-// Licensed to the .NET Foundation under one or more agreements.
+// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
@@ -9,7 +9,7 @@
using System.Threading.Tasks;
using Xunit;
-namespace Microsoft.Extensions.AI.ChatCompletion;
+namespace Microsoft.Extensions.AI;
public class FunctionInvokingChatClientApprovalsTests
{
@@ -45,8 +45,8 @@ public async Task AllFunctionCallsReplacedWithApprovalsWhenAllRequireApprovalAsy
[
new ChatMessage(ChatRole.Assistant,
[
- new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func1")),
- new FunctionApprovalRequestContent("callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } }))
+ new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")),
+ new FunctionApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } }))
])
];
@@ -81,8 +81,8 @@ public async Task AllFunctionCallsReplacedWithApprovalsWhenAnyRequireApprovalAsy
[
new ChatMessage(ChatRole.Assistant,
[
- new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func1")),
- new FunctionApprovalRequestContent("callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } }))
+ new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")),
+ new FunctionApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } }))
])
];
@@ -125,8 +125,8 @@ public async Task AllFunctionCallsReplacedWithApprovalsWhenAnyRequestOrAdditiona
[
new ChatMessage(ChatRole.Assistant,
[
- new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func1")),
- new FunctionApprovalRequestContent("callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } }))
+ new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")),
+ new FunctionApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } }))
])
];
@@ -152,13 +152,13 @@ public async Task ApprovedApprovalResponsesAreExecutedAsync()
new ChatMessage(ChatRole.User, "hello"),
new ChatMessage(ChatRole.Assistant,
[
- new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func1")),
- new FunctionApprovalRequestContent("callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } }))
+ new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")),
+ new FunctionApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } }))
]) { MessageId = "resp1" },
new ChatMessage(ChatRole.User,
[
- new FunctionApprovalResponseContent("callId1", true, new FunctionCallContent("callId1", "Func1")),
- new FunctionApprovalResponseContent("callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } }))
+ new FunctionApprovalResponseContent("ficc_callId1", true, new FunctionCallContent("callId1", "Func1")),
+ new FunctionApprovalResponseContent("ficc_callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } }))
]),
];
@@ -204,13 +204,13 @@ public async Task ApprovedApprovalResponsesAreGroupedWhenMessageIdIsNullAsync()
new ChatMessage(ChatRole.User, "hello"),
new ChatMessage(ChatRole.Assistant,
[
- new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func1")),
- new FunctionApprovalRequestContent("callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } }))
+ new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")),
+ new FunctionApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } }))
]), // Note: No MessageId set - this is the bug trigger
new ChatMessage(ChatRole.User,
[
- new FunctionApprovalResponseContent("callId1", true, new FunctionCallContent("callId1", "Func1")),
- new FunctionApprovalResponseContent("callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } }))
+ new FunctionApprovalResponseContent("ficc_callId1", true, new FunctionCallContent("callId1", "Func1")),
+ new FunctionApprovalResponseContent("ficc_callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } }))
]),
];
@@ -256,19 +256,19 @@ public async Task ApprovedApprovalResponsesFromSeparateFCCMessagesAreExecutedAsy
new ChatMessage(ChatRole.User, "hello"),
new ChatMessage(ChatRole.Assistant,
[
- new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func1")),
+ new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")),
]) { MessageId = "resp1" },
new ChatMessage(ChatRole.Assistant,
[
- new FunctionApprovalRequestContent("callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } }))
+ new FunctionApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } }))
]) { MessageId = "resp2" },
new ChatMessage(ChatRole.User,
[
- new FunctionApprovalResponseContent("callId1", true, new FunctionCallContent("callId1", "Func1")),
+ new FunctionApprovalResponseContent("ficc_callId1", true, new FunctionCallContent("callId1", "Func1")),
]),
new ChatMessage(ChatRole.User,
[
- new FunctionApprovalResponseContent("callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } }))
+ new FunctionApprovalResponseContent("ficc_callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } }))
]),
];
@@ -315,13 +315,13 @@ public async Task RejectedApprovalResponsesAreFailedAsync()
new ChatMessage(ChatRole.User, "hello"),
new ChatMessage(ChatRole.Assistant,
[
- new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func1")),
- new FunctionApprovalRequestContent("callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } }))
+ new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")),
+ new FunctionApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } }))
]) { MessageId = "resp1" },
new ChatMessage(ChatRole.User,
[
- new FunctionApprovalResponseContent("callId1", false, new FunctionCallContent("callId1", "Func1")),
- new FunctionApprovalResponseContent("callId2", false, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } }))
+ new FunctionApprovalResponseContent("ficc_callId1", false, new FunctionCallContent("callId1", "Func1")),
+ new FunctionApprovalResponseContent("ficc_callId2", false, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } }))
]),
];
@@ -374,13 +374,13 @@ public async Task MixedApprovedAndRejectedApprovalResponsesAreExecutedAndFailedA
new ChatMessage(ChatRole.User, "hello"),
new ChatMessage(ChatRole.Assistant,
[
- new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func1")),
- new FunctionApprovalRequestContent("callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } }))
+ new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")),
+ new FunctionApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } }))
]) { MessageId = "resp1" },
new ChatMessage(ChatRole.User,
[
- new FunctionApprovalResponseContent("callId1", false, new FunctionCallContent("callId1", "Func1")),
- new FunctionApprovalResponseContent("callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } }))
+ new FunctionApprovalResponseContent("ficc_callId1", false, new FunctionCallContent("callId1", "Func1")),
+ new FunctionApprovalResponseContent("ficc_callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } }))
]),
];
@@ -438,16 +438,16 @@ public async Task RejectedApprovalResponsesWithCustomReasonAsync()
new ChatMessage(ChatRole.User, "hello"),
new ChatMessage(ChatRole.Assistant,
[
- new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func1")),
- new FunctionApprovalRequestContent("callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary