Approach 2: MCP/Approvals/Tool Contents stabilization#7299
Approach 2: MCP/Approvals/Tool Contents stabilization#7299jozkee wants to merge 15 commits intodotnet:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR stabilizes MCP/tool-calling and approval-related content types across the Microsoft.Extensions.AI stack, updating both runtime code and tests to use the new ToolCallContent / ToolResultContent and ToolApproval* abstractions (and related serialization/telemetry plumbing).
Changes:
- Introduces/standardizes new base content types (
ToolCallContent,ToolResultContent,InputRequestContent,InputResponseContent) and migrates approvals toToolApprovalRequestContent/ToolApprovalResponseContent. - Updates OpenAI Responses conversion + OpenTelemetry serialization to the new content model (e.g.,
OutputsvsOutput, MCP callNamevsToolName, image/code tool call IDs). - Refreshes and expands test coverage for the new content hierarchy, serialization roundtrips, and mixed MCP/function approval flows.
Reviewed changes
Copilot reviewed 70 out of 70 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| test/Libraries/Microsoft.Extensions.AI.Tests/ChatReduction/SummarizingChatReducerTests.cs | Updates reducer tests to use InputRequestContent/InputResponseContent test types. |
| test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/OpenTelemetryChatClientTests.cs | Updates serialization expectations for tool call/result + approvals; aligns with constructor-based IDs and Outputs. |
| test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ImageGeneratingChatClientTests.cs | Switches image tool ID assertions from ImageId to CallId. |
| test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs | Updates approval assertions to ToolApprovalRequestContent/ToolApprovalResponseContent and ToolCall. |
| test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs | Adds mixed MCP/function approval scenarios; updates approval types and introduces input cloning helper for streaming/non-streaming parity. |
| test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs | Updates MCP naming/output/approval assertions and adds error + bearer-token extraction tests. |
| test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs | Updates integration tests to use ToolApprovalRequestContent and CreateResponse; switches MCP tool name/output properties. |
| test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs | Updates MCP tool auth/header tests; expands streaming update conversion coverage (including approval correlation behavior). |
| test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs | Adjusts tests for nullable Headers and removal of AuthorizationToken. |
| test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs | Updates source-gen context registrations for new polymorphic base types and tool call/result arrays. |
| test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputResponseContentTests.cs | Removes tests for deprecated UserInputResponseContent. |
| test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputRequestContentTests.cs | Removes tests for deprecated UserInputRequestContent. |
| test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolResultContentTests.cs | Adds roundtrip serialization coverage for ToolResultContent derived types. |
| test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolCallContentTests.cs | Adds roundtrip serialization coverage for ToolCallContent derived types. |
| test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolApprovalResponseContentTests.cs | Adds constructor validation + serialization roundtrip tests for ToolApprovalResponseContent. |
| test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolApprovalRequestContentTests.cs | Adds constructor validation + response factory + serialization roundtrip tests for ToolApprovalRequestContent. |
| test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolResultContentTests.cs | Updates MCP result content to Outputs and validates polymorphic roundtrips. |
| test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolCallContentTests.cs | Renames MCP call ToolName→Name, Arguments mutability update, and adds serialization roundtrip. |
| test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputResponseContentTests.cs | Adds base input-response type validation + derived serialization tests. |
| test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputRequestContentTests.cs | Adds base input-request type validation + derived serialization tests. |
| test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ImageGenerationToolResultContentTests.cs | Moves to ctor-based CallId and removes ImageId usage. |
| test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ImageGenerationToolCallContentTests.cs | Moves to ctor-based CallId and removes ImageId usage. |
| test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionResultContentTests.cs | Adds serialization roundtrip tests for ToolResultContent/AIContent polymorphism. |
| test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests.cs | Adds serialization roundtrip tests for ToolCallContent/AIContent polymorphism. |
| test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalResponseContentTests.cs | Removes deprecated FunctionApprovalResponseContent tests. |
| test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalRequestContentTests.cs | Removes deprecated FunctionApprovalRequestContent tests. |
| test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/CodeInterpreterToolResultContentTests.cs | Moves to ctor-based CallId and removes settable CallId. |
| test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/CodeInterpreterToolCallContentTests.cs | Moves to ctor-based CallId and removes settable CallId. |
| test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIContentTests.cs | Updates derived-type list and strengthens per-element polymorphic roundtrip checks. |
| test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs | Updates coalescing tests to use CallId and ctor-based tool result creation. |
| test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/AssertExtensions.cs | Extends message comparison helpers to handle tool approvals uniformly across tool call types. |
| src/Libraries/Microsoft.Extensions.AI/ChatReduction/SummarizingChatReducer.cs | Updates “tool-related content” detection to include InputRequestContent/InputResponseContent. |
| src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs | Updates OTel payload serialization for renamed properties (Name, Outputs, CallId) and unified approval types. |
| src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ImageGeneratingChatClient.cs | Switches to ctor-based image tool call/result creation with CallId. |
| src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs | Migrates approvals to ToolApproval* with composed request IDs; updates request/response processing logic. |
| src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs | Implements MCP approval request/response correlation in Responses conversion; updates tool call/result modeling and connector auth extraction from headers. |
| src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIJsonContext.cs | Removes IReadOnlyDictionary<string, object?> source-gen contract in favor of IDictionary<string, object?>. |
| src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs | Switches code interpreter tool call/result construction to ctor-based CallId. |
| src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.cs | Adds helper for registering derived types into polymorphism options. |
| src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs | Updates default serializer configuration to include new polymorphic bases and registers experimental tool types under ToolCallContent/ToolResultContent. |
| src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs | Stabilizes MCP tool; removes AuthorizationToken and makes Headers nullable/settable; renames Uri ctor arg to serverAddress. |
| src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json | Updates API baseline/staging to reflect stabilization and new content hierarchy. |
| src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolRequireSpecificApprovalMode.cs | Removes experimental annotations in support of stabilization. |
| src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolNeverRequireApprovalMode.cs | Removes experimental annotations in support of stabilization. |
| src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolApprovalMode.cs | Removes experimental/polymorphic annotations in support of stabilization and updated serialization model. |
| src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolAlwaysRequireApprovalMode.cs | Removes experimental annotations in support of stabilization. |
| src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/ApprovalRequiredAIFunction.cs | Removes experimental annotation (stabilizing the API). |
| src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputResponseContent.cs | Removes deprecated UserInputResponseContent. |
| src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputRequestContent.cs | Removes deprecated UserInputRequestContent. |
| src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolResultContent.cs | Introduces new ToolResultContent base type for tool results. |
| src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolCallContent.cs | Introduces new ToolCallContent base type for tool calls. |
| src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolApprovalResponseContent.cs | Introduces unified approval response content for tool calls. |
| src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolApprovalRequestContent.cs | Introduces unified approval request content and response factory helper. |
| src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs | Updates MCP tool results to derive from ToolResultContent and renames Output→Outputs. |
| src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs | Updates MCP tool calls to derive from ToolCallContent, renames ToolName→Name, and updates arguments type. |
| src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolApprovalResponseContent.cs | Removes deprecated MCP approval response content type. |
| src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolApprovalRequestContent.cs | Removes deprecated MCP approval request content type. |
| src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputResponseContent.cs | Introduces new InputResponseContent base type for input responses. |
| src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputRequestContent.cs | Introduces new InputRequestContent base type for input requests. |
| src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageGenerationToolResultContent.cs | Updates image result content to derive from ToolResultContent and use ctor CallId. |
| src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageGenerationToolCallContent.cs | Updates image call content to derive from ToolCallContent and use ctor CallId. |
| src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionResultContent.cs | Updates function result content to derive from ToolResultContent. |
| src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs | Updates function call content to derive from ToolCallContent. |
| src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalResponseContent.cs | Removes deprecated function approval response content type. |
| src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalRequestContent.cs | Removes deprecated function approval request content type. |
| src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolResultContent.cs | Updates code interpreter result content to derive from ToolResultContent and use ctor CallId. |
| src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolCallContent.cs | Updates code interpreter call content to derive from ToolCallContent and use ctor CallId. |
| src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs | Registers stabilized tool/approval content types as derived types of AIContent. |
| src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs | Updates content coalescing logic for ctor-based tool content and CallId usage. |
src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolApprovalRequestContent.cs
Outdated
Show resolved
Hide resolved
src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolApprovalRequestContent.cs
Show resolved
Hide resolved
src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolApprovalRequestContent.cs
Outdated
Show resolved
Hide resolved
src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs
Show resolved
Hide resolved
src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs
Show resolved
Hide resolved
src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs
Show resolved
Hide resolved
| // as well as the [JsonSerializable] attributes for them on the JsonContext should be removed. | ||
| // [JsonDerivedType(typeof(CodeInterpreterToolResultContent), "codeInterpreterToolResult")] | ||
| // [JsonDerivedType(typeof(ImageGenerationToolResultContent), "imageGenerationToolResult")] | ||
| public class ToolResultContent : AIContent |
There was a problem hiding this comment.
Without a property that represents the result of the tool, folks will need to downcast to some tool type they know that will have output. I wonder if it makes sense to have a result property on this, even if it's not the serialized property.
There was a problem hiding this comment.
We would need to reconciliate object? Result from FRC and IList<AIContent>? Outputs from the rest. I suggest we defer for future this kind of breaking change. This could come handy for APIs that could accept the loose ToolResultContent, for now, we have it just for symmetry with ToolCallContent and we don't really use it for more. It's also related to #7057.
There was a problem hiding this comment.
Are you worried about reconciling it later making a breaking change? I suppose we could always create a new read-only virtual property that's implemented by the derived types. This PR nearly solves #7057. It does look like a few reference counts on a unified base output property.
There was a problem hiding this comment.
I made ToolCall/ToolResult contents effectively sealed (internal ctors), so moving a property up the hierarchy is not a breaking change.
If we do add Outputs to the base, I believe we still need to keep object? Result on FunctionResultContent. Result carries the return value from AIFunction.InvokeAsync(), preserving the original CLR object for leaf clients to serialize lazily in whatever format they need. Converting to IList<AIContent> at construction time is lossy so the original object is gone and providers lose the ability to serialize it in their own format. It also avoids breaking a stable type.
I'm a bit concerned about the ambiguity, but I think if we advertise Result as the raw CLR output and Outputs as a structured representation we could make it work. We already have precedent for this in OpenAI responses where we check if Result is AIContent or IEnumerable<AIContent> before falling back to serialization.
There was a problem hiding this comment.
This is what it would look like: jozkee@90993a7.
For FunctionInvokingChatClient, I think we should not eagerly set Outputs since Result will be handled by leaf clients.
There was a problem hiding this comment.
@stephentoub could you please take a look at this?
There was a problem hiding this comment.
I don't have a strong opinion. If we ensure that all of the derived types have the same signature for it, we could push it down to ToolResultContent later, especially since I think all the derived content types that have it today are experimental (though we'll want to fix that soon, in particular for code interpreter). If we can rationalize what it looks like for FunctionResultContent and it makes sense, I'd be ok with it... but I think we should do it as a separate change.
1111f09 to
5343bb0
Compare
ericstj
left a comment
There was a problem hiding this comment.
This looks good to me - your call on the output abstraction. At least think through how you might do it compatibly.
| /// </summary> | ||
| /// <param name="callId">The tool call ID.</param> | ||
| /// <exception cref="ArgumentNullException"><paramref name="callId"/> is <see langword="null"/>.</exception> | ||
| internal ToolCallContent(string callId) |
There was a problem hiding this comment.
This should be public. Other tool call content / results should be able to derive from it, not just ones we define. We should also update the OpenAI response client to use this instead of AIContent for any call/result content it has types for but that we don't have an abstraction for.
| /// </summary> | ||
| /// <param name="callId">The tool call ID for which this is the result.</param> | ||
| /// <exception cref="ArgumentNullException"><paramref name="callId"/> is <see langword="null"/>.</exception> | ||
| internal ToolResultContent(string callId) |
| @@ -203,6 +203,19 @@ private static void AddAIContentType(JsonSerializerOptions options, Type content | |||
| }); | |||
There was a problem hiding this comment.
This looks identical to AddDerivedContentType, just using typeof(AIContent) as baseType. Can this just call AddDerivedContentType instead?
| AddAIContentType(options, typeof(ImageGenerationToolResultContent), typeDiscriminatorId: "imageGenerationToolResult", checkBuiltIn: false); | ||
|
|
||
| // Also register the experimental types as derived types of ToolCallContent/ToolResultContent. | ||
| AddDerivedContentType(options, typeof(ToolCallContent), typeof(CodeInterpreterToolCallContent), "codeInterpreterToolCall"); |
There was a problem hiding this comment.
Maybe:
a) we should also call this AddAIContentType and just have it be another overload that takes the base type
b) make it public, assuming others will need to register AIContent-derived types in a hierarchy
?
There was a problem hiding this comment.
Makes sense, I would prefer to make it public in a follow-up PR, though.
| // 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., ToolApprovalRequestContent) as InputRequestContent. | ||
| [JsonSerializable(typeof(InputRequestContent))] | ||
| [JsonSerializable(typeof(InputResponseContent))] | ||
|
|
||
| // ToolCallContent and ToolResultContent are polymorphic base types for tool calls/results. | ||
| [JsonSerializable(typeof(ToolCallContent))] | ||
| [JsonSerializable(typeof(ToolResultContent))] |
There was a problem hiding this comment.
Why do any of these need to be here? Especially once you add public ctors to ToolCall/ResultContent, they should be normal AIContent-derived types, with [JsonDerived] attributes on AIContent. For InputRequestContent/ResultContent, I understand the argument that they don't have public ctors and so a type discriminator is meaningless, but at the same time it shouldn't hurt anything and it makes it more maintainable in my opinion: they're just [JsonDerived] on the AIContent, and then you don't need a separate AddDerivedContent call, you don't need to list them separately as [JsonSerializable], etc... all the default handling kicks in.
There was a problem hiding this comment.
Yes, it made sense to me for InputContent but since we are addressing ToolContents I'm ok with also having the former as JsonDerived.
| message.Contents.Add(new ToolApprovalResponseContent( | ||
| mtcari.ApprovalRequestId, | ||
| mtcari.Approved, | ||
| new McpServerToolCallContent(mtcari.ApprovalRequestId, request.ToolName, request.ServerLabel) |
There was a problem hiding this comment.
What's the rationale for the mcpApprovalRequest dictionary storing the McpToolCallApprovalRequestItem rather than the ToolApprovalRequestContent instance? If it stored the latter, either instead of or in addition, then this could just use that same instance, right?
There was a problem hiding this comment.
No particular reason, what you describe is better.
| yield return CreateUpdate(new ToolApprovalResponseContent( | ||
| mtcari.ApprovalRequestId, | ||
| mtcari.Approved, | ||
| new McpServerToolCallContent(mtcari.ApprovalRequestId, request.ToolName, request.ServerLabel) |
| Outputs = mtci.Error is not null ? | ||
| [new ErrorContent(mtci.Error.ToString())] : | ||
| [new TextContent(mtci.ToolOutput)], |
There was a problem hiding this comment.
Why did this need to change?
| /// </summary> | ||
| private static bool HasAnyApprovalContent(List<ChatMessage> messages) => | ||
| messages.Exists(static m => m.Contents.Any(static c => c is FunctionApprovalRequestContent or FunctionApprovalResponseContent)); | ||
| messages.Exists(static m => m.Contents.Any(static c => c is ToolApprovalRequestContent or ToolApprovalResponseContent)); |
There was a problem hiding this comment.
Is this going to introduce any problems or perf issues now that it'll also return true for MCP approvals that are not handled by the FICC?
There was a problem hiding this comment.
Narrowed the filter. No correctness issue before since we are also filtering (the narrow way) later, that second filter needs to stay for pattern matching.
| switch (content) | ||
| { | ||
| case FunctionApprovalRequestContent farc: | ||
| case ToolApprovalRequestContent tarc when tarc.ToolCall is FunctionCallContent { InformationalOnly: false }: |
There was a problem hiding this comment.
Not for now, but I wonder if we're going to end up wanting to push InformationOnly down to ToolCallContent.
| /// The underlying provider is not guaranteed to support or honor the headers. | ||
| /// </para> | ||
| /// <para> | ||
| /// This property is useful for specifying the authentication header or other headers required by the MCP server. |
There was a problem hiding this comment.
Generally headers are case-insensitive. I wonder if we should make a comment about that here, because we don't have a good way to enforce it with this design (and the previous lazy-initialization looks like we had it wrong).
Providers are going to try to pull out "Authorization" or "authorization", and if the dictionary isn't using a case-insensitive comparer, it may not behave correctly.
There was a problem hiding this comment.
Yeah I don't think we have a good answer for this other than docs. We don't have a dictionary type that enforces case sensitiveness AFAIK and HttpHeaders may not be a good fit for this case. We have the same issue in https://github.com/modelcontextprotocol/csharp-sdk/blob/89fcf3f8b7db80bb7de6fcf7c8091b362fb18f84/src/ModelContextProtocol.Core/Client/HttpClientTransportOptions.cs#L78.
There was a problem hiding this comment.
Nit: not from your change, but this XML comment is stale; it doesn't coalesce based on Name
…ontent, InputRequestContent, and InputResponseContent; remove redundant [JsonSerializable] entries from AIJsonUtilities.Defaults * Consolidate AddDerivedContentType into AddAIContentType with baseType param * Make ToolCallContent and ToolResultContent constructors public
…ems to ToolCallContent/ToolResultContent in OpenAIResponsesChatClient (both non-streaming and streaming paths) * Store ToolApprovalRequestContent in mcpApprovalRequests dictionary instead of McpToolCallApprovalRequestItem, reusing ToolCall on correlation
Alternative to #7245.
Benefits:
Exception.Cost:
OutputswithResult(can be deferred, non-breaking in the future).CallIdnullability and name