From 2daa4b4dd6b48d907786513de2c49ac232d10b8f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 07:13:16 +0000 Subject: [PATCH 01/30] Initial plan From b5dac2d1908aa2a3f38af4b6f32fba8fe9e03bb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Fri, 30 Jan 2026 00:02:02 -0600 Subject: [PATCH 02/30] Rebase --- .../Contents/AIContent.cs | 2 - .../Contents/FunctionCallContent.cs | 2 + .../Contents/FunctionResultContent.cs | 2 + .../McpServerToolApprovalRequestContent.cs | 42 ---- .../McpServerToolApprovalResponseContent.cs | 33 --- .../Contents/McpServerToolCallContent.cs | 32 +-- .../Contents/McpServerToolResultContent.cs | 17 +- .../Contents/UserInputRequestContent.cs | 1 - .../Contents/UserInputResponseContent.cs | 1 - .../Utilities/AIJsonUtilities.Defaults.cs | 4 - .../OpenAIJsonContext.cs | 1 - .../OpenAIResponsesChatClient.cs | 67 +++-- .../FunctionInvokingChatClient.cs | 4 +- .../Contents/AIContentTests.cs | 4 +- .../Contents/FunctionCallContentTests.cs | 30 +++ .../Contents/FunctionResultContentTests.cs | 30 +++ .../Contents/McpServerToolCallContentTests.cs | 32 ++- .../McpServerToolResultContentTests.cs | 15 +- .../Contents/UserInputRequestContentTests.cs | 26 +- .../Contents/UserInputResponseContentTests.cs | 23 +- .../TestJsonSerializerContext.cs | 2 + .../OpenAIResponseClientIntegrationTests.cs | 15 +- .../OpenAIResponseClientTests.cs | 221 ++++++++++++++++- ...unctionInvokingChatClientApprovalsTests.cs | 230 +++++++++++++++++- 24 files changed, 621 insertions(+), 215 deletions(-) delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolApprovalRequestContent.cs delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolApprovalResponseContent.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs index af8b19c8d84..ffdf33f7645 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs @@ -28,8 +28,6 @@ namespace Microsoft.Extensions.AI; // [JsonDerivedType(typeof(FunctionApprovalResponseContent), typeDiscriminator: "functionApprovalResponse")] // [JsonDerivedType(typeof(McpServerToolCallContent), typeDiscriminator: "mcpServerToolCall")] // [JsonDerivedType(typeof(McpServerToolResultContent), typeDiscriminator: "mcpServerToolResult")] -// [JsonDerivedType(typeof(McpServerToolApprovalRequestContent), typeDiscriminator: "mcpServerToolApprovalRequest")] -// [JsonDerivedType(typeof(McpServerToolApprovalResponseContent), typeDiscriminator: "mcpServerToolApprovalResponse")] // [JsonDerivedType(typeof(CodeInterpreterToolCallContent), typeDiscriminator: "codeInterpreterToolCall")] // [JsonDerivedType(typeof(CodeInterpreterToolResultContent), typeDiscriminator: "codeInterpreterToolResult")] diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs index b8a135b7e1d..d7e986a9179 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs @@ -14,6 +14,8 @@ namespace Microsoft.Extensions.AI; /// Represents a function call request. /// [DebuggerDisplay("{DebuggerDisplay,nq}")] +[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] +[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..daf3e23ebd6 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionResultContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionResultContent.cs @@ -13,6 +13,8 @@ namespace Microsoft.Extensions.AI; /// Represents the result of a function call. /// [DebuggerDisplay("{DebuggerDisplay,nq}")] +[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] +[JsonDerivedType(typeof(McpServerToolResultContent), "mcpServerToolResult")] public class FunctionResultContent : AIContent { /// 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..a0d357211f3 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs @@ -2,8 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; @@ -17,40 +17,26 @@ namespace Microsoft.Extensions.AI; /// It is informational only. /// [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. + [JsonConstructor] + 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; + InvocationRequired = false; } - /// - /// 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..19b685a076e 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs @@ -2,8 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; @@ -17,7 +17,7 @@ namespace Microsoft.Extensions.AI; /// 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. @@ -25,18 +25,9 @@ public sealed class McpServerToolResultContent : AIContent /// The tool call ID. /// is . /// is empty or composed entirely of whitespace. + [JsonConstructor] public McpServerToolResultContent(string callId) + : base(Throw.IfNullOrWhitespace(callId), result: null) { - CallId = Throw.IfNullOrWhitespace(callId); } - - /// - /// Gets the tool call ID. - /// - public string CallId { get; } - - /// - /// Gets or sets the output of the tool call. - /// - public IList? Output { get; set; } } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputRequestContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputRequestContent.cs index 9f40b33253c..eba17dd8d5f 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputRequestContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputRequestContent.cs @@ -15,7 +15,6 @@ namespace Microsoft.Extensions.AI; [Experimental(DiagnosticIds.Experiments.AIFunctionApprovals, UrlFormat = DiagnosticIds.UrlFormat)] [JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] [JsonDerivedType(typeof(FunctionApprovalRequestContent), "functionApprovalRequest")] -[JsonDerivedType(typeof(McpServerToolApprovalRequestContent), "mcpServerToolApprovalRequest")] public class UserInputRequestContent : AIContent { /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputResponseContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputResponseContent.cs index eaddd46f920..65c063e7493 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputResponseContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputResponseContent.cs @@ -15,7 +15,6 @@ namespace Microsoft.Extensions.AI; [Experimental(DiagnosticIds.Experiments.AIFunctionApprovals, UrlFormat = DiagnosticIds.UrlFormat)] [JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] [JsonDerivedType(typeof(FunctionApprovalResponseContent), "functionApprovalResponse")] -[JsonDerivedType(typeof(McpServerToolApprovalResponseContent), "mcpServerToolApprovalResponse")] public class UserInputResponseContent : AIContent { /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs index d01294836bc..51ed08441f1 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs @@ -55,8 +55,6 @@ private static JsonSerializerOptions CreateDefaultOptions() AddAIContentType(options, typeof(FunctionApprovalResponseContent), typeDiscriminatorId: "functionApprovalResponse", checkBuiltIn: false); AddAIContentType(options, typeof(McpServerToolCallContent), typeDiscriminatorId: "mcpServerToolCall", checkBuiltIn: false); AddAIContentType(options, typeof(McpServerToolResultContent), typeDiscriminatorId: "mcpServerToolResult", checkBuiltIn: false); - AddAIContentType(options, typeof(McpServerToolApprovalRequestContent), typeDiscriminatorId: "mcpServerToolApprovalRequest", checkBuiltIn: false); - AddAIContentType(options, typeof(McpServerToolApprovalResponseContent), typeDiscriminatorId: "mcpServerToolApprovalResponse", checkBuiltIn: false); AddAIContentType(options, typeof(CodeInterpreterToolCallContent), typeDiscriminatorId: "codeInterpreterToolCall", checkBuiltIn: false); AddAIContentType(options, typeof(CodeInterpreterToolResultContent), typeDiscriminatorId: "codeInterpreterToolResult", checkBuiltIn: false); @@ -129,8 +127,6 @@ private static JsonSerializerOptions CreateDefaultOptions() [JsonSerializable(typeof(FunctionApprovalResponseContent))] [JsonSerializable(typeof(McpServerToolCallContent))] [JsonSerializable(typeof(McpServerToolResultContent))] - [JsonSerializable(typeof(McpServerToolApprovalRequestContent))] - [JsonSerializable(typeof(McpServerToolApprovalResponseContent))] [JsonSerializable(typeof(CodeInterpreterToolCallContent))] [JsonSerializable(typeof(CodeInterpreterToolResultContent))] [JsonSerializable(typeof(ResponseContinuationToken))] diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIJsonContext.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIJsonContext.cs index 33d17e2963e..59d287edde2 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIJsonContext.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIJsonContext.cs @@ -14,7 +14,6 @@ namespace Microsoft.Extensions.AI; WriteIndented = true)] [JsonSerializable(typeof(OpenAIClientExtensions.ToolJson))] [JsonSerializable(typeof(IDictionary))] -[JsonSerializable(typeof(IReadOnlyDictionary))] [JsonSerializable(typeof(string[]))] [JsonSerializable(typeof(IEnumerable))] [JsonSerializable(typeof(JsonElement))] diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index 5fb6ac1935a..d9183d30e67 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -207,9 +207,9 @@ internal static IEnumerable ToChatMessages(IEnumerable ToChatMessages(IEnumerable break; case McpToolCallApprovalRequestItem mtcari: - yield return CreateUpdate(new McpServerToolApprovalRequestContent(mtcari.Id, new(mtcari.Id, mtcari.ToolName, mtcari.ServerLabel) + yield return CreateUpdate(new FunctionApprovalRequestContent(mtcari.Id, new McpServerToolCallContent(mtcari.Id, mtcari.ToolName, mtcari.ServerLabel) { - Arguments = JsonSerializer.Deserialize(mtcari.ToolArguments.ToMemory().Span, OpenAIJsonContext.Default.IReadOnlyDictionaryStringObject)!, + Arguments = JsonSerializer.Deserialize(mtcari.ToolArguments, OpenAIJsonContext.Default.IDictionaryStringObject), RawRepresentation = mtcari, }) { @@ -819,7 +815,7 @@ internal static IEnumerable ToOpenAIResponseItems(IEnumerable? idToContentMapping = null; + Dictionary? idToContentMapping = null; foreach (ChatMessage input in inputs) { @@ -852,7 +848,7 @@ internal static IEnumerable ToOpenAIResponseItems(IEnumerable rawRep, - McpServerToolApprovalResponseContent mcpResp => ResponseItem.CreateMcpApprovalResponseItem(mcpResp.Id, mcpResp.Approved), + FunctionApprovalResponseContent { FunctionCall: McpServerToolCallContent } funcResp => ResponseItem.CreateMcpApprovalResponseItem(funcResp.Id, funcResp.Approved), _ => null }; @@ -934,6 +930,10 @@ internal static IEnumerable ToOpenAIResponseItems(IEnumerable contents) { @@ -1048,10 +1048,6 @@ static FunctionCallOutputResponseItem SerializeAIContent(string callId, IEnumera break; } break; - - case McpServerToolApprovalResponseContent mcpApprovalResponseContent: - yield return ResponseItem.CreateMcpApprovalResponseItem(mcpApprovalResponseContent.Id, mcpApprovalResponseContent.Approved); - break; } } @@ -1079,6 +1075,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, @@ -1088,34 +1088,33 @@ static FunctionCallOutputResponseItem SerializeAIContent(string callId, IEnumera AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IDictionary))))); break; - case McpServerToolApprovalRequestContent mcpApprovalRequestContent: + case FunctionApprovalRequestContent funcResp when funcResp.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; + funcResp.Id, + 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 BinaryData errorData) { - mtci.Error = BinaryData.FromString(errorContent.Message); + mtci.Error = errorData; } - else + else if (mstrc.Result is string outputString) { - mtci.ToolOutput = string.Concat(mstrc.Output?.OfType() ?? []); + mtci.ToolOutput = outputString; } yield return mtci; @@ -1300,7 +1299,7 @@ private static void AddMcpToolCallContent(McpToolCallItem mtci, IList { contents.Add(new McpServerToolCallContent(mtci.Id, mtci.ToolName, mtci.ServerLabel) { - Arguments = JsonSerializer.Deserialize(mtci.ToolArguments.ToMemory().Span, OpenAIJsonContext.Default.IReadOnlyDictionaryStringObject)!, + Arguments = JsonSerializer.Deserialize(mtci.ToolArguments, OpenAIJsonContext.Default.IDictionaryStringObject), // We purposefully do not set the RawRepresentation on the McpServerToolCallContent, only on the McpServerToolResultContent, to avoid // the same McpToolCallItem being included on two different AIContent instances. When these are roundtripped, we want only one @@ -1310,9 +1309,7 @@ private static void AddMcpToolCallContent(McpToolCallItem mtci, IList contents.Add(new McpServerToolResultContent(mtci.Id) { RawRepresentation = mtci, - Output = [mtci.Error is not null ? - new ErrorContent(mtci.Error.ToString()) : - new TextContent(mtci.ToolOutput)], + Result = mtci.Error ?? (object)mtci.ToolOutput, }); } diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs index cd449248867..a4732a36c0d 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.InvocationRequired: // 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); break; - case FunctionApprovalResponseContent farc: + case FunctionApprovalResponseContent farc when farc.FunctionCall.InvocationRequired: // 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); diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIContentTests.cs index e5734ccd7cf..71ee60c5983 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIContentTests.cs @@ -74,8 +74,8 @@ public void Serialization_DerivedTypes_Roundtrips() new FunctionApprovalResponseContent("request123", approved: true, new FunctionCallContent("call123", "functionName", new Dictionary { { "param1", 123 } })), new McpServerToolCallContent("call123", "myTool", "myServer"), new McpServerToolResultContent("call123"), - new McpServerToolApprovalRequestContent("request123", new McpServerToolCallContent("call123", "myTool", "myServer")), - new McpServerToolApprovalResponseContent("request123", approved: true) + new FunctionApprovalRequestContent("request123", new McpServerToolCallContent("call123", "myTool", "myServer")), + new FunctionApprovalResponseContent("request123", approved: true, new McpServerToolCallContent("call456", "myTool2", "myServer2")) ]); var serialized = JsonSerializer.Serialize(message, AIJsonUtilities.DefaultOptions); diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests.cs index 394728b7236..86975e79d6e 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,34 @@ 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_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..e3f07b01cba 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,34 @@ public void ItShouldBeSerializableAndDeserializableWithException() Assert.Equal(sut.Result, deserializedSut.Result?.ToString()); Assert.Null(deserializedSut.Exception); } + + [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/McpServerToolCallContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolCallContentTests.cs index d5c5b43ed0a..d8eafdbcdbd 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.False(c.InvocationRequired); } [Fact] @@ -39,22 +41,42 @@ 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.Equal("callId1", c.CallId); - Assert.Equal("toolName", c.ToolName); + Assert.Equal("toolName", c.Name); Assert.Equal("serverName", c.ServerName); + Assert.False(c.InvocationRequired); } [Fact] 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" } } + }; + + var json = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions); + var deserializedContent = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions); + + Assert.NotNull(deserializedContent); + 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..c405c21b300 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,7 +54,7 @@ public void Serialization_Roundtrips() { var content = new McpServerToolResultContent("call123") { - Output = new List { new TextContent("result") } + Result = "result" }; var json = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions); @@ -63,6 +62,6 @@ public void Serialization_Roundtrips() Assert.NotNull(deserializedContent); Assert.Equal(content.CallId, deserializedContent.CallId); - Assert.NotNull(deserializedContent.Output); + 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 index fc4dac9cabb..c63773d6d9a 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputRequestContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputRequestContentTests.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Text.Json; using Xunit; @@ -33,27 +32,30 @@ public void Constructor_Roundtrips(string id) [Fact] public void Serialization_DerivedTypes_Roundtrips() { - UserInputRequestContent content = new FunctionApprovalRequestContent("request123", new FunctionCallContent("call123", "functionName", new Dictionary { { "param1", 123 } })); - 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")), + 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.UserInputRequestContentArray); var deserializedContents = JsonSerializer.Deserialize(serializedContents, TestJsonSerializerContext.Default.UserInputRequestContentArray); Assert.NotNull(deserializedContents); - - Assert.Equal(contents.Count(), deserializedContents.Length); + Assert.Equal(contents.Length, deserializedContents.Length); for (int i = 0; i < deserializedContents.Length; i++) { - Assert.NotNull(contents.ElementAt(i)); - Assert.Equal(contents.ElementAt(i).GetType(), deserializedContents[i].GetType()); + Assert.NotNull(deserializedContents[i]); + Assert.Equal(contents[i].GetType(), deserializedContents[i].GetType()); } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputResponseContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputResponseContentTests.cs index 2442e57272d..b77a828e50c 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputResponseContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputResponseContentTests.cs @@ -31,26 +31,31 @@ public void Constructor_Roundtrips(string 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), + + // Uncomment once McpServerToolCallContent is no longer experimental. + 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.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.NotNull(deserializedContents[i]); Assert.Equal(contents[i].GetType(), deserializedContents[i].GetType()); } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs index faaa799baf4..98c4ba273db 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs @@ -41,4 +41,6 @@ namespace Microsoft.Extensions.AI; [JsonSerializable(typeof(ResponseContinuationToken))] [JsonSerializable(typeof(UserInputRequestContent[]))] [JsonSerializable(typeof(UserInputResponseContent[]))] +[JsonSerializable(typeof(FunctionCallContent[]))] +[JsonSerializable(typeof(FunctionResultContent[]))] internal sealed partial class TestJsonSerializerContext : JsonSerializerContext; diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs index 1421e780dca..1335ecb45c1 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs @@ -144,7 +144,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); } @@ -198,8 +198,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) { @@ -407,8 +407,10 @@ await client.GetStreamingResponseAsync(input, chatOptions).ToChatResponseAsync() if (approval) { input.AddRange(response.Messages); - var approvalRequest = Assert.Single(response.Messages.SelectMany(m => m.Contents).OfType()); - Assert.Equal("search_events", approvalRequest.ToolCall.ToolName); + var approvalRequest = Assert.Single(response.Messages.SelectMany(m => m.Contents).OfType()); + var mcpCallContent = Assert.IsType(approvalRequest.FunctionCall); + Assert.Equal("search_events", mcpCallContent.ToolName); + Assert.Equal("search_events", mcpCallContent.Name); input.Add(new ChatMessage(ChatRole.Tool, [approvalRequest.CreateResponse(true)])); response = streaming ? @@ -421,8 +423,7 @@ await client.GetStreamingResponseAsync(input, chatOptions).ToChatResponseAsync() Assert.Equal("search_events", toolCall.ToolName); var toolResult = Assert.Single(response.Messages.SelectMany(m => m.Contents).OfType()); - var content = Assert.IsType(Assert.Single(toolResult.Output!)); - Assert.Equal(@"{""events"": [], ""next_page_token"": null}", content.Text); + Assert.Equal(@"{""events"": [], ""next_page_token"": null}", toolResult.Result); } } diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs index d1a6749450b..068697079c1 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs @@ -1286,7 +1286,7 @@ public async Task McpToolCall_ApprovalRequired_NonStreaming(string role) { Tools = [new HostedMcpServerTool("deepwiki", new Uri("https://mcp.deepwiki.com/mcp"))] }; - McpServerToolApprovalRequestContent approvalRequest; + FunctionApprovalRequestContent approvalRequest; using (VerbatimHttpHandler handler = new(input, output)) using (HttpClient httpClient = new(handler)) @@ -1296,7 +1296,7 @@ public async Task McpToolCall_ApprovalRequired_NonStreaming(string role) "Tell me the path to the README.md file for Microsoft.Extensions.AI.Abstractions in the dotnet/extensions repository", chatOptions); - approvalRequest = Assert.Single(response.Messages.SelectMany(m => m.Contents).OfType()); + approvalRequest = Assert.Single(response.Messages.SelectMany(m => m.Contents).OfType()); chatOptions.ConversationId = response.ConversationId; } @@ -1441,8 +1441,7 @@ public async Task McpToolCall_ApprovalRequired_NonStreaming(string role) var result = Assert.IsType(message.Contents[1]); Assert.Equal("mcp_06ee3b1962eeb8470068e6b21cbaa081a3b5aa2a6c989f4c6f", result.CallId); - Assert.NotNull(result.Output); - Assert.StartsWith("The `README.md` file for `Microsoft.Extensions.AI.Abstractions` is located at", Assert.IsType(Assert.Single(result.Output)).Text); + Assert.StartsWith("The `README.md` file for `Microsoft.Extensions.AI.Abstractions` is located at", Assert.IsType(result.Result)); Assert.NotNull(response.Usage); Assert.Equal(542, response.Usage.InputTokenCount); @@ -1695,8 +1694,7 @@ public async Task McpToolCall_ApprovalNotRequired_NonStreaming(bool rawTool) var firstResult = Assert.IsType(message.Contents[2]); Assert.Equal("mcp_68be4166acfc8191bc5e0a751eed358b0384f747588fc3f5", firstResult.CallId); - Assert.NotNull(firstResult.Output); - Assert.StartsWith("Available pages for dotnet/extensions", Assert.IsType(Assert.Single(firstResult.Output)).Text); + Assert.StartsWith("Available pages for dotnet/extensions", Assert.IsType(firstResult.Result)); var secondCall = Assert.IsType(message.Contents[3]); Assert.Equal("mcp_68be416900f88191837ae0718339a4ce0384f747588fc3f5", secondCall.CallId); @@ -1708,8 +1706,7 @@ public async Task McpToolCall_ApprovalNotRequired_NonStreaming(bool rawTool) var secondResult = Assert.IsType(message.Contents[4]); Assert.Equal("mcp_68be416900f88191837ae0718339a4ce0384f747588fc3f5", secondResult.CallId); - Assert.NotNull(secondResult.Output); - Assert.StartsWith("The `README.md` file for `Microsoft.Extensions.AI.Abstractions` is located at", Assert.IsType(Assert.Single(secondResult.Output)).Text); + Assert.StartsWith("The `README.md` file for `Microsoft.Extensions.AI.Abstractions` is located at", Assert.IsType(secondResult.Result)); Assert.NotNull(response.Usage); Assert.Equal(1329, response.Usage.InputTokenCount); @@ -2108,8 +2105,7 @@ public async Task McpToolCall_ApprovalNotRequired_Streaming() var firstResult = Assert.IsType(message.Contents[2]); Assert.Equal("mcp_68be4503d45c819e89cb574361c8eba003a2537be0e84a54", firstResult.CallId); - Assert.NotNull(firstResult.Output); - Assert.StartsWith("Available pages for dotnet/extensions", Assert.IsType(Assert.Single(firstResult.Output)).Text); + Assert.StartsWith("Available pages for dotnet/extensions", Assert.IsType(firstResult.Result)); var secondCall = Assert.IsType(message.Contents[3]); Assert.Equal("mcp_68be4505f134819e806c002f27cce0c303a2537be0e84a54", secondCall.CallId); @@ -2121,8 +2117,7 @@ public async Task McpToolCall_ApprovalNotRequired_Streaming() var secondResult = Assert.IsType(message.Contents[4]); Assert.Equal("mcp_68be4505f134819e806c002f27cce0c303a2537be0e84a54", secondResult.CallId); - Assert.NotNull(secondResult.Output); - Assert.StartsWith("The path to the `README.md` file", Assert.IsType(Assert.Single(secondResult.Output)).Text); + Assert.StartsWith("The path to the `README.md` file", Assert.IsType(secondResult.Result)); Assert.NotNull(response.Usage); Assert.Equal(1420, response.Usage.InputTokenCount); @@ -2130,6 +2125,208 @@ 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.ToolName); + Assert.NotNull(toolCall.Arguments); + Assert.Empty(toolCall.Arguments); + + var toolResult = Assert.IsType(message.Contents[2]); + Assert.Equal("mcp_689023b0fa88819f99f48aff343d5ad50475557f6fefb5f0", toolResult.CallId); + var errorData = Assert.IsType(toolResult.Result); + var errorJson = JsonDocument.Parse(errorData); + Assert.Equal("mcp_tool_execution_error", errorJson.RootElement.GetProperty("type").GetString()); + var contentArray = errorJson.RootElement.GetProperty("content"); + Assert.Equal(1, contentArray.GetArrayLength()); + Assert.Equal("text", contentArray[0].GetProperty("type").GetString()); + Assert.Equal("An error occurred invoking 'test_error'.", contentArray[0].GetProperty("text").GetString()); + + Assert.NotNull(response.Usage); + Assert.Equal(500, response.Usage.InputTokenCount); + Assert.Equal(50, response.Usage.OutputTokenCount); + Assert.Equal(550, response.Usage.TotalTokenCount); + } + [Fact] public async Task McpToolCall_WithAuthorizationTokenAndCustomHeaders_IncludesInRequest() { diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs index dd77ef370c6..b43862c3c81 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs @@ -9,7 +9,7 @@ using System.Threading.Tasks; using Xunit; -namespace Microsoft.Extensions.AI.ChatCompletion; +namespace Microsoft.Extensions.AI; public class FunctionInvokingChatClientApprovalsTests { @@ -1156,6 +1156,219 @@ public async Task FunctionCallsWithInvocationRequiredFalseAreNotReplacedWithAppr Assert.Equal(0, functionInvokedCount); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task FunctionCallReplacedWithApproval_MixedWithMcpApprovalAsync(bool useAdditionalTools) + { + AITool[] tools = + [ + new ApprovalRequiredAIFunction(AIFunctionFactory.Create(() => "Result 1", "Func")), + new HostedMcpServerTool("myServer", "https://localhost/mcp") + ]; + + var options = new ChatOptions + { + Tools = useAdditionalTools ? null : tools + }; + + List input = + [ + new ChatMessage(ChatRole.User, "hello"), + ]; + + List downstreamClientOutput = + [ + new ChatMessage(ChatRole.Assistant, + [ + new FunctionCallContent("callId1", "Func"), + new FunctionApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer")) + ]) + ]; + + List expectedOutput = + [ + new ChatMessage(ChatRole.Assistant, + [ + new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func")), + new FunctionApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer")) + ]) + ]; + + await InvokeAndAssertAsync(options, input, downstreamClientOutput, expectedOutput, additionalTools: useAdditionalTools ? tools : null); + await InvokeAndAssertStreamingAsync(options, input, downstreamClientOutput, expectedOutput, additionalTools: useAdditionalTools ? tools : null); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ApprovedApprovalResponseIsExecuted_MixedWithMcpApprovalAsync(bool useAdditionalTools) + { + AITool[] tools = + [ + new ApprovalRequiredAIFunction(AIFunctionFactory.Create(() => "Result 1", "Func")), + new HostedMcpServerTool("myServer", "https://localhost/mcp") + ]; + + var options = new ChatOptions + { + Tools = useAdditionalTools ? null : tools + }; + + List input = + [ + new ChatMessage(ChatRole.User, "hello"), + new ChatMessage(ChatRole.Assistant, + [ + new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func")), + new FunctionApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer")) + ]) { MessageId = "resp1" }, + new ChatMessage(ChatRole.User, + [ + new FunctionApprovalResponseContent("callId1", true, new FunctionCallContent("callId1", "Func")), + new FunctionApprovalResponseContent("callId2", true, new McpServerToolCallContent("callId2", "McpCall", "myServer")) + ]), + ]; + + List expectedDownstreamClientInput = + [ + new ChatMessage(ChatRole.User, "hello"), + new ChatMessage(ChatRole.Assistant, + [ + new FunctionApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer")) + ]), + new ChatMessage(ChatRole.User, + [ + new FunctionApprovalResponseContent("callId2", true, new McpServerToolCallContent("callId2", "McpCall", "myServer")) + ]), + new ChatMessage(ChatRole.Assistant, + [ + new FunctionCallContent("callId1", "Func") + ]), + new ChatMessage(ChatRole.Tool, + [ + new FunctionResultContent("callId1", result: "Result 1") + ]), + ]; + + List downstreamClientOutput = + [ + new ChatMessage(ChatRole.Assistant, [ + new McpServerToolResultContent("callId2") { Result = new List { new TextContent("Result 2") } }, + new TextContent("world") + ]) + ]; + + List output = + [ + new ChatMessage(ChatRole.Assistant, + [ + new FunctionCallContent("callId1", "Func") + ]), + new ChatMessage(ChatRole.Tool, + [ + new FunctionResultContent("callId1", result: "Result 1") + ]), + new ChatMessage(ChatRole.Assistant, [ + new McpServerToolResultContent("callId2") { Result = new List { new TextContent("Result 2") } }, + new TextContent("world") + ]) + ]; + + await InvokeAndAssertAsync(options, input, downstreamClientOutput, output, expectedDownstreamClientInput, additionalTools: useAdditionalTools ? tools : null); + await InvokeAndAssertStreamingAsync(options, input, downstreamClientOutput, output, expectedDownstreamClientInput, additionalTools: useAdditionalTools ? tools : null); + } + + [Theory] + [InlineData(false, true, false)] + [InlineData(false, false, true)] + [InlineData(true, true, false)] + [InlineData(true, false, true)] + public async Task RejectedApprovalResponses_MixedWithMcpApprovalAsync(bool useAdditionalTools, bool approveFuncCall, bool approveMcpCall) + { + Assert.NotEqual(approveFuncCall, approveMcpCall); + + AITool[] tools = + [ + new ApprovalRequiredAIFunction(AIFunctionFactory.Create(() => "Result 1", "Func")), + new HostedMcpServerTool("myServer", "https://localhost/mcp") + ]; + + var options = new ChatOptions + { + Tools = useAdditionalTools ? null : tools + }; + + List input = + [ + new ChatMessage(ChatRole.User, "hello"), + new ChatMessage(ChatRole.Assistant, + [ + new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func")), + new FunctionApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer")) + ]) { MessageId = "resp1" }, + new ChatMessage(ChatRole.User, + [ + new FunctionApprovalResponseContent("callId1", approveFuncCall, new FunctionCallContent("callId1", "Func")), + new FunctionApprovalResponseContent("callId2", approveMcpCall, new McpServerToolCallContent("callId2", "McpCall", "myServer")) + ]), + ]; + + List expectedDownstreamClientInput = [ + new ChatMessage(ChatRole.User, "hello"), + new ChatMessage(ChatRole.Assistant, + [ + new FunctionApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer")) + ]), + new ChatMessage(ChatRole.User, + [ + new FunctionApprovalResponseContent("callId2", approveMcpCall, new McpServerToolCallContent("callId2", "McpCall", "myServer")) + ]), + new ChatMessage(ChatRole.Assistant, + [ + new FunctionCallContent("callId1", "Func") + ]), + new ChatMessage(ChatRole.Tool, + [ + approveFuncCall ? + new FunctionResultContent("callId1", result: "Result 1") : + new FunctionResultContent("callId1", result: "Tool call invocation rejected.") + ]), + ]; + + List downstreamClientOutput = + [ + new ChatMessage(ChatRole.Assistant, [ + new TextContent("world"), + .. approveMcpCall ? + [new McpServerToolResultContent("callId2") { Result = new List { new TextContent("Result 2") } }] : + Array.Empty() + ]) + ]; + + List output = [ + new ChatMessage(ChatRole.Assistant, + [ + new FunctionCallContent("callId1", "Func"), + ]), + new ChatMessage(ChatRole.Tool, + [ + approveFuncCall ? + new FunctionResultContent("callId1", result: "Result 1") : + new FunctionResultContent("callId1", result: "Tool call invocation rejected.") + ]), + new ChatMessage(ChatRole.Assistant, [ + new TextContent("world"), + .. approveMcpCall ? + [new McpServerToolResultContent("callId2") { Result = new List { new TextContent("Result 2") } }] : + Array.Empty() + ]) + ]; + + await InvokeAndAssertAsync(options, input, downstreamClientOutput, output, expectedDownstreamClientInput, additionalTools: useAdditionalTools ? tools : null); + await InvokeAndAssertStreamingAsync(options, input, downstreamClientOutput, output, expectedDownstreamClientInput, additionalTools: useAdditionalTools ? tools : null); + } + private static Task> InvokeAndAssertAsync( ChatOptions? options, List input, @@ -1212,7 +1425,7 @@ private static async Task> InvokeAndAssertMultiRoundAsync( IChatClient service = configurePipeline(innerClient.AsBuilder()).Build(); - var result = await service.GetResponseAsync(new EnumeratedOnceEnumerable(input), options, cts.Token); + var result = await service.GetResponseAsync(new EnumeratedOnceEnumerable(CloneInput(input)), options, cts.Token); Assert.NotNull(result); var actualOutput = result.Messages as List ?? result.Messages.ToList(); @@ -1294,7 +1507,7 @@ private static async Task> InvokeAndAssertStreamingMultiRoundA IChatClient service = configurePipeline(innerClient.AsBuilder()).Build(); - var result = await service.GetStreamingResponseAsync(new EnumeratedOnceEnumerable(input), options, cts.Token).ToChatResponseAsync(); + var result = await service.GetStreamingResponseAsync(new EnumeratedOnceEnumerable(CloneInput(input)), options, cts.Token).ToChatResponseAsync(); Assert.NotNull(result); var actualOutput = result.Messages as List ?? result.Messages.ToList(); @@ -1313,4 +1526,15 @@ private static async IAsyncEnumerable YieldAsync(params T[] items) yield return item; } } + + private static List CloneInput(List input) => + input.Select(m => new ChatMessage(m.Role, m.Contents.Select(CloneFcc).ToList()) { MessageId = m.MessageId }).ToList(); + + private static AIContent CloneFcc(AIContent c) => c switch + { + FunctionCallContent fcc => new FunctionCallContent(fcc.CallId, fcc.Name, fcc.Arguments) { InvocationRequired = fcc.InvocationRequired }, + FunctionApprovalRequestContent farc => new FunctionApprovalRequestContent(farc.Id, (FunctionCallContent)CloneFcc(farc.FunctionCall)), + FunctionApprovalResponseContent farc => new FunctionApprovalResponseContent(farc.Id, farc.Approved, (FunctionCallContent)CloneFcc(farc.FunctionCall)) { Reason = farc.Reason }, + _ => c + }; } From 052dbe3381beb7a9ed1573be980f285455182283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Fri, 30 Jan 2026 00:33:18 -0600 Subject: [PATCH 03/30] Add CompatibilitySuppressions.xml --- .../CompatibilitySuppressions.xml | 214 ++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml 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..e4e433ff404 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml @@ -0,0 +1,214 @@ + + + + + CP0001 + T:Microsoft.Extensions.AI.McpServerToolApprovalRequestContent + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0001 + T:Microsoft.Extensions.AI.McpServerToolApprovalResponseContent + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolCallContent.get_ToolName + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolCallContent.get_Arguments + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolResultContent.get_Output + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolResultContent.set_Output(System.Collections.Generic.IList{Microsoft.Extensions.AI.AIContent}) + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0001 + T:Microsoft.Extensions.AI.McpServerToolApprovalRequestContent + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0001 + T:Microsoft.Extensions.AI.McpServerToolApprovalResponseContent + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolCallContent.get_ToolName + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolCallContent.get_Arguments + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolResultContent.get_Output + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolResultContent.set_Output(System.Collections.Generic.IList{Microsoft.Extensions.AI.AIContent}) + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0001 + T:Microsoft.Extensions.AI.McpServerToolApprovalRequestContent + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0001 + T:Microsoft.Extensions.AI.McpServerToolApprovalResponseContent + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolCallContent.get_ToolName + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolCallContent.get_Arguments + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolResultContent.get_Output + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolResultContent.set_Output(System.Collections.Generic.IList{Microsoft.Extensions.AI.AIContent}) + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0001 + T:Microsoft.Extensions.AI.McpServerToolApprovalRequestContent + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0001 + T:Microsoft.Extensions.AI.McpServerToolApprovalResponseContent + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolCallContent.get_ToolName + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolCallContent.get_Arguments + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolResultContent.get_Output + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolResultContent.set_Output(System.Collections.Generic.IList{Microsoft.Extensions.AI.AIContent}) + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + 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 + + + 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.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.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 + + From 59b5ebb77ba724fbf98fe15a23353fea4563f644 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 16:36:49 +0000 Subject: [PATCH 04/30] Fix ToolName->Name and CloneFcc to handle McpServerToolCallContent Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../OpenAIResponseClientIntegrationTests.cs | 4 ++-- .../OpenAIResponseClientTests.cs | 12 ++++++------ .../FunctionInvokingChatClientApprovalsTests.cs | 5 +++++ 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs index 1335ecb45c1..ed079c8dd18 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs @@ -409,7 +409,7 @@ await client.GetStreamingResponseAsync(input, chatOptions).ToChatResponseAsync() input.AddRange(response.Messages); var approvalRequest = Assert.Single(response.Messages.SelectMany(m => m.Contents).OfType()); var mcpCallContent = Assert.IsType(approvalRequest.FunctionCall); - Assert.Equal("search_events", mcpCallContent.ToolName); + Assert.Equal("search_events", mcpCallContent.Name); Assert.Equal("search_events", mcpCallContent.Name); input.Add(new ChatMessage(ChatRole.Tool, [approvalRequest.CreateResponse(true)])); @@ -420,7 +420,7 @@ 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()); Assert.Equal(@"{""events"": [], ""next_page_token"": null}", toolResult.Result); diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs index 068697079c1..ce7692def34 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs @@ -1433,7 +1433,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()); @@ -1687,7 +1687,7 @@ 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()); @@ -1699,7 +1699,7 @@ public async Task McpToolCall_ApprovalNotRequired_NonStreaming(bool rawTool) 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()); @@ -2098,7 +2098,7 @@ 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()); @@ -2110,7 +2110,7 @@ public async Task McpToolCall_ApprovalNotRequired_Streaming() 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()); @@ -2307,7 +2307,7 @@ public async Task McpToolCall_ErrorResponse_NonStreaming(bool rawTool) var toolCall = Assert.IsType(message.Contents[1]); Assert.Equal("mcp_689023b0fa88819f99f48aff343d5ad50475557f6fefb5f0", toolCall.CallId); Assert.Equal("mymcp", toolCall.ServerName); - Assert.Equal("test_error", toolCall.ToolName); + Assert.Equal("test_error", toolCall.Name); Assert.NotNull(toolCall.Arguments); Assert.Empty(toolCall.Arguments); diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs index b43862c3c81..8d08bab75cb 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs @@ -1532,6 +1532,11 @@ private static List CloneInput(List input) => private static AIContent CloneFcc(AIContent c) => c switch { + McpServerToolCallContent mstcc => new McpServerToolCallContent(mstcc.CallId, mstcc.Name, mstcc.ServerName) + { + Arguments = mstcc.Arguments, + InvocationRequired = mstcc.InvocationRequired + }, FunctionCallContent fcc => new FunctionCallContent(fcc.CallId, fcc.Name, fcc.Arguments) { InvocationRequired = fcc.InvocationRequired }, FunctionApprovalRequestContent farc => new FunctionApprovalRequestContent(farc.Id, (FunctionCallContent)CloneFcc(farc.FunctionCall)), FunctionApprovalResponseContent farc => new FunctionApprovalResponseContent(farc.Id, farc.Approved, (FunctionCallContent)CloneFcc(farc.FunctionCall)) { Reason = farc.Reason }, From 500863e37f4bebe66430a3d61cc6a12cf9e0f44d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 16:40:59 +0000 Subject: [PATCH 05/30] Remove duplicate assertion for mcpCallContent.Name Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../OpenAIResponseClientIntegrationTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs index ed079c8dd18..578f8712f56 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs @@ -410,7 +410,6 @@ await client.GetStreamingResponseAsync(input, chatOptions).ToChatResponseAsync() var approvalRequest = Assert.Single(response.Messages.SelectMany(m => m.Contents).OfType()); var mcpCallContent = Assert.IsType(approvalRequest.FunctionCall); Assert.Equal("search_events", mcpCallContent.Name); - Assert.Equal("search_events", mcpCallContent.Name); input.Add(new ChatMessage(ChatRole.Tool, [approvalRequest.CreateResponse(true)])); response = streaming ? From 0ee1ed62e9f8b270eedfb55dbfd19bdf3c5f2b1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Fri, 30 Jan 2026 11:31:05 -0600 Subject: [PATCH 06/30] Update handling of experimental McpServerTool types in sourcegen serialization and tests --- .../Contents/FunctionCallContent.cs | 8 ++++++-- .../Contents/FunctionResultContent.cs | 8 ++++++-- .../Utilities/AIJsonUtilities.Defaults.cs | 7 +++++++ .../Utilities/AIJsonUtilities.cs | 18 ++++++++++++++++-- .../Contents/FunctionCallContentTests.cs | 10 ++++++++-- .../Contents/FunctionResultContentTests.cs | 10 ++++++++-- .../Contents/UserInputResponseContentTests.cs | 2 -- 7 files changed, 51 insertions(+), 12 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs index d7e986a9179..056d5abede8 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs @@ -14,8 +14,12 @@ namespace Microsoft.Extensions.AI; /// Represents a function call request. /// [DebuggerDisplay("{DebuggerDisplay,nq}")] -[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] -[JsonDerivedType(typeof(McpServerToolCallContent), "mcpServerToolCall")] + +// These should be added in once McpServerToolCallContent is no longer [Experimental]. +// If they're included while still experimental, any JsonSerializerContext that includes +// FunctionCallContent will incur errors about using experimental types in its source generated files. +// [JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] +// [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 daf3e23ebd6..fccd15dd0f2 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionResultContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionResultContent.cs @@ -13,8 +13,12 @@ namespace Microsoft.Extensions.AI; /// Represents the result of a function call. /// [DebuggerDisplay("{DebuggerDisplay,nq}")] -[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] -[JsonDerivedType(typeof(McpServerToolResultContent), "mcpServerToolResult")] + +// These should be added in once McpServerToolResultContent is no longer [Experimental]. +// If they're included while still experimental, any JsonSerializerContext that includes +// FunctionResultContent will incur errors about using experimental types in its source generated files. +// [JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] +// [JsonDerivedType(typeof(McpServerToolResultContent), "mcpServerToolResult")] public class FunctionResultContent : AIContent { /// 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 51ed08441f1..38c8e2ce411 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs @@ -58,6 +58,13 @@ private static JsonSerializerOptions CreateDefaultOptions() AddAIContentType(options, typeof(CodeInterpreterToolCallContent), typeDiscriminatorId: "codeInterpreterToolCall", checkBuiltIn: false); AddAIContentType(options, typeof(CodeInterpreterToolResultContent), typeDiscriminatorId: "codeInterpreterToolResult", checkBuiltIn: false); + // Temporary workaround: McpServerToolCallContent/McpServerToolResultContent are [Experimental] and can't be + // added as [JsonDerivedType] on FunctionCallContent/FunctionResultContent yet. Add the polymorphism at runtime. + // Once they're no longer [Experimental], the [JsonPolymorphic] and [JsonDerivedType] attributes should be + // uncommented on FunctionCallContent/FunctionResultContent and these lines removed. + AddDerivedType(options, typeof(McpServerToolCallContent), typeDiscriminatorId: "mcpServerToolCall"); + AddDerivedType(options, typeof(McpServerToolResultContent), typeDiscriminatorId: "mcpServerToolResult"); + if (JsonSerializer.IsReflectionEnabledByDefault) { // If reflection-based serialization is enabled by default, use it as a fallback for all other types. diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.cs index b69d0fb2aab..c538e25377a 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.cs @@ -2,9 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -#if !NET using System.Diagnostics; -#endif using System.IO; using System.Linq; using System.Security.Cryptography; @@ -203,6 +201,22 @@ private static void AddAIContentType(JsonSerializerOptions options, Type content }); } + private static void AddDerivedType(JsonSerializerOptions options, Type contentType, string typeDiscriminatorId) + where TBase : class + { + Debug.Assert(typeof(TBase) == typeof(FunctionCallContent) || typeof(TBase) == typeof(FunctionResultContent), $"Unexpected base type: {typeof(TBase)}"); + + IJsonTypeInfoResolver resolver = options.TypeInfoResolver ?? DefaultOptions.TypeInfoResolver!; + options.TypeInfoResolver = resolver.WithAddedModifier(typeInfo => + { + if (typeInfo.Type == typeof(TBase)) + { + // TypeDiscriminatorPropertyName must be set because TBase doesn't have [JsonPolymorphic] + (typeInfo.PolymorphismOptions ??= new() { TypeDiscriminatorPropertyName = "$type" }).DerivedTypes.Add(new(contentType, typeDiscriminatorId)); + } + }); + } + #if NET /// Provides a stream that writes to an . private sealed class IncrementalHashStream : Stream 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 86975e79d6e..d8e7d41f9b5 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Text.Json; using System.Text.Json.Nodes; +using System.Text.Json.Serialization.Metadata; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -425,8 +426,13 @@ public void Serialization_DerivedTypes_Roundtrips() } // Verify the array roundtrips - var serializedContents = JsonSerializer.Serialize(contents, TestJsonSerializerContext.Default.FunctionCallContentArray); - var deserializedContents = JsonSerializer.Deserialize(serializedContents, TestJsonSerializerContext.Default.FunctionCallContentArray); + // Note: Change back to TestJsonSerializerContext.Default.FunctionCallContentArray once McpServerToolCallContent is no longer [Experimental] + // We need to create new options with reflection support for the array type since TestJsonSerializerContext can't include + // FunctionCallContent[] without also referencing the [Experimental] McpServerToolCallContent type. + var optionsWithArraySupport = new JsonSerializerOptions(AIJsonUtilities.DefaultOptions); + optionsWithArraySupport.TypeInfoResolverChain.Add(new DefaultJsonTypeInfoResolver()); + var serializedContents = JsonSerializer.Serialize(contents, optionsWithArraySupport); + var deserializedContents = JsonSerializer.Deserialize(serializedContents, optionsWithArraySupport); Assert.NotNull(deserializedContents); Assert.Equal(contents.Length, deserializedContents.Length); for (int i = 0; i < deserializedContents.Length; i++) 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 e3f07b01cba..7dbf4888000 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionResultContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionResultContentTests.cs @@ -3,6 +3,7 @@ using System; using System.Text.Json; +using System.Text.Json.Serialization.Metadata; using Xunit; namespace Microsoft.Extensions.AI; @@ -111,8 +112,13 @@ public void Serialization_DerivedTypes_Roundtrips() } // Verify the array roundtrips - var serializedContents = JsonSerializer.Serialize(contents, TestJsonSerializerContext.Default.FunctionResultContentArray); - var deserializedContents = JsonSerializer.Deserialize(serializedContents, TestJsonSerializerContext.Default.FunctionResultContentArray); + // Note: Change back to TestJsonSerializerContext.Default.FunctionResultContentArray once McpServerToolResultContent is no longer [Experimental] + // We need to create new options with reflection support for the array type since TestJsonSerializerContext can't include + // FunctionResultContent[] without also referencing the [Experimental] McpServerToolResultContent type. + var optionsWithArraySupport = new JsonSerializerOptions(AIJsonUtilities.DefaultOptions); + optionsWithArraySupport.TypeInfoResolverChain.Add(new DefaultJsonTypeInfoResolver()); + var serializedContents = JsonSerializer.Serialize(contents, optionsWithArraySupport); + var deserializedContents = JsonSerializer.Deserialize(serializedContents, optionsWithArraySupport); Assert.NotNull(deserializedContents); Assert.Equal(contents.Length, deserializedContents.Length); for (int i = 0; i < deserializedContents.Length; i++) diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputResponseContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputResponseContentTests.cs index b77a828e50c..33eef795507 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputResponseContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputResponseContentTests.cs @@ -34,8 +34,6 @@ public void Serialization_DerivedTypes_Roundtrips() UserInputResponseContent[] contents = [ new FunctionApprovalResponseContent("request123", true, new FunctionCallContent("call123", "functionName")), - - // Uncomment once McpServerToolCallContent is no longer experimental. new FunctionApprovalResponseContent("request456", true, new McpServerToolCallContent("call456", "myTool", "myServer")), ]; From cd19f1e92e56b1d305e733b8f6ec66638cc1aae0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Thu, 5 Feb 2026 18:09:21 -0600 Subject: [PATCH 07/30] Remove User prefix from Input contents and rename id -> requestId --- .../FunctionApprovalRequestContent.cs | 15 ++++----- .../FunctionApprovalResponseContent.cs | 13 ++++---- ...questContent.cs => InputRequestContent.cs} | 18 +++++------ ...onseContent.cs => InputResponseContent.cs} | 18 +++++------ .../Contents/McpServerToolCallContent.cs | 2 -- .../Contents/McpServerToolResultContent.cs | 2 -- .../Utilities/AIJsonUtilities.Defaults.cs | 4 +-- .../OpenAIResponsesChatClient.cs | 6 ++-- .../FunctionInvokingChatClient.cs | 2 +- .../ChatReduction/SummarizingChatReducer.cs | 4 +-- .../FunctionApprovalRequestContentTests.cs | 14 ++++---- .../FunctionApprovalResponseContentTests.cs | 10 +++--- ...ntTests.cs => InputRequestContentTests.cs} | 28 ++++++++-------- ...tTests.cs => InputResponseContentTests.cs} | 28 ++++++++-------- .../TestJsonSerializerContext.cs | 4 +-- ...unctionInvokingChatClientApprovalsTests.cs | 4 +-- .../SummarizingChatReducerTests.cs | 32 +++++++++---------- 17 files changed, 99 insertions(+), 105 deletions(-) rename src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/{UserInputRequestContent.cs => InputRequestContent.cs} (61%) rename src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/{UserInputResponseContent.cs => InputResponseContent.cs} (61%) rename test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/{UserInputRequestContentTests.cs => InputRequestContentTests.cs} (64%) rename test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/{UserInputResponseContentTests.cs => InputResponseContentTests.cs} (63%) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalRequestContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalRequestContent.cs index f5a394cd63d..ac0131caa28 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalRequestContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalRequestContent.cs @@ -12,18 +12,17 @@ namespace Microsoft.Extensions.AI; /// Represents a request for user approval of 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 identifier of this request. /// The function call that requires user approval. + /// 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); } @@ -39,5 +38,5 @@ public FunctionApprovalRequestContent(string id, FunctionCallContent functionCal /// 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 }; + 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..e79cc92e289 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalResponseContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalResponseContent.cs @@ -12,19 +12,18 @@ namespace Microsoft.Extensions.AI; /// Represents a response to a function approval request. /// [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. - /// if the function call is approved; otherwise, . + /// The 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. + /// 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); diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputRequestContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputRequestContent.cs similarity index 61% rename from src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputRequestContent.cs rename to src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputRequestContent.cs index eba17dd8d5f..9b3a5be20f1 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputRequestContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputRequestContent.cs @@ -15,21 +15,21 @@ namespace Microsoft.Extensions.AI; [Experimental(DiagnosticIds.Experiments.AIFunctionApprovals, UrlFormat = DiagnosticIds.UrlFormat)] [JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] [JsonDerivedType(typeof(FunctionApprovalRequestContent), "functionApprovalRequest")] -public class UserInputRequestContent : AIContent +public class InputRequestContent : AIContent { /// - /// Initializes a new instance of the class. + /// 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) + /// The ID that uniquely identifies the user input request/response pair. + /// is . + /// is empty or composed entirely of whitespace. + protected InputRequestContent(string requestId) { - Id = Throw.IfNullOrWhitespace(id); + RequestId = Throw.IfNullOrWhitespace(requestId); } /// /// Gets the ID that uniquely identifies the user input request/response pair. /// - public string Id { get; } -} + public string RequestId { get; } +} \ No newline at end of file diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputResponseContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputResponseContent.cs similarity index 61% rename from src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputResponseContent.cs rename to src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputResponseContent.cs index 65c063e7493..ce76c84dbd3 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputResponseContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputResponseContent.cs @@ -15,21 +15,21 @@ namespace Microsoft.Extensions.AI; [Experimental(DiagnosticIds.Experiments.AIFunctionApprovals, UrlFormat = DiagnosticIds.UrlFormat)] [JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] [JsonDerivedType(typeof(FunctionApprovalResponseContent), "functionApprovalResponse")] -public class UserInputResponseContent : AIContent +public class InputResponseContent : AIContent { /// - /// Initializes a new instance of the class. + /// 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) + /// The ID that uniquely identifies the user input request/response pair. + /// is . + /// is empty or composed entirely of whitespace. + protected InputResponseContent(string requestId) { - Id = Throw.IfNullOrWhitespace(id); + RequestId = Throw.IfNullOrWhitespace(requestId); } /// /// Gets the ID that uniquely identifies the user input request/response pair. /// - public string Id { get; } -} + public string RequestId { get; } +} \ No newline at end of file diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs index a0d357211f3..a4b336d128f 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs @@ -3,7 +3,6 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.Text.Json.Serialization; using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; @@ -27,7 +26,6 @@ public sealed class McpServerToolCallContent : FunctionCallContent /// The MCP server name that hosts the tool. /// or is . /// or is empty or composed entirely of whitespace. - [JsonConstructor] public McpServerToolCallContent(string callId, string name, string? serverName) : base(Throw.IfNullOrWhitespace(callId), Throw.IfNullOrWhitespace(name)) { diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs index 19b685a076e..0000b2fe525 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs @@ -3,7 +3,6 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.Text.Json.Serialization; using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; @@ -25,7 +24,6 @@ public sealed class McpServerToolResultContent : FunctionResultContent /// The tool call ID. /// is . /// is empty or composed entirely of whitespace. - [JsonConstructor] public McpServerToolResultContent(string callId) : base(Throw.IfNullOrWhitespace(callId), result: null) { 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 38c8e2ce411..364b794d35d 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs @@ -128,8 +128,8 @@ private static JsonSerializerOptions CreateDefaultOptions() // 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(InputRequestContent))] + [JsonSerializable(typeof(InputResponseContent))] [JsonSerializable(typeof(FunctionApprovalRequestContent))] [JsonSerializable(typeof(FunctionApprovalResponseContent))] [JsonSerializable(typeof(McpServerToolCallContent))] diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index d9183d30e67..c99b909dfaf 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -848,7 +848,7 @@ internal static IEnumerable ToOpenAIResponseItems(IEnumerable rawRep, - FunctionApprovalResponseContent { FunctionCall: McpServerToolCallContent } funcResp => ResponseItem.CreateMcpApprovalResponseItem(funcResp.Id, funcResp.Approved), + FunctionApprovalResponseContent { FunctionCall: McpServerToolCallContent } funcResp => ResponseItem.CreateMcpApprovalResponseItem(funcResp.RequestId, funcResp.Approved), _ => null }; @@ -931,7 +931,7 @@ internal static IEnumerable ToOpenAIResponseItems(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/Contents/FunctionApprovalRequestContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalRequestContentTests.cs index cc5cc1dd8d9..fada9b3adea 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); @@ -85,7 +85,7 @@ public void Serialization_Roundtrips() var deserializedContent = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions); Assert.NotNull(deserializedContent); - Assert.Equal(content.Id, deserializedContent.Id); + 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..06f6f8d29a1 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,7 +30,7 @@ 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); } @@ -49,7 +49,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/UserInputRequestContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputRequestContentTests.cs similarity index 64% rename from test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputRequestContentTests.cs rename to test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputRequestContentTests.cs index c63773d6d9a..76636a5e879 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputRequestContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputRequestContentTests.cs @@ -6,16 +6,16 @@ using System.Text.Json; using Xunit; -namespace Microsoft.Extensions.AI.Contents; +namespace Microsoft.Extensions.AI; -public class UserInputRequestContentTests +public class InputRequestContentTests { [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 ")); + Assert.Throws("requestId", () => new TestInputRequestContent(null!)); + Assert.Throws("requestId", () => new TestInputRequestContent("")); + Assert.Throws("requestId", () => new TestInputRequestContent("\r\t\n ")); } [Theory] @@ -24,15 +24,15 @@ public void Constructor_InvalidArguments_Throws() [InlineData("!@#")] public void Constructor_Roundtrips(string id) { - TestUserInputRequestContent content = new(id); + TestInputRequestContent content = new(id); - Assert.Equal(id, content.Id); + Assert.Equal(id, content.RequestId); } [Fact] public void Serialization_DerivedTypes_Roundtrips() { - UserInputRequestContent[] contents = + InputRequestContent[] contents = [ new FunctionApprovalRequestContent("request123", new FunctionCallContent("call123", "functionName", new Dictionary { { "param1", 123 } })), new FunctionApprovalRequestContent("request456", new McpServerToolCallContent("call456", "myTool", "myServer")), @@ -42,14 +42,14 @@ public void Serialization_DerivedTypes_Roundtrips() foreach (var content in contents) { var serialized = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions); - var deserialized = JsonSerializer.Deserialize(serialized, 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.UserInputRequestContentArray); - var deserializedContents = JsonSerializer.Deserialize(serializedContents, TestJsonSerializerContext.Default.UserInputRequestContentArray); + 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++) @@ -59,10 +59,10 @@ public void Serialization_DerivedTypes_Roundtrips() } } - private sealed class TestUserInputRequestContent : UserInputRequestContent + private sealed class TestInputRequestContent : InputRequestContent { - public TestUserInputRequestContent(string id) - : base(id) + public TestInputRequestContent(string requestId) + : base(requestId) { } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputResponseContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputResponseContentTests.cs similarity index 63% rename from test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputResponseContentTests.cs rename to test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputResponseContentTests.cs index 33eef795507..3a2b6411538 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputResponseContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputResponseContentTests.cs @@ -5,16 +5,16 @@ using System.Text.Json; using Xunit; -namespace Microsoft.Extensions.AI.Contents; +namespace Microsoft.Extensions.AI; -public class UserInputResponseContentTests +public class InputResponseContentTests { [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 ")); + Assert.Throws("requestId", () => new TestInputResponseContent(null!)); + Assert.Throws("requestId", () => new TestInputResponseContent("")); + Assert.Throws("requestId", () => new TestInputResponseContent("\r\t\n ")); } [Theory] @@ -23,15 +23,15 @@ public void Constructor_InvalidArguments_Throws() [InlineData("!@#")] public void Constructor_Roundtrips(string id) { - TestUserInputResponseContent content = new(id); + TestInputResponseContent content = new(id); - Assert.Equal(id, content.Id); + Assert.Equal(id, content.RequestId); } [Fact] public void Serialization_DerivedTypes_Roundtrips() { - UserInputResponseContent[] contents = + InputResponseContent[] contents = [ new FunctionApprovalResponseContent("request123", true, new FunctionCallContent("call123", "functionName")), new FunctionApprovalResponseContent("request456", true, new McpServerToolCallContent("call456", "myTool", "myServer")), @@ -41,14 +41,14 @@ public void Serialization_DerivedTypes_Roundtrips() foreach (var content in contents) { var serialized = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions); - var deserialized = JsonSerializer.Deserialize(serialized, 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.UserInputResponseContentArray); - var deserializedContents = JsonSerializer.Deserialize(serializedContents, TestJsonSerializerContext.Default.UserInputResponseContentArray); + 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++) @@ -58,10 +58,10 @@ public void Serialization_DerivedTypes_Roundtrips() } } - private class TestUserInputResponseContent : UserInputResponseContent + private class TestInputResponseContent : InputResponseContent { - public TestUserInputResponseContent(string id) - : base(id) + public TestInputResponseContent(string requestId) + : base(requestId) { } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs index 98c4ba273db..0b697566a88 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs @@ -39,8 +39,8 @@ 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[]))] internal sealed partial class TestJsonSerializerContext : JsonSerializerContext; diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs index 8d08bab75cb..7cf236868aa 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs @@ -1538,8 +1538,8 @@ private static List CloneInput(List input) => InvocationRequired = mstcc.InvocationRequired }, FunctionCallContent fcc => new FunctionCallContent(fcc.CallId, fcc.Name, fcc.Arguments) { InvocationRequired = fcc.InvocationRequired }, - FunctionApprovalRequestContent farc => new FunctionApprovalRequestContent(farc.Id, (FunctionCallContent)CloneFcc(farc.FunctionCall)), - FunctionApprovalResponseContent farc => new FunctionApprovalResponseContent(farc.Id, farc.Approved, (FunctionCallContent)CloneFcc(farc.FunctionCall)) { Reason = farc.Reason }, + FunctionApprovalRequestContent farc => new FunctionApprovalRequestContent(farc.RequestId, (FunctionCallContent)CloneFcc(farc.FunctionCall)), + FunctionApprovalResponseContent farc => new FunctionApprovalResponseContent(farc.RequestId, farc.Approved, (FunctionCallContent)CloneFcc(farc.FunctionCall)) { Reason = farc.Reason }, _ => c }; } diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatReduction/SummarizingChatReducerTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatReduction/SummarizingChatReducerTests.cs index 8fa801c4811..5c92abf8a9a 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatReduction/SummarizingChatReducerTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatReduction/SummarizingChatReducerTests.cs @@ -96,15 +96,15 @@ public async Task ReduceAsync_PreservesCompleteToolCallSequence() new ChatMessage(ChatRole.User, "What's the time?"), new ChatMessage(ChatRole.Assistant, "Let me check"), new ChatMessage(ChatRole.User, "What's the weather?"), - new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call1", "get_weather"), new TestUserInputRequestContent("uir1")]), + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call1", "get_weather"), new TestInputRequestContent("uir1")]), new ChatMessage(ChatRole.Tool, [new FunctionResultContent("call1", "Sunny")]), - new ChatMessage(ChatRole.User, [new TestUserInputResponseContent("uir1")]), + new ChatMessage(ChatRole.User, [new TestInputResponseContent("uir1")]), new ChatMessage(ChatRole.Assistant, "It's sunny"), ]; chatClient.GetResponseAsyncCallback = (msgs, _, _) => { - Assert.DoesNotContain(msgs, m => m.Contents.Any(c => c is FunctionCallContent or FunctionResultContent or TestUserInputRequestContent or TestUserInputResponseContent)); + Assert.DoesNotContain(msgs, m => m.Contents.Any(c => c is FunctionCallContent or FunctionResultContent or TestInputRequestContent or TestInputResponseContent)); return Task.FromResult(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Asked about time"))); }; @@ -120,10 +120,10 @@ public async Task ReduceAsync_PreservesCompleteToolCallSequence() m => { Assert.Contains(m.Contents, c => c is FunctionCallContent); - Assert.Contains(m.Contents, c => c is TestUserInputRequestContent); + Assert.Contains(m.Contents, c => c is TestInputRequestContent); }, m => Assert.Contains(m.Contents, c => c is FunctionResultContent), - m => Assert.Contains(m.Contents, c => c is TestUserInputResponseContent), + m => Assert.Contains(m.Contents, c => c is TestInputResponseContent), m => Assert.Contains("sunny", m.Text)); } @@ -185,9 +185,9 @@ public async Task ReduceAsync_ExcludesToolCallsFromSummarizedPortion() List messages = [ new ChatMessage(ChatRole.User, "What's the weather in Seattle?"), - new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call1", "get_weather", new Dictionary { ["location"] = "Seattle" }), new TestUserInputRequestContent("uir2")]), + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call1", "get_weather", new Dictionary { ["location"] = "Seattle" }), new TestInputRequestContent("uir2")]), new ChatMessage(ChatRole.Tool, [new FunctionResultContent("call1", "Sunny, 72°F")]), - new ChatMessage(ChatRole.User, [new TestUserInputResponseContent("uir2")]), + new ChatMessage(ChatRole.User, [new TestInputResponseContent("uir2")]), new ChatMessage(ChatRole.Assistant, "It's sunny and 72°F in Seattle."), new ChatMessage(ChatRole.User, "What about New York?"), new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call2", "get_weather", new Dictionary { ["location"] = "New York" })]), @@ -200,7 +200,7 @@ public async Task ReduceAsync_ExcludesToolCallsFromSummarizedPortion() var msgList = msgs.ToList(); Assert.Equal(4, msgList.Count); // 3 non-function messages + system prompt - Assert.DoesNotContain(msgList, m => m.Contents.Any(c => c is FunctionCallContent or FunctionResultContent or TestUserInputRequestContent or TestUserInputResponseContent)); + Assert.DoesNotContain(msgList, m => m.Contents.Any(c => c is FunctionCallContent or FunctionResultContent or TestInputRequestContent or TestInputResponseContent)); Assert.Contains(msgList, m => m.Text.Contains("What's the weather in Seattle?")); Assert.Contains(msgList, m => m.Text.Contains("sunny and 72°F in Seattle")); Assert.Contains(msgList, m => m.Text.Contains("What about New York?")); @@ -220,8 +220,8 @@ public async Task ReduceAsync_ExcludesToolCallsFromSummarizedPortion() Assert.Contains(resultList, m => m.Contents.Any(c => c is FunctionResultContent fr && fr.CallId == "call2")); Assert.DoesNotContain(resultList, m => m.Contents.Any(c => c is FunctionCallContent fc && fc.CallId == "call1")); Assert.DoesNotContain(resultList, m => m.Contents.Any(c => c is FunctionResultContent fr && fr.CallId == "call1")); - Assert.DoesNotContain(resultList, m => m.Contents.Any(c => c is TestUserInputRequestContent)); - Assert.DoesNotContain(resultList, m => m.Contents.Any(c => c is TestUserInputResponseContent)); + Assert.DoesNotContain(resultList, m => m.Contents.Any(c => c is TestInputRequestContent)); + Assert.DoesNotContain(resultList, m => m.Contents.Any(c => c is TestInputResponseContent)); Assert.DoesNotContain(resultList, m => m.Text.Contains("sunny and 72°F in Seattle")); } @@ -385,18 +385,18 @@ need frequent exercise. The user then asked about whether they're good around ki m => Assert.StartsWith("Do they make good lap dogs", m.Text, StringComparison.Ordinal)); } - private sealed class TestUserInputRequestContent : UserInputRequestContent + private sealed class TestInputRequestContent : InputRequestContent { - public TestUserInputRequestContent(string id) - : base(id) + public TestInputRequestContent(string requestId) + : base(requestId) { } } - private sealed class TestUserInputResponseContent : UserInputResponseContent + private sealed class TestInputResponseContent : InputResponseContent { - public TestUserInputResponseContent(string id) - : base(id) + public TestInputResponseContent(string requestId) + : base(requestId) { } } From d2d6aa29c728170cadb3aaa6be1d9ff95efcbe59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Wed, 4 Feb 2026 13:06:07 -0600 Subject: [PATCH 08/30] Use string.Empty on mcp approval requests --- .../Contents/McpServerToolCallContent.cs | 4 +--- .../Contents/McpServerToolResultContent.cs | 4 +--- .../OpenAIResponsesChatClient.cs | 4 ++-- .../Contents/McpServerToolCallContentTests.cs | 13 ++++++++++--- .../Contents/McpServerToolResultContentTests.cs | 9 ++++++++- 5 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs index a4b336d128f..344e58531fe 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs @@ -4,7 +4,6 @@ using System; using System.Diagnostics.CodeAnalysis; using Microsoft.Shared.DiagnosticIds; -using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI; @@ -25,9 +24,8 @@ public sealed class McpServerToolCallContent : FunctionCallContent /// 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 name, string? serverName) - : base(Throw.IfNullOrWhitespace(callId), Throw.IfNullOrWhitespace(name)) + : base(callId, name) { ServerName = serverName; InvocationRequired = false; diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs index 0000b2fe525..0e9a1b8ebad 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs @@ -4,7 +4,6 @@ using System; using System.Diagnostics.CodeAnalysis; using Microsoft.Shared.DiagnosticIds; -using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI; @@ -23,9 +22,8 @@ public sealed class McpServerToolResultContent : FunctionResultContent /// /// The tool call ID. /// is . - /// is empty or composed entirely of whitespace. public McpServerToolResultContent(string callId) - : base(Throw.IfNullOrWhitespace(callId), result: null) + : base(callId, result: null) { } } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index c99b909dfaf..eea9665ca4f 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -207,7 +207,7 @@ internal static IEnumerable ToChatMessages(IEnumerable break; case McpToolCallApprovalRequestItem mtcari: - yield return CreateUpdate(new FunctionApprovalRequestContent(mtcari.Id, new McpServerToolCallContent(mtcari.Id, mtcari.ToolName, mtcari.ServerLabel) + yield return CreateUpdate(new FunctionApprovalRequestContent(mtcari.Id, new McpServerToolCallContent(string.Empty, mtcari.ToolName, mtcari.ServerLabel) { Arguments = JsonSerializer.Deserialize(mtcari.ToolArguments, OpenAIJsonContext.Default.IDictionaryStringObject), RawRepresentation = mtcari, 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 d8eafdbcdbd..24826504cd8 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolCallContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolCallContentTests.cs @@ -54,13 +54,20 @@ public void Constructor_PropsRoundtrip() [Fact] public void Constructor_Throws() { - Assert.Throws("callId", () => new McpServerToolCallContent(string.Empty, "name", null)); - Assert.Throws("name", () => new McpServerToolCallContent("callId1", string.Empty, null)); - Assert.Throws("callId", () => new McpServerToolCallContent(null!, "name", null)); Assert.Throws("name", () => new McpServerToolCallContent("callId1", null!, null)); } + [Fact] + public void Constructor_EmptyCallId_Accepted() + { + McpServerToolCallContent c = new(string.Empty, "toolName", "serverName"); + + Assert.Equal(string.Empty, c.CallId); + Assert.Equal("toolName", c.Name); + Assert.Equal("serverName", c.ServerName); + } + [Fact] public void Serialization_Roundtrips() { 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 c405c21b300..beea93fd7f3 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolResultContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolResultContentTests.cs @@ -45,10 +45,17 @@ public void Constructor_PropsRoundtrip() [Fact] public void Constructor_Throws() { - Assert.Throws("callId", () => new McpServerToolResultContent(string.Empty)); Assert.Throws("callId", () => new McpServerToolResultContent(null!)); } + [Fact] + public void Constructor_EmptyCallId_Accepted() + { + McpServerToolResultContent c = new(string.Empty); + + Assert.Equal(string.Empty, c.CallId); + } + [Fact] public void Serialization_Roundtrips() { From b81cb33aa8764a35fdd58b63d198625955816267 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Thu, 5 Feb 2026 19:42:16 -0600 Subject: [PATCH 09/30] Remove AuthorizationToken and make Headers settable --- .../Tools/HostedMcpServerTool.cs | 43 +--------- .../OpenAIResponsesChatClient.cs | 11 ++- .../Tools/HostedMcpServerToolTests.cs | 86 +++---------------- .../OpenAIConversionTests.cs | 15 ++-- .../OpenAIResponseClientIntegrationTests.cs | 8 +- .../OpenAIResponseClientTests.cs | 8 +- 6 files changed, 37 insertions(+), 134 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs index ef95d68031c..5078a11c7f6 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs @@ -15,15 +15,9 @@ namespace Microsoft.Extensions.AI; [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. /// @@ -107,39 +101,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 +132,12 @@ 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. /// /// - public IDictionary Headers => _headers ??= new Dictionary(); + public IDictionary? Headers { get; set; } } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index eea9665ca4f..104a8617540 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -600,16 +600,19 @@ 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. + if (mcpTool.Headers?.TryGetValue("Authorization", out string? authHeader) is true && + authHeader?.StartsWith("Bearer ", StringComparison.Ordinal) is true) + { + responsesMcpTool.AuthorizationToken = authHeader.Substring("Bearer ".Length); + } } if (mcpTool.AllowedTools is not null) 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..723daca7325 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() { diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs index 1aa7e1e4d0f..6d0d19a7d4a 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs @@ -386,7 +386,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(); @@ -404,9 +404,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(); @@ -514,11 +517,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(); @@ -527,7 +530,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); } diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs index 578f8712f56..f8efc90860b 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs @@ -372,7 +372,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. " + @@ -389,9 +389,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}" }, } ], }; diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs index ce7692def34..3dc115861b0 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs @@ -2396,11 +2396,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); From 4515843fd48f56b48237cdcbbd53a77f00f2a0ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Thu, 5 Feb 2026 20:23:45 -0600 Subject: [PATCH 10/30] Prefix RequestIds with ficc_ on FunctionInvokingChatClient --- .../OpenAIResponsesChatClient.cs | 6 +- .../FunctionInvokingChatClient.cs | 6 +- .../AssertExtensions.cs | 17 +++ ...unctionInvokingChatClientApprovalsTests.cs | 120 +++++++++--------- .../FunctionInvokingChatClientTests.cs | 10 +- 5 files changed, 89 insertions(+), 70 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index 104a8617540..599ae8e85f9 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -207,7 +207,8 @@ internal static IEnumerable ToChatMessages(IEnumerable break; case McpToolCallApprovalRequestItem mtcari: - yield return CreateUpdate(new FunctionApprovalRequestContent(mtcari.Id, new McpServerToolCallContent(string.Empty, mtcari.ToolName, mtcari.ServerLabel) + // 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, OpenAIJsonContext.Default.IDictionaryStringObject), RawRepresentation = mtcari, diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs index b2af369e0bd..06ca6e58695 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs @@ -1542,7 +1542,7 @@ private static bool CurrentActivityIsInvokeAgent ref List? targetList = ref approvalResponse.Approved ? ref approvedFunctionCalls : ref rejectedFunctionCalls; ChatMessage? requestMessage = null; - _ = allApprovalRequestsMessages?.TryGetValue(approvalResponse.FunctionCall.CallId, 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.InvocationRequired) { updatedContent ??= [.. content]; // Clone the list if we haven't already - updatedContent[i] = new FunctionApprovalRequestContent(fcc.CallId, fcc); + updatedContent[i] = new FunctionApprovalRequestContent($"ficc_{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($"ficc_{functionCall.CallId}", functionCall); outputMessages[messageIndex] = message; lastMessageIndex = messageIndex; 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 { { "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 { { "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("ficc_callId1", false, new FunctionCallContent("callId1", "Func1")) { Reason = "User denied permission for this operation" }, - new FunctionApprovalResponseContent("callId2", false, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new FunctionApprovalResponseContent("ficc_callId2", false, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) { Reason = "Function Func2 is not allowed at this time" } @@ -504,15 +504,15 @@ public async Task MixedApprovalResponsesWithCustomAndDefaultReasonsAsync() 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("callId3", new FunctionCallContent("callId3", "Func3", arguments: new Dictionary { { "s", "test" } })) + new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), + new FunctionApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })), + new FunctionApprovalRequestContent("ficc_callId3", new FunctionCallContent("callId3", "Func3", arguments: new Dictionary { { "s", "test" } })) ]) { MessageId = "resp1" }, new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("callId1", false, new FunctionCallContent("callId1", "Func1")) { Reason = "Custom rejection for Func1" }, - new FunctionApprovalResponseContent("callId2", false, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })), - new FunctionApprovalResponseContent("callId3", true, new FunctionCallContent("callId3", "Func3", arguments: new Dictionary { { "s", "test" } })) + new FunctionApprovalResponseContent("ficc_callId1", false, new FunctionCallContent("callId1", "Func1")) { Reason = "Custom rejection for Func1" }, + new FunctionApprovalResponseContent("ficc_callId2", false, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })), + new FunctionApprovalResponseContent("ficc_callId3", true, new FunctionCallContent("callId3", "Func3", arguments: new Dictionary { { "s", "test" } })) ]), ]; @@ -596,11 +596,11 @@ public async Task RejectedApprovalResponsesWithEmptyOrWhitespaceReasonUsesDefaul 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.User, [ - new FunctionApprovalResponseContent("callId1", false, new FunctionCallContent("callId1", "Func1")) + new FunctionApprovalResponseContent("ficc_callId1", false, new FunctionCallContent("callId1", "Func1")) { Reason = reason }, @@ -654,13 +654,13 @@ public async Task ApprovedInputsAreExecutedAndFunctionResultsAreConvertedAsync() 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 } })) ]), ]; @@ -682,7 +682,7 @@ public async Task ApprovedInputsAreExecutedAndFunctionResultsAreConvertedAsync() new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId1", result: "Result 1"), new FunctionResultContent("callId2", result: "Result 2: 42")]), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 3 } })) + new FunctionApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 3 } })) ]), ]; @@ -708,23 +708,23 @@ public async Task AlreadyExecutedApprovalsAreIgnoredAsync() 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 } })) ]), new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("callId1", "Func1"), new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })]), new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId1", result: "Result 1"), new FunctionResultContent("callId2", result: "Result 2: 42")]), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("callId3", new FunctionCallContent("callId3", "Func1")), + new FunctionApprovalRequestContent("ficc_callId3", new FunctionCallContent("callId3", "Func1")), ]) { MessageId = "resp2" }, new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("callId3", true, new FunctionCallContent("callId3", "Func1")), + new FunctionApprovalResponseContent("ficc_callId3", true, new FunctionCallContent("callId3", "Func1")), ]), ]; @@ -834,7 +834,7 @@ public async Task ApprovalRequestWithoutApprovalResponseThrowsAsync() 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" }, ]; @@ -864,8 +864,8 @@ public async Task ApprovedApprovalResponsesWithoutApprovalRequestAreExecutedAsyn new ChatMessage(ChatRole.User, "hello"), 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 } })) ]), ]; @@ -910,8 +910,8 @@ public async Task FunctionCallContentIsNotPassedToDownstreamServiceWithServiceTh [ 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 } })) ]), ]; @@ -1190,7 +1190,7 @@ public async Task FunctionCallReplacedWithApproval_MixedWithMcpApprovalAsync(boo [ new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func")), + new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func")), new FunctionApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer")) ]) ]; @@ -1220,12 +1220,12 @@ public async Task ApprovedApprovalResponseIsExecuted_MixedWithMcpApprovalAsync(b new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func")), + new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func")), new FunctionApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer")) ]) { MessageId = "resp1" }, new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("callId1", true, new FunctionCallContent("callId1", "Func")), + new FunctionApprovalResponseContent("ficc_callId1", true, new FunctionCallContent("callId1", "Func")), new FunctionApprovalResponseContent("callId2", true, new McpServerToolCallContent("callId2", "McpCall", "myServer")) ]), ]; @@ -1304,12 +1304,12 @@ public async Task RejectedApprovalResponses_MixedWithMcpApprovalAsync(bool useAd new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func")), + new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func")), new FunctionApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer")) ]) { MessageId = "resp1" }, new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("callId1", approveFuncCall, new FunctionCallContent("callId1", "Func")), + new FunctionApprovalResponseContent("ficc_callId1", approveFuncCall, new FunctionCallContent("callId1", "Func")), new FunctionApprovalResponseContent("callId2", approveMcpCall, new McpServerToolCallContent("callId2", "McpCall", "myServer")) ]), ]; diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs index 6cd6e857996..72e7043a15d 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs @@ -3291,7 +3291,7 @@ public async Task LogsFunctionRequiresApproval() new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func1")) + new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")) ]) ]; @@ -3327,11 +3327,11 @@ public async Task LogsProcessingApprovalResponse() new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func1")) + new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")) ]), new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("callId1", true, new FunctionCallContent("callId1", "Func1")) + new FunctionApprovalResponseContent("ficc_callId1", true, new FunctionCallContent("callId1", "Func1")) ]) }; @@ -3364,11 +3364,11 @@ public async Task LogsFunctionRejected() new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func1")) + new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")) ]), new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("callId1", false, new FunctionCallContent("callId1", "Func1")) { Reason = "User denied" } + new FunctionApprovalResponseContent("ficc_callId1", false, new FunctionCallContent("callId1", "Func1")) { Reason = "User denied" } ]) }; From 7fe9c0d6df02f3de453d0597ba327880e543f9d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Thu, 5 Feb 2026 20:29:03 -0600 Subject: [PATCH 11/30] Rename serverUrl -> serverAddress to align with string overload --- .../Tools/HostedMcpServerTool.cs | 30 +++++++++---------- .../Tools/HostedMcpServerToolTests.cs | 4 +-- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs index 5078a11c7f6..118fb33afab 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs @@ -49,12 +49,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)) { } @@ -62,27 +62,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; } /// 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 723daca7325..454fd74a731 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs @@ -127,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!)); } } From b5bb861faefe120cd164c4049e9a145229eb2fc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Thu, 5 Feb 2026 20:45:08 -0600 Subject: [PATCH 12/30] OpenAI: Revert to use ErrorContent and TextContent --- .../OpenAIResponsesChatClient.cs | 4 +++- .../OpenAIResponseClientTests.cs | 19 +++++++------------ 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index 599ae8e85f9..2376c1e9935 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -1314,7 +1314,9 @@ private static void AddMcpToolCallContent(McpToolCallItem mtci, IList contents.Add(new McpServerToolResultContent(mtci.Id) { RawRepresentation = mtci, - Result = mtci.Error ?? (object)mtci.ToolOutput, + Result = mtci.Error is not null ? + new ErrorContent(mtci.Error.ToString()) : + new TextContent(mtci.ToolOutput), }); } diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs index 3dc115861b0..071c7a4d43c 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs @@ -1441,7 +1441,7 @@ public async Task McpToolCall_ApprovalRequired_NonStreaming(string role) var result = Assert.IsType(message.Contents[1]); Assert.Equal("mcp_06ee3b1962eeb8470068e6b21cbaa081a3b5aa2a6c989f4c6f", result.CallId); - Assert.StartsWith("The `README.md` file for `Microsoft.Extensions.AI.Abstractions` is located at", Assert.IsType(result.Result)); + 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); @@ -1694,7 +1694,7 @@ public async Task McpToolCall_ApprovalNotRequired_NonStreaming(bool rawTool) var firstResult = Assert.IsType(message.Contents[2]); Assert.Equal("mcp_68be4166acfc8191bc5e0a751eed358b0384f747588fc3f5", firstResult.CallId); - Assert.StartsWith("Available pages for dotnet/extensions", Assert.IsType(firstResult.Result)); + 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); @@ -1706,7 +1706,7 @@ public async Task McpToolCall_ApprovalNotRequired_NonStreaming(bool rawTool) var secondResult = Assert.IsType(message.Contents[4]); Assert.Equal("mcp_68be416900f88191837ae0718339a4ce0384f747588fc3f5", secondResult.CallId); - Assert.StartsWith("The `README.md` file for `Microsoft.Extensions.AI.Abstractions` is located at", Assert.IsType(secondResult.Result)); + 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); @@ -2105,7 +2105,7 @@ public async Task McpToolCall_ApprovalNotRequired_Streaming() var firstResult = Assert.IsType(message.Contents[2]); Assert.Equal("mcp_68be4503d45c819e89cb574361c8eba003a2537be0e84a54", firstResult.CallId); - Assert.StartsWith("Available pages for dotnet/extensions", Assert.IsType(firstResult.Result)); + 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); @@ -2117,7 +2117,7 @@ public async Task McpToolCall_ApprovalNotRequired_Streaming() var secondResult = Assert.IsType(message.Contents[4]); Assert.Equal("mcp_68be4505f134819e806c002f27cce0c303a2537be0e84a54", secondResult.CallId); - Assert.StartsWith("The path to the `README.md` file", Assert.IsType(secondResult.Result)); + Assert.StartsWith("The path to the `README.md` file", Assert.IsType(secondResult.Result).Text); Assert.NotNull(response.Usage); Assert.Equal(1420, response.Usage.InputTokenCount); @@ -2313,13 +2313,8 @@ public async Task McpToolCall_ErrorResponse_NonStreaming(bool rawTool) var toolResult = Assert.IsType(message.Contents[2]); Assert.Equal("mcp_689023b0fa88819f99f48aff343d5ad50475557f6fefb5f0", toolResult.CallId); - var errorData = Assert.IsType(toolResult.Result); - var errorJson = JsonDocument.Parse(errorData); - Assert.Equal("mcp_tool_execution_error", errorJson.RootElement.GetProperty("type").GetString()); - var contentArray = errorJson.RootElement.GetProperty("content"); - Assert.Equal(1, contentArray.GetArrayLength()); - Assert.Equal("text", contentArray[0].GetProperty("type").GetString()); - Assert.Equal("An error occurred invoking 'test_error'.", contentArray[0].GetProperty("text").GetString()); + 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); From ca5d7d3d6866ef3ef1ca41cd41e244c284cb4c4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Thu, 5 Feb 2026 21:29:31 -0600 Subject: [PATCH 13/30] Remove Experimental --- scripts/MakeApiBaselines.ps1 | 2 +- .../FunctionApprovalRequestContent.cs | 3 - .../FunctionApprovalResponseContent.cs | 3 - .../Contents/InputRequestContent.cs | 3 - .../Contents/InputResponseContent.cs | 3 - .../Contents/McpServerToolCallContent.cs | 3 - .../Contents/McpServerToolResultContent.cs | 3 - .../Functions/ApprovalRequiredAIFunction.cs | 3 - ...dMcpServerToolAlwaysRequireApprovalMode.cs | 3 - .../HostedMcpServerToolApprovalMode.cs | 3 - ...edMcpServerToolNeverRequireApprovalMode.cs | 3 - ...cpServerToolRequireSpecificApprovalMode.cs | 3 - .../Microsoft.Extensions.AI.Abstractions.json | 254 +++++++++++++++++- .../Tools/HostedMcpServerTool.cs | 3 - 14 files changed, 254 insertions(+), 38 deletions(-) diff --git a/scripts/MakeApiBaselines.ps1 b/scripts/MakeApiBaselines.ps1 index dcebd769a3e..f3b77112661 100644 --- a/scripts/MakeApiBaselines.ps1 +++ b/scripts/MakeApiBaselines.ps1 @@ -16,7 +16,7 @@ Write-Output "Installing required toolset" InitializeDotNetCli -install $true | Out-Null $Project = $PSScriptRoot + "/../eng/Tools/ApiChief/ApiChief.csproj" -$Command = $PSScriptRoot + "/../artifacts/bin/ApiChief/Debug/net9.0/ApiChief.dll" +$Command = $PSScriptRoot + "/../artifacts/bin/ApiChief/Debug/net10.0/ApiChief.dll" $LibrariesFolder = $PSScriptRoot + "/../src/Libraries" Write-Output "Building ApiChief tool" diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalRequestContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalRequestContent.cs index ac0131caa28..3dc130bdb51 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalRequestContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalRequestContent.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; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI; @@ -11,7 +9,6 @@ namespace Microsoft.Extensions.AI; /// /// Represents a request for user approval of a function call. /// -[Experimental(DiagnosticIds.Experiments.AIFunctionApprovals, UrlFormat = DiagnosticIds.UrlFormat)] public sealed class FunctionApprovalRequestContent : InputRequestContent { /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalResponseContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalResponseContent.cs index e79cc92e289..d91ddd66111 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalResponseContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalResponseContent.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; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI; @@ -11,7 +9,6 @@ namespace Microsoft.Extensions.AI; /// /// Represents a response to a function approval request. /// -[Experimental(DiagnosticIds.Experiments.AIFunctionApprovals, UrlFormat = DiagnosticIds.UrlFormat)] public sealed class FunctionApprovalResponseContent : InputResponseContent { /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputRequestContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputRequestContent.cs index 9b3a5be20f1..a4000b3cc40 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputRequestContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputRequestContent.cs @@ -2,9 +2,7 @@ // 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; @@ -12,7 +10,6 @@ namespace Microsoft.Extensions.AI; /// /// Represents a request for user input. /// -[Experimental(DiagnosticIds.Experiments.AIFunctionApprovals, UrlFormat = DiagnosticIds.UrlFormat)] [JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] [JsonDerivedType(typeof(FunctionApprovalRequestContent), "functionApprovalRequest")] public class InputRequestContent : AIContent diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputResponseContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputResponseContent.cs index ce76c84dbd3..3bf5ba96e7a 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputResponseContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputResponseContent.cs @@ -2,9 +2,7 @@ // 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; @@ -12,7 +10,6 @@ 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")] public class InputResponseContent : AIContent diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs index 344e58531fe..14c106912fa 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.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; @@ -14,7 +12,6 @@ namespace Microsoft.Extensions.AI; /// This content type is used to represent 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 McpServerToolCallContent : FunctionCallContent { /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs index 0e9a1b8ebad..f34897d80b0 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.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; @@ -14,7 +12,6 @@ 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 : FunctionResultContent { /// 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..03313e0ca73 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,7 +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")] 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 e6ad6cc28b6..830a9d479f3 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 @@ -1,5 +1,5 @@ { - "Name": "Microsoft.Extensions.AI.Abstractions, Version=9.6.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35", + "Name": "Microsoft.Extensions.AI.Abstractions, Version=10.3.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35", "Types": [ { "Type": "sealed class Microsoft.Extensions.AI.AdditionalPropertiesDictionary : Microsoft.Extensions.AI.AdditionalPropertiesDictionary", @@ -727,6 +727,16 @@ } ] }, + { + "Type": "sealed class Microsoft.Extensions.AI.ApprovalRequiredAIFunction : Microsoft.Extensions.AI.DelegatingAIFunction", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.ApprovalRequiredAIFunction.ApprovalRequiredAIFunction(Microsoft.Extensions.AI.AIFunction innerFunction);", + "Stage": "Stable" + } + ] + }, { "Type": "sealed class Microsoft.Extensions.AI.AutoChatToolMode : Microsoft.Extensions.AI.ChatToolMode", "Stage": "Stable", @@ -1791,6 +1801,50 @@ } ] }, + { + "Type": "sealed class Microsoft.Extensions.AI.FunctionApprovalRequestContent : Microsoft.Extensions.AI.InputRequestContent", + "Stage": "Stable", + "Methods": [ + { + "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": "Stable" + } + ], + "Properties": [ + { + "Member": "Microsoft.Extensions.AI.FunctionCallContent Microsoft.Extensions.AI.FunctionApprovalRequestContent.FunctionCall { get; }", + "Stage": "Stable" + } + ] + }, + { + "Type": "sealed class Microsoft.Extensions.AI.FunctionApprovalResponseContent : Microsoft.Extensions.AI.InputResponseContent", + "Stage": "Stable", + "Methods": [ + { + "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": "Stable" + }, + { + "Member": "Microsoft.Extensions.AI.FunctionCallContent Microsoft.Extensions.AI.FunctionApprovalResponseContent.FunctionCall { get; }", + "Stage": "Stable" + }, + { + "Member": "string? Microsoft.Extensions.AI.FunctionApprovalResponseContent.Reason { get; set; }", + "Stage": "Stable" + } + ] + }, { "Type": "class Microsoft.Extensions.AI.FunctionCallContent : Microsoft.Extensions.AI.AIContent", "Stage": "Stable", @@ -1987,6 +2041,146 @@ } ] }, + { + "Type": "class Microsoft.Extensions.AI.HostedMcpServerTool : Microsoft.Extensions.AI.AITool", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.HostedMcpServerTool.HostedMcpServerTool(string serverName, string serverAddress);", + "Stage": "Stable" + }, + { + "Member": "Microsoft.Extensions.AI.HostedMcpServerTool.HostedMcpServerTool(string serverName, string serverAddress, System.Collections.Generic.IReadOnlyDictionary? additionalProperties);", + "Stage": "Stable" + }, + { + "Member": "Microsoft.Extensions.AI.HostedMcpServerTool.HostedMcpServerTool(string serverName, System.Uri serverAddress);", + "Stage": "Stable" + }, + { + "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": "Stable" + }, + { + "Member": "System.Collections.Generic.IList? Microsoft.Extensions.AI.HostedMcpServerTool.AllowedTools { get; set; }", + "Stage": "Stable" + }, + { + "Member": "Microsoft.Extensions.AI.HostedMcpServerToolApprovalMode? Microsoft.Extensions.AI.HostedMcpServerTool.ApprovalMode { get; set; }", + "Stage": "Stable" + }, + { + "Member": "System.Collections.Generic.IDictionary? Microsoft.Extensions.AI.HostedMcpServerTool.Headers { get; set; }", + "Stage": "Stable" + }, + { + "Member": "override string Microsoft.Extensions.AI.HostedMcpServerTool.Name { get; }", + "Stage": "Stable" + }, + { + "Member": "string Microsoft.Extensions.AI.HostedMcpServerTool.ServerAddress { get; }", + "Stage": "Stable" + }, + { + "Member": "string? Microsoft.Extensions.AI.HostedMcpServerTool.ServerDescription { get; set; }", + "Stage": "Stable" + }, + { + "Member": "string Microsoft.Extensions.AI.HostedMcpServerTool.ServerName { get; }", + "Stage": "Stable" + } + ] + }, + { + "Type": "sealed class Microsoft.Extensions.AI.HostedMcpServerToolAlwaysRequireApprovalMode : Microsoft.Extensions.AI.HostedMcpServerToolApprovalMode", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.HostedMcpServerToolAlwaysRequireApprovalMode.HostedMcpServerToolAlwaysRequireApprovalMode();", + "Stage": "Stable" + }, + { + "Member": "override bool Microsoft.Extensions.AI.HostedMcpServerToolAlwaysRequireApprovalMode.Equals(object? obj);", + "Stage": "Stable" + }, + { + "Member": "override int Microsoft.Extensions.AI.HostedMcpServerToolAlwaysRequireApprovalMode.GetHashCode();", + "Stage": "Stable" + } + ] + }, + { + "Type": "class Microsoft.Extensions.AI.HostedMcpServerToolApprovalMode", + "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": "Stable" + } + ], + "Properties": [ + { + "Member": "static Microsoft.Extensions.AI.HostedMcpServerToolAlwaysRequireApprovalMode Microsoft.Extensions.AI.HostedMcpServerToolApprovalMode.AlwaysRequire { get; }", + "Stage": "Stable" + }, + { + "Member": "static Microsoft.Extensions.AI.HostedMcpServerToolNeverRequireApprovalMode Microsoft.Extensions.AI.HostedMcpServerToolApprovalMode.NeverRequire { get; }", + "Stage": "Stable" + } + ] + }, + { + "Type": "sealed class Microsoft.Extensions.AI.HostedMcpServerToolNeverRequireApprovalMode : Microsoft.Extensions.AI.HostedMcpServerToolApprovalMode", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.HostedMcpServerToolNeverRequireApprovalMode.HostedMcpServerToolNeverRequireApprovalMode();", + "Stage": "Stable" + }, + { + "Member": "override bool Microsoft.Extensions.AI.HostedMcpServerToolNeverRequireApprovalMode.Equals(object? obj);", + "Stage": "Stable" + }, + { + "Member": "override int Microsoft.Extensions.AI.HostedMcpServerToolNeverRequireApprovalMode.GetHashCode();", + "Stage": "Stable" + } + ] + }, + { + "Type": "sealed class Microsoft.Extensions.AI.HostedMcpServerToolRequireSpecificApprovalMode : Microsoft.Extensions.AI.HostedMcpServerToolApprovalMode", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.HostedMcpServerToolRequireSpecificApprovalMode.HostedMcpServerToolRequireSpecificApprovalMode(System.Collections.Generic.IList? alwaysRequireApprovalToolNames, System.Collections.Generic.IList? neverRequireApprovalToolNames);", + "Stage": "Stable" + }, + { + "Member": "override bool Microsoft.Extensions.AI.HostedMcpServerToolRequireSpecificApprovalMode.Equals(object? obj);", + "Stage": "Stable" + }, + { + "Member": "override int Microsoft.Extensions.AI.HostedMcpServerToolRequireSpecificApprovalMode.GetHashCode();", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "System.Collections.Generic.IList? Microsoft.Extensions.AI.HostedMcpServerToolRequireSpecificApprovalMode.AlwaysRequireApprovalToolNames { get; set; }", + "Stage": "Stable" + }, + { + "Member": "System.Collections.Generic.IList? Microsoft.Extensions.AI.HostedMcpServerToolRequireSpecificApprovalMode.NeverRequireApprovalToolNames { get; set; }", + "Stage": "Stable" + } + ] + }, { "Type": "class Microsoft.Extensions.AI.HostedWebSearchTool : Microsoft.Extensions.AI.AITool", "Stage": "Stable", @@ -2093,6 +2287,64 @@ } ] }, + { + "Type": "class Microsoft.Extensions.AI.InputRequestContent : Microsoft.Extensions.AI.AIContent", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.InputRequestContent.InputRequestContent(string requestId);", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "string Microsoft.Extensions.AI.InputRequestContent.RequestId { get; }", + "Stage": "Stable" + } + ] + }, + { + "Type": "class Microsoft.Extensions.AI.InputResponseContent : Microsoft.Extensions.AI.AIContent", + "Stage": "Stable", + "Methods": [ + { + "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.McpServerToolCallContent : Microsoft.Extensions.AI.FunctionCallContent", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.McpServerToolCallContent.McpServerToolCallContent(string callId, string name, string? serverName);", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "string? Microsoft.Extensions.AI.McpServerToolCallContent.ServerName { get; }", + "Stage": "Stable" + } + ] + }, + { + "Type": "sealed class Microsoft.Extensions.AI.McpServerToolResultContent : Microsoft.Extensions.AI.FunctionResultContent", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.McpServerToolResultContent.McpServerToolResultContent(string callId);", + "Stage": "Stable" + } + ] + }, { "Type": "sealed class Microsoft.Extensions.AI.NoneChatToolMode : Microsoft.Extensions.AI.ChatToolMode", "Stage": "Stable", diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs index 118fb33afab..58e76b27bb4 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,7 +10,6 @@ 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 { /// Any additional properties associated with the tool. From 23eed812324dda2fcbe42824cd98c58c1373d9ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Thu, 5 Feb 2026 21:46:02 -0600 Subject: [PATCH 14/30] Update CompatibilitySuppressions.xml --- .../CompatibilitySuppressions.xml | 229 +++++++++++++++++- 1 file changed, 222 insertions(+), 7 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml index e4e433ff404..d266b9d2d4e 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml @@ -1,6 +1,7 @@ - - + + + CP0001 T:Microsoft.Extensions.AI.McpServerToolApprovalRequestContent @@ -15,9 +16,30 @@ 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.McpServerToolCallContent.get_ToolName + 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 @@ -29,6 +51,13 @@ 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 @@ -43,6 +72,21 @@ 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 @@ -57,9 +101,30 @@ 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.McpServerToolCallContent.get_ToolName + 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 @@ -71,6 +136,13 @@ 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 @@ -85,6 +157,21 @@ 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 @@ -99,9 +186,30 @@ 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.McpServerToolCallContent.get_ToolName + 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 @@ -113,6 +221,13 @@ 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 @@ -127,6 +242,21 @@ 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 @@ -141,9 +271,30 @@ 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.McpServerToolCallContent.get_ToolName + 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 @@ -155,6 +306,13 @@ 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 @@ -169,6 +327,21 @@ 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 @@ -183,9 +356,30 @@ 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.McpServerToolCallContent.get_ToolName + 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 @@ -197,6 +391,13 @@ 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 @@ -211,4 +412,18 @@ 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 + From 4cc083c79de8cc9ebc2d58ecdf7f91bb084ab5db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Thu, 5 Feb 2026 22:29:32 -0600 Subject: [PATCH 15/30] Cleanup JsonPolymorphic for Stabilized types --- .../Contents/AIContent.cs | 8 +++---- .../Contents/FunctionCallContent.cs | 8 ++----- .../Contents/FunctionResultContent.cs | 8 ++----- .../Contents/InputRequestContent.cs | 2 +- .../Contents/InputResponseContent.cs | 2 +- .../HostedMcpServerToolApprovalMode.cs | 2 +- .../Utilities/AIJsonUtilities.Defaults.cs | 24 ++++++------------- .../Utilities/AIJsonUtilities.cs | 18 ++------------ .../Contents/FunctionCallContentTests.cs | 10 ++------ .../Contents/FunctionResultContentTests.cs | 10 ++------ 10 files changed, 24 insertions(+), 68 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs index ffdf33f7645..55d01b2d310 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs @@ -18,16 +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(CodeInterpreterToolCallContent), typeDiscriminator: "codeInterpreterToolCall")] // [JsonDerivedType(typeof(CodeInterpreterToolResultContent), typeDiscriminator: "codeInterpreterToolResult")] diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs index 056d5abede8..2ad3342afb2 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs @@ -14,12 +14,8 @@ namespace Microsoft.Extensions.AI; /// Represents a function call request. /// [DebuggerDisplay("{DebuggerDisplay,nq}")] - -// These should be added in once McpServerToolCallContent is no longer [Experimental]. -// If they're included while still experimental, any JsonSerializerContext that includes -// FunctionCallContent will incur errors about using experimental types in its source generated files. -// [JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] -// [JsonDerivedType(typeof(McpServerToolCallContent), "mcpServerToolCall")] +[JsonPolymorphic] +[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 fccd15dd0f2..0a11bde3cf1 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionResultContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionResultContent.cs @@ -13,12 +13,8 @@ namespace Microsoft.Extensions.AI; /// Represents the result of a function call. /// [DebuggerDisplay("{DebuggerDisplay,nq}")] - -// These should be added in once McpServerToolResultContent is no longer [Experimental]. -// If they're included while still experimental, any JsonSerializerContext that includes -// FunctionResultContent will incur errors about using experimental types in its source generated files. -// [JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] -// [JsonDerivedType(typeof(McpServerToolResultContent), "mcpServerToolResult")] +[JsonPolymorphic] +[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 index a4000b3cc40..60295f03f55 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputRequestContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputRequestContent.cs @@ -10,7 +10,7 @@ namespace Microsoft.Extensions.AI; /// /// Represents a request for user input. /// -[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] +[JsonPolymorphic] [JsonDerivedType(typeof(FunctionApprovalRequestContent), "functionApprovalRequest")] public class InputRequestContent : AIContent { diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputResponseContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputResponseContent.cs index 3bf5ba96e7a..4625114b0f9 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputResponseContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputResponseContent.cs @@ -10,7 +10,7 @@ namespace Microsoft.Extensions.AI; /// /// Represents the response to a request for user input. /// -[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] +[JsonPolymorphic] [JsonDerivedType(typeof(FunctionApprovalResponseContent), "functionApprovalResponse")] public class InputResponseContent : AIContent { diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolApprovalMode.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolApprovalMode.cs index 03313e0ca73..0647fefa083 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolApprovalMode.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolApprovalMode.cs @@ -13,7 +13,7 @@ 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 . /// -[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] +[JsonPolymorphic] [JsonDerivedType(typeof(HostedMcpServerToolNeverRequireApprovalMode), typeDiscriminator: "never")] [JsonDerivedType(typeof(HostedMcpServerToolAlwaysRequireApprovalMode), typeDiscriminator: "always")] [JsonDerivedType(typeof(HostedMcpServerToolRequireSpecificApprovalMode), typeDiscriminator: "requireSpecific")] 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 364b794d35d..f7e6274ebee 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs @@ -51,20 +51,9 @@ 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(CodeInterpreterToolCallContent), typeDiscriminatorId: "codeInterpreterToolCall", checkBuiltIn: false); AddAIContentType(options, typeof(CodeInterpreterToolResultContent), typeDiscriminatorId: "codeInterpreterToolResult", checkBuiltIn: false); - // Temporary workaround: McpServerToolCallContent/McpServerToolResultContent are [Experimental] and can't be - // added as [JsonDerivedType] on FunctionCallContent/FunctionResultContent yet. Add the polymorphism at runtime. - // Once they're no longer [Experimental], the [JsonPolymorphic] and [JsonDerivedType] attributes should be - // uncommented on FunctionCallContent/FunctionResultContent and these lines removed. - AddDerivedType(options, typeof(McpServerToolCallContent), typeDiscriminatorId: "mcpServerToolCall"); - AddDerivedType(options, typeof(McpServerToolResultContent), typeDiscriminatorId: "mcpServerToolResult"); - if (JsonSerializer.IsReflectionEnabledByDefault) { // If reflection-based serialization is enabled by default, use it as a fallback for all other types. @@ -126,14 +115,15 @@ private static JsonSerializerOptions CreateDefaultOptions() [JsonSerializable(typeof(AIContent))] [JsonSerializable(typeof(IEnumerable))] - // Temporary workaround: These should be implicitly added in once they're no longer [Experimental] - // and are included via [JsonDerivedType] on AIContent. + // 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))] - [JsonSerializable(typeof(FunctionApprovalRequestContent))] - [JsonSerializable(typeof(FunctionApprovalResponseContent))] - [JsonSerializable(typeof(McpServerToolCallContent))] - [JsonSerializable(typeof(McpServerToolResultContent))] + + // Temporary workaround: These should be implicitly added in once they're no longer [Experimental] + // and are included via [JsonDerivedType] on AIContent. [JsonSerializable(typeof(CodeInterpreterToolCallContent))] [JsonSerializable(typeof(CodeInterpreterToolResultContent))] [JsonSerializable(typeof(ResponseContinuationToken))] diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.cs index c538e25377a..b69d0fb2aab 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.cs @@ -2,7 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +#if !NET using System.Diagnostics; +#endif using System.IO; using System.Linq; using System.Security.Cryptography; @@ -201,22 +203,6 @@ private static void AddAIContentType(JsonSerializerOptions options, Type content }); } - private static void AddDerivedType(JsonSerializerOptions options, Type contentType, string typeDiscriminatorId) - where TBase : class - { - Debug.Assert(typeof(TBase) == typeof(FunctionCallContent) || typeof(TBase) == typeof(FunctionResultContent), $"Unexpected base type: {typeof(TBase)}"); - - IJsonTypeInfoResolver resolver = options.TypeInfoResolver ?? DefaultOptions.TypeInfoResolver!; - options.TypeInfoResolver = resolver.WithAddedModifier(typeInfo => - { - if (typeInfo.Type == typeof(TBase)) - { - // TypeDiscriminatorPropertyName must be set because TBase doesn't have [JsonPolymorphic] - (typeInfo.PolymorphismOptions ??= new() { TypeDiscriminatorPropertyName = "$type" }).DerivedTypes.Add(new(contentType, typeDiscriminatorId)); - } - }); - } - #if NET /// Provides a stream that writes to an . private sealed class IncrementalHashStream : Stream 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 d8e7d41f9b5..1e4a05a9cd4 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests.cs @@ -7,7 +7,6 @@ using System.Linq; using System.Text.Json; using System.Text.Json.Nodes; -using System.Text.Json.Serialization.Metadata; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -426,13 +425,8 @@ public void Serialization_DerivedTypes_Roundtrips() } // Verify the array roundtrips - // Note: Change back to TestJsonSerializerContext.Default.FunctionCallContentArray once McpServerToolCallContent is no longer [Experimental] - // We need to create new options with reflection support for the array type since TestJsonSerializerContext can't include - // FunctionCallContent[] without also referencing the [Experimental] McpServerToolCallContent type. - var optionsWithArraySupport = new JsonSerializerOptions(AIJsonUtilities.DefaultOptions); - optionsWithArraySupport.TypeInfoResolverChain.Add(new DefaultJsonTypeInfoResolver()); - var serializedContents = JsonSerializer.Serialize(contents, optionsWithArraySupport); - var deserializedContents = JsonSerializer.Deserialize(serializedContents, optionsWithArraySupport); + 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++) 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 7dbf4888000..694749d2528 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionResultContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionResultContentTests.cs @@ -3,7 +3,6 @@ using System; using System.Text.Json; -using System.Text.Json.Serialization.Metadata; using Xunit; namespace Microsoft.Extensions.AI; @@ -112,13 +111,8 @@ public void Serialization_DerivedTypes_Roundtrips() } // Verify the array roundtrips - // Note: Change back to TestJsonSerializerContext.Default.FunctionResultContentArray once McpServerToolResultContent is no longer [Experimental] - // We need to create new options with reflection support for the array type since TestJsonSerializerContext can't include - // FunctionResultContent[] without also referencing the [Experimental] McpServerToolResultContent type. - var optionsWithArraySupport = new JsonSerializerOptions(AIJsonUtilities.DefaultOptions); - optionsWithArraySupport.TypeInfoResolverChain.Add(new DefaultJsonTypeInfoResolver()); - var serializedContents = JsonSerializer.Serialize(contents, optionsWithArraySupport); - var deserializedContents = JsonSerializer.Deserialize(serializedContents, optionsWithArraySupport); + 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++) From f5e3b37b0385b415997d1402a5559606e63e2dff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Thu, 5 Feb 2026 23:40:08 -0600 Subject: [PATCH 16/30] Update test --- .../OpenAIResponseClientIntegrationTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs index f8efc90860b..c07bb0a6dd4 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs @@ -422,7 +422,8 @@ await client.GetStreamingResponseAsync(input, chatOptions).ToChatResponseAsync() Assert.Equal("search_events", toolCall.Name); var toolResult = Assert.Single(response.Messages.SelectMany(m => m.Contents).OfType()); - Assert.Equal(@"{""events"": [], ""next_page_token"": null}", toolResult.Result); + var content = Assert.IsType(toolResult.Result); + Assert.Equal(@"{""events"": [], ""next_page_token"": null}", content.Text); } } From 9a950b3a9542175ea62ce228af83f86967f7ac09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Fri, 6 Feb 2026 00:31:14 -0600 Subject: [PATCH 17/30] Fix merge errors --- .../Contents/McpServerToolCallContent.cs | 2 +- .../OpenAIResponsesChatClient.cs | 4 +- .../FunctionInvokingChatClient.cs | 4 +- .../ChatCompletion/OpenTelemetryChatClient.cs | 68 +++++++++---------- 4 files changed, 39 insertions(+), 39 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs index 14c106912fa..d4af4a7bc94 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs @@ -25,7 +25,7 @@ public McpServerToolCallContent(string callId, string name, string? serverName) : base(callId, name) { ServerName = serverName; - InvocationRequired = false; + InformationalOnly = true; } /// diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index dd108eb6089..9e799366e52 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -1129,9 +1129,9 @@ static FunctionCallOutputResponseItem SerializeAIContent(string callId, IEnumera AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IDictionary))))); break; - case FunctionApprovalRequestContent funcResp when funcResp.FunctionCall is McpServerToolCallContent mcpToolCall: + case FunctionApprovalRequestContent funcReq when funcReq.FunctionCall is McpServerToolCallContent mcpToolCall: yield return ResponseItem.CreateMcpApprovalRequestItem( - funcResp.RequestId, + funcReq.RequestId, mcpToolCall.ServerName, mcpToolCall.Name, BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes( diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs index 40e25aa631d..8b1235a61dc 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 when farc.FunctionCall.InvocationRequired: + 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.RequestId, message); break; - case FunctionApprovalResponseContent farc when farc.FunctionCall.InvocationRequired: + 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); diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs index 4bc1ec54982..63fdc1d3dfc 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 From 66e28b1b60237bf6495161225a6ea0acf8ec98bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Fri, 6 Feb 2026 00:40:16 -0600 Subject: [PATCH 18/30] Revert "Use string.Empty on mcp approval requests" This reverts commit d2d6aa29c728170cadb3aaa6be1d9ff95efcbe59. --- .../Contents/McpServerToolCallContent.cs | 4 +++- .../Contents/McpServerToolResultContent.cs | 4 +++- .../Contents/McpServerToolCallContentTests.cs | 13 +++---------- .../Contents/McpServerToolResultContentTests.cs | 9 +-------- 4 files changed, 10 insertions(+), 20 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs index d4af4a7bc94..9fbf8420ad6 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI; @@ -21,8 +22,9 @@ public sealed class McpServerToolCallContent : FunctionCallContent /// 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 name, string? serverName) - : base(callId, name) + : base(Throw.IfNullOrWhitespace(callId), Throw.IfNullOrWhitespace(name)) { ServerName = serverName; InformationalOnly = true; diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs index f34897d80b0..694b86d4e43 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI; @@ -19,8 +20,9 @@ public sealed class McpServerToolResultContent : FunctionResultContent /// /// The tool call ID. /// is . + /// is empty or composed entirely of whitespace. public McpServerToolResultContent(string callId) - : base(callId, result: null) + : base(Throw.IfNullOrWhitespace(callId), result: null) { } } 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 24826504cd8..d8eafdbcdbd 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolCallContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolCallContentTests.cs @@ -54,20 +54,13 @@ public void Constructor_PropsRoundtrip() [Fact] public void Constructor_Throws() { + Assert.Throws("callId", () => new McpServerToolCallContent(string.Empty, "name", null)); + Assert.Throws("name", () => new McpServerToolCallContent("callId1", string.Empty, null)); + Assert.Throws("callId", () => new McpServerToolCallContent(null!, "name", null)); Assert.Throws("name", () => new McpServerToolCallContent("callId1", null!, null)); } - [Fact] - public void Constructor_EmptyCallId_Accepted() - { - McpServerToolCallContent c = new(string.Empty, "toolName", "serverName"); - - Assert.Equal(string.Empty, c.CallId); - Assert.Equal("toolName", c.Name); - Assert.Equal("serverName", c.ServerName); - } - [Fact] public void Serialization_Roundtrips() { 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 beea93fd7f3..c405c21b300 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolResultContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolResultContentTests.cs @@ -45,17 +45,10 @@ public void Constructor_PropsRoundtrip() [Fact] public void Constructor_Throws() { + Assert.Throws("callId", () => new McpServerToolResultContent(string.Empty)); Assert.Throws("callId", () => new McpServerToolResultContent(null!)); } - [Fact] - public void Constructor_EmptyCallId_Accepted() - { - McpServerToolResultContent c = new(string.Empty); - - Assert.Equal(string.Empty, c.CallId); - } - [Fact] public void Serialization_Roundtrips() { From 8579d3c4355d719293b82c93cd92ff8c6e4411ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Fri, 6 Feb 2026 00:58:57 -0600 Subject: [PATCH 19/30] Fix more merge errors --- .../Contents/McpServerToolCallContentTests.cs | 7 +++++-- ...FunctionInvokingChatClientApprovalsTests.cs | 4 ++-- .../OpenTelemetryChatClientTests.cs | 18 ++++++++---------- 3 files changed, 15 insertions(+), 14 deletions(-) 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 d8eafdbcdbd..9e73af5df34 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolCallContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolCallContentTests.cs @@ -22,7 +22,7 @@ public void Constructor_PropsDefault() Assert.Equal("toolName", c.Name); Assert.Null(c.ServerName); Assert.Null(c.Arguments); - Assert.False(c.InvocationRequired); + Assert.True(c.InformationalOnly); } [Fact] @@ -45,10 +45,13 @@ public void Constructor_PropsRoundtrip() 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.Name); Assert.Equal("serverName", c.ServerName); - Assert.False(c.InvocationRequired); } [Fact] diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs index e10704dbe42..120646134c7 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs @@ -1584,9 +1584,9 @@ private static List CloneInput(List input) => McpServerToolCallContent mstcc => new McpServerToolCallContent(mstcc.CallId, mstcc.Name, mstcc.ServerName) { Arguments = mstcc.Arguments, - InvocationRequired = mstcc.InvocationRequired + InformationalOnly = mstcc.InformationalOnly }, - FunctionCallContent fcc => new FunctionCallContent(fcc.CallId, fcc.Name, fcc.Arguments) { InvocationRequired = fcc.InvocationRequired }, + FunctionCallContent fcc => new FunctionCallContent(fcc.CallId, fcc.Name, fcc.Arguments) { InformationalOnly = fcc.InformationalOnly }, FunctionApprovalRequestContent farc => new FunctionApprovalRequestContent(farc.RequestId, (FunctionCallContent)CloneFcc(farc.FunctionCall)), FunctionApprovalResponseContent farc => new FunctionApprovalResponseContent(farc.RequestId, farc.Approved, (FunctionCallContent)CloneFcc(farc.FunctionCall)) { Reason = farc.Reason }, _ => c diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/OpenTelemetryChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/OpenTelemetryChatClientTests.cs index 3f1c9f59bce..fbe78145f91 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/OpenTelemetryChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/OpenTelemetryChatClientTests.cs @@ -615,7 +615,7 @@ public async Task ServerToolCallContentTypes_SerializedCorrectly(bool streaming) new ImageGenerationToolCallContent { ImageId = "img-123" }, new ImageGenerationToolResultContent { ImageId = "img-123", Outputs = [new UriContent(new Uri("https://example.com/image.png"), "image/png")] }, new McpServerToolCallContent("mcp-call-1", "myTool", "myServer") { Arguments = new Dictionary { ["param1"] = "value1" } }, - new McpServerToolResultContent("mcp-call-1") { Output = [new TextContent("Tool result")] }, + new McpServerToolResultContent("mcp-call-1") { Result = new TextContent("Tool result") }, ])); }, GetStreamingResponseAsyncCallback = CallbackAsync, @@ -631,7 +631,7 @@ async static IAsyncEnumerable CallbackAsync( yield return new() { Contents = [new ImageGenerationToolCallContent { ImageId = "img-123" }] }; yield return new() { Contents = [new ImageGenerationToolResultContent { ImageId = "img-123", Outputs = [new UriContent(new Uri("https://example.com/image.png"), "image/png")] }] }; yield return new() { Contents = [new McpServerToolCallContent("mcp-call-1", "myTool", "myServer") { Arguments = new Dictionary { ["param1"] = "value1" } }] }; - yield return new() { Contents = [new McpServerToolResultContent("mcp-call-1") { Output = [new TextContent("Tool result")] }] }; + yield return new() { Contents = [new McpServerToolResultContent("mcp-call-1") { Result = new TextContent("Tool result") }] }; } using var chatClient = innerClient @@ -734,12 +734,10 @@ async static IAsyncEnumerable CallbackAsync( "id": "mcp-call-1", "server_tool_call_response": { "type": "mcp", - "output": [ - { - "$type": "text", - "text": "Tool result" - } - ] + "output": { + "$type": "text", + "text": "Tool result" + } } } ] @@ -785,11 +783,11 @@ public async Task McpServerToolApprovalContentTypes_SerializedCorrectly() [ new(ChatRole.Assistant, [ - new McpServerToolApprovalRequestContent("approval-1", toolCall), + new FunctionApprovalRequestContent("approval-1", toolCall), ]), new(ChatRole.User, [ - new McpServerToolApprovalResponseContent("approval-1", true), + new FunctionApprovalResponseContent("approval-1", true, toolCall), ]), ]; From 2d8bd4f3fea2910681b348f8d294580906bd8691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Fri, 6 Feb 2026 01:18:40 -0600 Subject: [PATCH 20/30] Improve docs --- .../Contents/FunctionApprovalRequestContent.cs | 11 ++++++----- .../Contents/FunctionApprovalResponseContent.cs | 11 ++++++----- .../Contents/InputRequestContent.cs | 8 ++++---- .../Contents/InputResponseContent.cs | 8 ++++---- 4 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalRequestContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalRequestContent.cs index 3dc130bdb51..f884d6ee187 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalRequestContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalRequestContent.cs @@ -7,14 +7,15 @@ namespace Microsoft.Extensions.AI; /// -/// Represents a request for user approval of a function call. +/// Represents a request for approval before invoking a function call. /// public sealed class FunctionApprovalRequestContent : InputRequestContent { /// /// Initializes a new instance of the class. /// - /// The identifier of this request. /// The function call that requires user approval. + /// The unique identifier that correlates this request with its corresponding response. + /// The function call that requires approval before execution. /// is . /// is empty or composed entirely of whitespace. /// is . @@ -25,15 +26,15 @@ public FunctionApprovalRequestContent(string requestId, FunctionCallContent func } /// - /// 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. + /// 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 d91ddd66111..04f12309b37 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalResponseContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalResponseContent.cs @@ -7,15 +7,16 @@ namespace Microsoft.Extensions.AI; /// -/// Represents a response to a function approval request. +/// Represents a response to a , indicating whether the function call was approved. /// public sealed class FunctionApprovalResponseContent : InputResponseContent { /// /// Initializes a new instance of the class. /// - /// The identifier of the associated with this response. /// if the function call is approved; otherwise, . - /// The function call that requires user approval. + /// The unique identifier of the associated with this response. + /// if the function call is approved; otherwise, . + /// The function call that was subject to approval. /// is . /// is empty or composed entirely of whitespace. /// is . @@ -27,12 +28,12 @@ public FunctionApprovalResponseContent(string requestId, bool approved, Function } /// - /// 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/InputRequestContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputRequestContent.cs index 60295f03f55..79fbb442a17 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputRequestContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputRequestContent.cs @@ -8,7 +8,7 @@ namespace Microsoft.Extensions.AI; /// -/// Represents a request for user input. +/// Represents a request for input from the user or application. /// [JsonPolymorphic] [JsonDerivedType(typeof(FunctionApprovalRequestContent), "functionApprovalRequest")] @@ -17,7 +17,7 @@ public class InputRequestContent : AIContent /// /// Initializes a new instance of the class. /// - /// The ID that uniquely identifies the user input request/response pair. + /// The unique identifier that correlates this request with its corresponding response. /// is . /// is empty or composed entirely of whitespace. protected InputRequestContent(string requestId) @@ -26,7 +26,7 @@ protected InputRequestContent(string requestId) } /// - /// Gets the ID that uniquely identifies the user input request/response pair. + /// Gets the unique identifier that correlates this request with its corresponding . /// public string RequestId { get; } -} \ No newline at end of file +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputResponseContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputResponseContent.cs index 4625114b0f9..3c9530c325d 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputResponseContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputResponseContent.cs @@ -8,7 +8,7 @@ namespace Microsoft.Extensions.AI; /// -/// Represents the response to a request for user input. +/// Represents the response to an . /// [JsonPolymorphic] [JsonDerivedType(typeof(FunctionApprovalResponseContent), "functionApprovalResponse")] @@ -17,7 +17,7 @@ public class InputResponseContent : AIContent /// /// Initializes a new instance of the class. /// - /// The ID that uniquely identifies the user input request/response pair. + /// The unique identifier that correlates this response with its corresponding request. /// is . /// is empty or composed entirely of whitespace. protected InputResponseContent(string requestId) @@ -26,7 +26,7 @@ protected InputResponseContent(string requestId) } /// - /// Gets the ID that uniquely identifies the user input request/response pair. + /// Gets the unique identifier that correlates this response with its corresponding . /// public string RequestId { get; } -} \ No newline at end of file +} From 7fd2c4aae92bac64772e36848e0c556e7a414653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Fri, 6 Feb 2026 12:23:48 -0600 Subject: [PATCH 21/30] Address jsonpolymorphic feedback and improve tests --- .../Contents/FunctionCallContent.cs | 1 - .../Contents/FunctionResultContent.cs | 1 - .../Contents/InputRequestContent.cs | 1 - .../Contents/InputResponseContent.cs | 1 - .../Contents/AIContentTests.cs | 11 ++++++- .../FunctionApprovalRequestContentTests.cs | 25 ++++++++++----- .../FunctionApprovalResponseContentTests.cs | 31 ++++++++++++++++++- .../Contents/FunctionCallContentTests.cs | 26 ++++++++++++++++ .../Contents/FunctionResultContentTests.cs | 21 +++++++++++++ .../Contents/McpServerToolCallContentTests.cs | 27 ++++++++++------ .../McpServerToolResultContentTests.cs | 19 +++++++++--- 11 files changed, 136 insertions(+), 28 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs index 6f5d73d1699..042e862f986 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs @@ -14,7 +14,6 @@ namespace Microsoft.Extensions.AI; /// Represents a function call request. /// [DebuggerDisplay("{DebuggerDisplay,nq}")] -[JsonPolymorphic] [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 0a11bde3cf1..9ffdb11472d 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionResultContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionResultContent.cs @@ -13,7 +13,6 @@ namespace Microsoft.Extensions.AI; /// Represents the result of a function call. /// [DebuggerDisplay("{DebuggerDisplay,nq}")] -[JsonPolymorphic] [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 index 79fbb442a17..80fdf292d50 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputRequestContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputRequestContent.cs @@ -10,7 +10,6 @@ namespace Microsoft.Extensions.AI; /// /// Represents a request for input from the user or application. /// -[JsonPolymorphic] [JsonDerivedType(typeof(FunctionApprovalRequestContent), "functionApprovalRequest")] public class InputRequestContent : AIContent { diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputResponseContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputResponseContent.cs index 3c9530c325d..086427dbab5 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputResponseContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputResponseContent.cs @@ -10,7 +10,6 @@ namespace Microsoft.Extensions.AI; /// /// Represents the response to an . /// -[JsonPolymorphic] [JsonDerivedType(typeof(FunctionApprovalResponseContent), "functionApprovalResponse")] public class InputResponseContent : AIContent { diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIContentTests.cs index 71ee60c5983..9b69be10cc2 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIContentTests.cs @@ -75,9 +75,18 @@ public void Serialization_DerivedTypes_Roundtrips() new McpServerToolCallContent("call123", "myTool", "myServer"), new McpServerToolResultContent("call123"), new FunctionApprovalRequestContent("request123", new McpServerToolCallContent("call123", "myTool", "myServer")), - new FunctionApprovalResponseContent("request123", approved: true, new McpServerToolCallContent("call456", "myTool2", "myServer2")) + new FunctionApprovalResponseContent("request123", approved: true, new McpServerToolCallContent("call456", "myTool2", "myServer2")), ]); + // 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 fada9b3adea..8028167d8ad 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalRequestContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalRequestContentTests.cs @@ -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.RequestId, deserializedContent.RequestId); - 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 06f6f8d29a1..036ef83b65a 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalResponseContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalResponseContentTests.cs @@ -35,10 +35,39 @@ public void Constructor_Roundtrips(string id, bool 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")) { 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 59e6d63eccc..0ca4db7a123 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests.cs @@ -406,6 +406,32 @@ public static void CreateFromParsedArguments_NullInput_ThrowsArgumentNullExcepti 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() { 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 694749d2528..7bf2cdf9d6f 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionResultContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionResultContentTests.cs @@ -92,6 +92,27 @@ public void ItShouldBeSerializableAndDeserializableWithException() 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() { 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 9e73af5df34..cc696618257 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolCallContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolCallContentTests.cs @@ -72,14 +72,23 @@ public void Serialization_Roundtrips() Arguments = new Dictionary { { "arg1", "value1" } } }; - var json = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions); - var deserializedContent = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions); - - Assert.NotNull(deserializedContent); - 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()); + 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 c405c21b300..d71a1a6243f 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolResultContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolResultContentTests.cs @@ -57,11 +57,20 @@ public void Serialization_Roundtrips() 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.Equal("result", ((JsonElement)deserializedContent.Result!).GetString()); + 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()); + } } } From 2c5f6fbd34497be7be44b48e1da55f90b9d2149d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Fri, 6 Feb 2026 15:55:50 -0600 Subject: [PATCH 22/30] McpToolCallApprovalResponseItem conversion to MEAI correctly and implement correlation to obtain McpServerToolCallContent from request --- .../OpenAIResponsesChatClient.cs | 44 +++ .../OpenAIConversionTests.cs | 258 +++++++++++++++++- 2 files changed, 299 insertions(+), 3 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index 9e799366e52..dadc5330dcd 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -164,6 +164,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) { @@ -207,6 +208,9 @@ internal static IEnumerable ToChatMessages(IEnumerable ToChatMessages(IEnumerable FromOpenAIStreamingRe ChatRole? lastRole = null; bool anyFunctions = false; ResponseStatus? latestResponseStatus = null; + Dictionary? mcpApprovalRequests = null; UpdateConversationId(resumeResponseId); @@ -422,6 +445,9 @@ ChatResponseUpdate CreateUpdate(AIContent? content = null) => break; case McpToolCallApprovalRequestItem mtcari: + // 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) { @@ -433,6 +459,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: var codeUpdate = CreateUpdate(); AddCodeInterpreterContents(cicri, codeUpdate.Contents); diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs index aa0149d730d..06ed5261a46 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs @@ -881,13 +881,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] @@ -977,6 +1090,145 @@ public void AsChatMessages_FromResponseItems_WithFunctionCall_HandlesCorrectly() Assert.Equal("value", functionCall.Arguments!["param"]?.ToString()); } + [Fact] + public void AsChatMessages_FromResponseItems_AllContentTypes_RoundtripsWithRawRepresentation() + { + // 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); + } + [Fact] public void AsOpenAIChatCompletion_WithNullArgument_ThrowsArgumentNullException() { From ced9e364553b7a901da57082d41b782d97a3c43f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Fri, 6 Feb 2026 16:48:39 -0600 Subject: [PATCH 23/30] Fix roundtrip of McpServerToolResultContent.Result --- .../OpenAIResponsesChatClient.cs | 8 +++--- .../OpenAIConversionTests.cs | 28 ++++++++++++++++++- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index dadc5330dcd..fcaa8b27dab 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -1193,13 +1193,13 @@ static FunctionCallOutputResponseItem SerializeAIContent(string callId, IEnumera BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes( associatedCall.Arguments!, AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IDictionary))))); - if (mstrc.Result is BinaryData errorData) + if (mstrc.Result is ErrorContent errorContent) { - mtci.Error = errorData; + mtci.Error = BinaryData.FromString(errorContent.Message); } - else if (mstrc.Result is string outputString) + else if (mstrc.Result is TextContent textContent) { - mtci.ToolOutput = outputString; + mtci.ToolOutput = textContent.Text; } yield return mtci; diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs index 06ed5261a46..c1097dfac9a 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs @@ -1091,7 +1091,7 @@ public void AsChatMessages_FromResponseItems_WithFunctionCall_HandlesCorrectly() } [Fact] - public void AsChatMessages_FromResponseItems_AllContentTypes_RoundtripsWithRawRepresentation() + public void AsChatMessages_FromResponseItems_AllContentTypes_SetsRawRepresentation() { // Create ResponseItems of various types that ToChatMessages handles. // Each type should roundtrip with RawRepresentation set. @@ -1229,6 +1229,32 @@ public void AsChatMessages_McpToolCallApprovalResponseItem_WithoutCorrelatedRequ 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() { From 89a0ba91fc730fc8e3820d9339cbef90a0d89896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Fri, 6 Feb 2026 16:56:36 -0600 Subject: [PATCH 24/30] Refactor approval request ID generation in FunctionInvokingChatClient --- .../ChatCompletion/FunctionInvokingChatClient.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs index 8b1235a61dc..7fcf5b770de 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs @@ -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($"ficc_{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($"ficc_{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. From d0a56eb677155b816f765e1f75c9d7ac8dedb588 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 00:21:06 +0000 Subject: [PATCH 25/30] Address documentation feedback for MCP content types and Headers property Co-authored-by: jozkee <16040868+jozkee@users.noreply.github.com> --- .../Contents/FunctionApprovalRequestContent.cs | 2 +- .../Contents/McpServerToolCallContent.cs | 5 ++++- .../Tools/HostedMcpServerTool.cs | 3 +++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalRequestContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalRequestContent.cs index f884d6ee187..27782fa1248 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalRequestContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalRequestContent.cs @@ -14,7 +14,7 @@ public sealed class FunctionApprovalRequestContent : InputRequestContent /// /// Initializes a new instance of the class. /// - /// The unique identifier that correlates this request with its corresponding response. + /// The unique identifier that correlates this request with its corresponding response. This is typically not the same as the function call ID. /// The function call that requires approval before execution. /// is . /// is empty or composed entirely of whitespace. diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs index 9fbf8420ad6..26ea549cc6c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs @@ -10,8 +10,11 @@ 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 generally considered informational only, and is provided either as part of an approval request +/// to convey what is being approved or as information about what tool was invoked elsewhere in the stack. +/// /// public sealed class McpServerToolCallContent : FunctionCallContent { diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs index 58e76b27bb4..63f0f31ad90 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs @@ -135,6 +135,9 @@ private static string ValidateUrl(Uri serverAddress) /// /// The underlying provider is not guaranteed to support or honor the headers. /// + /// + /// This can be used to supply authorization tokens required by the remote MCP server. + /// /// public IDictionary? Headers { get; set; } } From f9e7e0ea0556f65e2532e43564d96902b1633805 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 00:58:36 +0000 Subject: [PATCH 26/30] Apply documentation suggestions from @jozkee Co-authored-by: jozkee <16040868+jozkee@users.noreply.github.com> --- .../Contents/FunctionApprovalRequestContent.cs | 2 +- .../Contents/McpServerToolCallContent.cs | 4 ++-- .../Tools/HostedMcpServerTool.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalRequestContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalRequestContent.cs index 27782fa1248..5e9cbcc3d04 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalRequestContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalRequestContent.cs @@ -14,7 +14,7 @@ public sealed class FunctionApprovalRequestContent : InputRequestContent /// /// Initializes a new instance of the class. /// - /// The unique identifier that correlates this request with its corresponding response. This is typically not the same as the function call ID. + /// 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. diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs index 26ea549cc6c..581c91a4528 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs @@ -12,8 +12,8 @@ namespace Microsoft.Extensions.AI; /// /// /// This content type is used to represent an invocation of an MCP server tool by a hosted service. -/// It is generally considered informational only, and is provided either as part of an approval request -/// to convey what is being approved or as information about what tool was invoked elsewhere in the stack. +/// 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. /// /// public sealed class McpServerToolCallContent : FunctionCallContent diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs index 63f0f31ad90..682933df7f7 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs @@ -136,7 +136,7 @@ private static string ValidateUrl(Uri serverAddress) /// The underlying provider is not guaranteed to support or honor the headers. /// /// - /// This can be used to supply authorization tokens required by the remote MCP server. + /// This property is useful for specifying the authentication header or other headers required by the MCP server. /// /// public IDictionary? Headers { get; set; } From a43576ab8e1de36eba78037590f0e845ed55de7b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:24:51 +0000 Subject: [PATCH 27/30] Add remarks to McpServerToolCallContent constructor documentation Co-authored-by: jozkee <16040868+jozkee@users.noreply.github.com> --- .../Contents/McpServerToolCallContent.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs index 581c91a4528..62b7b25f065 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs @@ -26,6 +26,10 @@ public sealed class McpServerToolCallContent : FunctionCallContent /// The MCP server name that hosts the tool. /// 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)) { From 420502e863d03298cec4d148ec8554e9246206c1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:35:36 +0000 Subject: [PATCH 28/30] Fix case-insensitive Bearer token extraction and add test, fix MakeApiBaselines.ps1 TFM Co-authored-by: jozkee <16040868+jozkee@users.noreply.github.com> --- scripts/MakeApiBaselines.ps1 | 5 +- .../OpenAIResponsesChatClient.cs | 12 ++- .../OpenAIResponseClientTests.cs | 85 +++++++++++++++++++ 3 files changed, 97 insertions(+), 5 deletions(-) diff --git a/scripts/MakeApiBaselines.ps1 b/scripts/MakeApiBaselines.ps1 index f3b77112661..1abfd92087a 100644 --- a/scripts/MakeApiBaselines.ps1 +++ b/scripts/MakeApiBaselines.ps1 @@ -15,8 +15,9 @@ Write-Output "Installing required toolset" . $PSScriptRoot/../eng/common/tools.ps1 InitializeDotNetCli -install $true | Out-Null +$TargetFramework = "net10.0" $Project = $PSScriptRoot + "/../eng/Tools/ApiChief/ApiChief.csproj" -$Command = $PSScriptRoot + "/../artifacts/bin/ApiChief/Debug/net10.0/ApiChief.dll" +$Command = $PSScriptRoot + "/../artifacts/bin/ApiChief/Debug/$TargetFramework/ApiChief.dll" $LibrariesFolder = $PSScriptRoot + "/../src/Libraries" Write-Output "Building ApiChief tool" @@ -28,7 +29,7 @@ Write-Output "Creating API baseline files in the src/Libraries folder" Get-ChildItem -Path $LibrariesFolder -Depth 1 -Include *.csproj | ForEach-Object ` { $name = Split-Path $_.FullName -LeafBase - $path = "$PSScriptRoot\..\artifacts\bin\$name\Debug\net9.0\$name.dll" + $path = "$PSScriptRoot\..\artifacts\bin\$name\Debug\$TargetFramework\$name.dll" Write-Host " Processing" $name dotnet $Command $path emit baseline -o "$LibrariesFolder/$name/$name.json" } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index fcaa8b27dab..25e4e626f11 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -654,10 +654,16 @@ void IDisposable.Dispose() else { // For connectors: extract Bearer token from Headers and set as AuthorizationToken. - if (mcpTool.Headers?.TryGetValue("Authorization", out string? authHeader) is true && - authHeader?.StartsWith("Bearer ", StringComparison.Ordinal) is true) + // Use case-insensitive comparison since auth scheme is case-insensitive per RFC 7235. + if (mcpTool.Headers?.TryGetValue("Authorization", out string? authHeader) is true) { - responsesMcpTool.AuthorizationToken = authHeader.Substring("Bearer ".Length); + string? trimmedAuthHeader = authHeader?.TrimStart(); + + if (!string.IsNullOrEmpty(trimmedAuthHeader) && + trimmedAuthHeader!.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) + { + responsesMcpTool.AuthorizationToken = trimmedAuthHeader.Substring("Bearer ".Length); + } } } diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs index e88d7097768..a5c0d39a74f 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs @@ -2406,6 +2406,91 @@ 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")] + 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() { From ec9ebec0be5784eaa6d8dd2cc87d2c953b23e1b0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 19:30:44 +0000 Subject: [PATCH 29/30] Trim both ends of header value and allow flexible whitespace after Bearer Co-authored-by: jozkee <16040868+jozkee@users.noreply.github.com> --- .../OpenAIResponsesChatClient.cs | 5 +++-- .../OpenAIResponseClientTests.cs | 3 +++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index 25e4e626f11..c266e91dcf3 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -655,14 +655,15 @@ void IDisposable.Dispose() { // 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) { - string? trimmedAuthHeader = authHeader?.TrimStart(); + string? trimmedAuthHeader = authHeader?.Trim(); if (!string.IsNullOrEmpty(trimmedAuthHeader) && trimmedAuthHeader!.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) { - responsesMcpTool.AuthorizationToken = trimmedAuthHeader.Substring("Bearer ".Length); + responsesMcpTool.AuthorizationToken = trimmedAuthHeader.Substring("Bearer ".Length).TrimStart(); } } } diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs index a5c0d39a74f..a45533758ae 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs @@ -2411,6 +2411,9 @@ public async Task McpToolCall_WithAuthorizationTokenAndCustomHeaders_IncludesInR [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 From 6127a729a18aaad379a9110fce811a9b58502f1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Tue, 10 Feb 2026 12:56:22 -0600 Subject: [PATCH 30/30] Address feedback --- .../HostedMcpServerToolApprovalMode.cs | 1 - .../OpenAIResponsesChatClient.cs | 12 ++++-------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolApprovalMode.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolApprovalMode.cs index 0647fefa083..9c4ca705238 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolApprovalMode.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolApprovalMode.cs @@ -13,7 +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 . /// -[JsonPolymorphic] [JsonDerivedType(typeof(HostedMcpServerToolNeverRequireApprovalMode), typeDiscriminator: "never")] [JsonDerivedType(typeof(HostedMcpServerToolAlwaysRequireApprovalMode), typeDiscriminator: "always")] [JsonDerivedType(typeof(HostedMcpServerToolRequireSpecificApprovalMode), typeDiscriminator: "requireSpecific")] diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index c266e91dcf3..c7b1b806c87 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -656,15 +656,11 @@ void IDisposable.Dispose() // 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) + if (mcpTool.Headers?.TryGetValue("Authorization", out string? authHeader) is true && + authHeader.AsSpan().Trim() is { Length: > 0 } trimmedAuthHeader && + trimmedAuthHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) { - string? trimmedAuthHeader = authHeader?.Trim(); - - if (!string.IsNullOrEmpty(trimmedAuthHeader) && - trimmedAuthHeader!.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) - { - responsesMcpTool.AuthorizationToken = trimmedAuthHeader.Substring("Bearer ".Length).TrimStart(); - } + responsesMcpTool.AuthorizationToken = trimmedAuthHeader.Slice("Bearer ".Length).TrimStart().ToString(); } }