Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
2daa4b4
Initial plan
Copilot Jan 30, 2026
b5dac2d
Rebase
jozkee Jan 30, 2026
052dbe3
Add CompatibilitySuppressions.xml
jozkee Jan 30, 2026
59b5ebb
Fix ToolName->Name and CloneFcc to handle McpServerToolCallContent
Copilot Jan 30, 2026
500863e
Remove duplicate assertion for mcpCallContent.Name
Copilot Jan 30, 2026
0ee1ed6
Update handling of experimental McpServerTool types in sourcegen seri…
jozkee Jan 30, 2026
cd19f1e
Remove User prefix from Input contents and rename id -> requestId
jozkee Feb 6, 2026
d2d6aa2
Use string.Empty on mcp approval requests
jozkee Feb 4, 2026
b81cb33
Remove AuthorizationToken and make Headers settable
jozkee Feb 6, 2026
4515843
Prefix RequestIds with ficc_ on FunctionInvokingChatClient
jozkee Feb 6, 2026
7fe9c0d
Rename serverUrl -> serverAddress to align with string overload
jozkee Feb 6, 2026
b5bb861
OpenAI: Revert to use ErrorContent and TextContent
jozkee Feb 6, 2026
ca5d7d3
Remove Experimental
jozkee Feb 6, 2026
23eed81
Update CompatibilitySuppressions.xml
jozkee Feb 6, 2026
4cc083c
Cleanup JsonPolymorphic for Stabilized types
jozkee Feb 6, 2026
f5e3b37
Update test
jozkee Feb 6, 2026
74a1165
Merge branch 'main' of https://github.com/dotnet/extensions into temp
jozkee Feb 6, 2026
9a950b3
Fix merge errors
jozkee Feb 6, 2026
66e28b1
Revert "Use string.Empty on mcp approval requests"
jozkee Feb 6, 2026
8579d3c
Fix more merge errors
jozkee Feb 6, 2026
2d8bd4f
Improve docs
jozkee Feb 6, 2026
7fd2c4a
Address jsonpolymorphic feedback and improve tests
jozkee Feb 6, 2026
2c5f6fb
McpToolCallApprovalResponseItem conversion to MEAI correctly and impl…
jozkee Feb 6, 2026
ced9e36
Fix roundtrip of McpServerToolResultContent.Result
jozkee Feb 6, 2026
89a0ba9
Refactor approval request ID generation in FunctionInvokingChatClient
jozkee Feb 6, 2026
d0a56eb
Address documentation feedback for MCP content types and Headers prop…
Copilot Feb 7, 2026
f9e7e0e
Apply documentation suggestions from @jozkee
Copilot Feb 7, 2026
a43576a
Add remarks to McpServerToolCallContent constructor documentation
Copilot Feb 9, 2026
420502e
Fix case-insensitive Bearer token extraction and add test, fix MakeAp…
Copilot Feb 9, 2026
ec9ebec
Trim both ends of header value and allow flexible whitespace after Be…
Copilot Feb 9, 2026
6127a72
Address feedback
jozkee Feb 10, 2026
80b4e96
Merge branch 'main' of https://github.com/dotnet/extensions into copi…
jozkee Feb 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,16 @@ namespace Microsoft.Extensions.AI;
[JsonDerivedType(typeof(TextReasoningContent), typeDiscriminator: "reasoning")]
[JsonDerivedType(typeof(UriContent), typeDiscriminator: "uri")]
[JsonDerivedType(typeof(UsageContent), typeDiscriminator: "usage")]
[JsonDerivedType(typeof(FunctionApprovalRequestContent), typeDiscriminator: "functionApprovalRequest")]
[JsonDerivedType(typeof(FunctionApprovalResponseContent), typeDiscriminator: "functionApprovalResponse")]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these right? Shouldn't they be InputRequestedContent / InputResponseContent?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They're needed.
If we replace functionApprovalRequest with base inputsRequest, Serialize<AIContent>(new FunctionApprovalRequestContent(…)) wouldn't work.
If we had both, it would work but it doesn't make sense to specify inputs as JsonDerived since that's non-instantiable, hence why is specified as [JsonSerializable].

Note: serialization of unknown types throws by default, so for FCC/FRC, we need to specify mcpTool*Content in both places.

Copy link
Member

@stephentoub stephentoub Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we replace functionApprovalRequest with base inputsRequest, Serialize(new FunctionApprovalRequestContent(…)) wouldn't work.

@eiriktsarpalis is that expected? The fact that this throws really surprises me:

using System.Text.Json;
using System.Text.Json.Serialization;

Console.WriteLine(JsonSerializer.Serialize<A>(new C()));

[JsonDerivedType(typeof(B), "b")]
class A { }

[JsonDerivedType(typeof(C), "c")]
class B : A { }

class C : B { }

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stephentoub Isn't your question answered in the doc I linked to?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Answered where? There's no ambiguity here.

Copy link
Member

@jozkee jozkee Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Answered where?

"To handle unknown derived types, you must opt in to such support using an annotation on the base type". Looks like you are expecting recursive lookup of JsonDerivedTypes and STJ only looks for the attributes of the T.

Filed dotnet/runtime#124198, furthermore, it is by design: dotnet/runtime#81236 (comment) (thanks MihuBot).

Copy link
Member

@stephentoub stephentoub Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like you are expecting recursive lookup of JsonDerivedTypes and STJ only looks for the attributes of the T.

Exactly.

it is by design: dotnet/runtime#81236 (comment) (thanks MihuBot).

I don't understand the explanation there, @eiriktsarpalis. If I have type Base, I'm the only one that can add attributes to Base. If I choose to add Middle in an attribute on Base, then either I'm also in control of Middle or I'm already giving away the ability to control what happens on deserialization. And if I'm in control of Middle, then recursively the same things apply to Middle, namely I'm the only one that can add attributes to it. What case are we trying to prevent here? The current solution is very awkward and unexpected, and it requires the same attribute with the same arguments in multiple places.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fact that this throws really surprises me:

It's not a behaviour we can guarantee in the general case, in a way that works well with trimmed/AOT apps:

// Assembly 1
[JsonDerivedType(typeof(B1), "b1")]
class A;

class B1;

// Assembly 2
[JsonDerivedType(typeof(C), "c")]
class B2 : A;

class C : B2;

// Console App
JsonSerializer.Deserialize<A>("""{ "type" : "c" }""");

One could argue of course that your example uses an unbroken chain of annotations which forces the entire hierarchy in a single project, but making a special case out of that is not something that was considered when the feature was being designed.

By default, polymorphic serialization is invariant but this can be changed via configuration in JsonPolymorphicAttribute. There is an open API proposal that I suspect addresses the same problem in an inverted manner, by having derived types inherit the polymorphic configuration of their base. This does violate encapsulation concerns on first inspection, however by forcing the entire transitive hierarchy on the base type it also avoids ambiguities like the one pointed out above.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// Assembly 1
// Assembly 2

I don't understand your example. A doesn't have an attribute on it for B2. This is not the same case as mine.

One could argue of course that your example uses an unbroken chain of annotations which forces the entire hierarchy in a single project, but making a special case out of that is not something that was considered when the feature was being designed.

I'm surprised we didn't consider that. This would seem to be the majority case. We should fix this.

[JsonDerivedType(typeof(McpServerToolCallContent), typeDiscriminator: "mcpServerToolCall")]
[JsonDerivedType(typeof(McpServerToolResultContent), typeDiscriminator: "mcpServerToolResult")]

// These should be added in once they're no longer [Experimental]. If they're included while still
// experimental, any JsonSerializerContext that includes AIContent will incur errors about using
// experimental types in its source generated files. When [Experimental] is removed from these types,
// these lines should be uncommented and the corresponding lines in AIJsonUtilities.CreateDefaultOptions
// as well as the [JsonSerializable] attributes for them on the JsonContext should be removed.
// [JsonDerivedType(typeof(FunctionApprovalRequestContent), typeDiscriminator: "functionApprovalRequest")]
// [JsonDerivedType(typeof(FunctionApprovalResponseContent), typeDiscriminator: "functionApprovalResponse")]
// [JsonDerivedType(typeof(McpServerToolCallContent), typeDiscriminator: "mcpServerToolCall")]
// [JsonDerivedType(typeof(McpServerToolResultContent), typeDiscriminator: "mcpServerToolResult")]
// [JsonDerivedType(typeof(McpServerToolApprovalRequestContent), typeDiscriminator: "mcpServerToolApprovalRequest")]
// [JsonDerivedType(typeof(McpServerToolApprovalResponseContent), typeDiscriminator: "mcpServerToolApprovalResponse")]
// [JsonDerivedType(typeof(CodeInterpreterToolCallContent), typeDiscriminator: "codeInterpreterToolCall")]
// [JsonDerivedType(typeof(CodeInterpreterToolResultContent), typeDiscriminator: "codeInterpreterToolResult")]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,39 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;

namespace Microsoft.Extensions.AI;

/// <summary>
/// Represents a request for user approval of a function call.
/// Represents a request for approval before invoking a function call.
/// </summary>
[Experimental(DiagnosticIds.Experiments.AIFunctionApprovals, UrlFormat = DiagnosticIds.UrlFormat)]
public sealed class FunctionApprovalRequestContent : UserInputRequestContent
public sealed class FunctionApprovalRequestContent : InputRequestContent
{
/// <summary>
/// Initializes a new instance of the <see cref="FunctionApprovalRequestContent"/> class.
/// </summary>
/// <param name="id">The ID that uniquely identifies the function approval request/response pair.</param>
/// <param name="functionCall">The function call that requires user approval.</param>
/// <exception cref="ArgumentNullException"><paramref name="id"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="id"/> is empty or composed entirely of whitespace.</exception>
/// <param name="requestId">The unique identifier that correlates this request with its corresponding response. This may differ from the <see cref="FunctionCallContent.CallId"/> of the specified <paramref name="functionCall"/>.</param>
/// <param name="functionCall">The function call that requires approval before execution.</param>
/// <exception cref="ArgumentNullException"><paramref name="requestId"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="requestId"/> is empty or composed entirely of whitespace.</exception>
/// <exception cref="ArgumentNullException"><paramref name="functionCall"/> is <see langword="null"/>.</exception>
public FunctionApprovalRequestContent(string id, FunctionCallContent functionCall)
: base(id)
public FunctionApprovalRequestContent(string requestId, FunctionCallContent functionCall)
: base(requestId)
{
FunctionCall = Throw.IfNull(functionCall);
}

/// <summary>
/// Gets the function call that pre-invoke approval is required for.
/// Gets the function call that requires approval before execution.
/// </summary>
public FunctionCallContent FunctionCall { get; }

/// <summary>
/// Creates a <see cref="FunctionApprovalResponseContent"/> to indicate whether the function call is approved or rejected based on the value of <paramref name="approved"/>.
/// Creates a <see cref="FunctionApprovalResponseContent"/> indicating whether the function call is approved or rejected.
/// </summary>
/// <param name="approved"><see langword="true"/> if the function call is approved; otherwise, <see langword="false"/>.</param>
/// <param name="reason">An optional reason for the approval or rejection.</param>
/// <returns>The <see cref="FunctionApprovalResponseContent"/> representing the approval response.</returns>
public FunctionApprovalResponseContent CreateResponse(bool approved, string? reason = null) => new(Id, approved, FunctionCall) { Reason = reason };
/// <returns>The <see cref="FunctionApprovalResponseContent"/> correlated with this request.</returns>
public FunctionApprovalResponseContent CreateResponse(bool approved, string? reason = null) => new(RequestId, approved, FunctionCall) { Reason = reason };
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,38 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;

namespace Microsoft.Extensions.AI;

/// <summary>
/// Represents a response to a function approval request.
/// Represents a response to a <see cref="FunctionApprovalRequestContent"/>, indicating whether the function call was approved.
/// </summary>
[Experimental(DiagnosticIds.Experiments.AIFunctionApprovals, UrlFormat = DiagnosticIds.UrlFormat)]
public sealed class FunctionApprovalResponseContent : UserInputResponseContent
public sealed class FunctionApprovalResponseContent : InputResponseContent
{
/// <summary>
/// Initializes a new instance of the <see cref="FunctionApprovalResponseContent"/> class.
/// </summary>
/// <param name="id">The ID that uniquely identifies the function approval request/response pair.</param>
/// <param name="requestId">The unique identifier of the <see cref="FunctionApprovalRequestContent"/> associated with this response.</param>
/// <param name="approved"><see langword="true"/> if the function call is approved; otherwise, <see langword="false"/>.</param>
/// <param name="functionCall">The function call that requires user approval.</param>
/// <exception cref="ArgumentNullException"><paramref name="id"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="id"/> is empty or composed entirely of whitespace.</exception>
/// <param name="functionCall">The function call that was subject to approval.</param>
/// <exception cref="ArgumentNullException"><paramref name="requestId"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="requestId"/> is empty or composed entirely of whitespace.</exception>
/// <exception cref="ArgumentNullException"><paramref name="functionCall"/> is <see langword="null"/>.</exception>
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);
}

/// <summary>
/// Gets a value indicating whether the user approved the request.
/// Gets a value indicating whether the function call was approved for execution.
/// </summary>
public bool Approved { get; }

/// <summary>
/// Gets the function call for which approval was requested.
/// Gets the function call that was subject to approval.
/// </summary>
public FunctionCallContent FunctionCall { get; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ namespace Microsoft.Extensions.AI;
/// Represents a function call request.
/// </summary>
[DebuggerDisplay("{DebuggerDisplay,nq}")]
[JsonDerivedType(typeof(McpServerToolCallContent), "mcpServerToolCall")]
public class FunctionCallContent : AIContent
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ namespace Microsoft.Extensions.AI;
/// Represents the result of a function call.
/// </summary>
[DebuggerDisplay("{DebuggerDisplay,nq}")]
[JsonDerivedType(typeof(McpServerToolResultContent), "mcpServerToolResult")]
public class FunctionResultContent : AIContent
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Text.Json.Serialization;
using Microsoft.Shared.Diagnostics;

namespace Microsoft.Extensions.AI;

/// <summary>
/// Represents a request for input from the user or application.
/// </summary>
[JsonDerivedType(typeof(FunctionApprovalRequestContent), "functionApprovalRequest")]
public class InputRequestContent : AIContent
{
/// <summary>
/// Initializes a new instance of the <see cref="InputRequestContent"/> class.
/// </summary>
/// <param name="requestId">The unique identifier that correlates this request with its corresponding response.</param>
/// <exception cref="ArgumentNullException"><paramref name="requestId"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="requestId"/> is empty or composed entirely of whitespace.</exception>
protected InputRequestContent(string requestId)
{
RequestId = Throw.IfNullOrWhitespace(requestId);
}

/// <summary>
/// Gets the unique identifier that correlates this request with its corresponding <see cref="InputResponseContent"/>.
/// </summary>
public string RequestId { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Text.Json.Serialization;
using Microsoft.Shared.Diagnostics;

namespace Microsoft.Extensions.AI;

/// <summary>
/// Represents the response to an <see cref="InputRequestContent"/>.
/// </summary>
[JsonDerivedType(typeof(FunctionApprovalResponseContent), "functionApprovalResponse")]
public class InputResponseContent : AIContent
{
/// <summary>
/// Initializes a new instance of the <see cref="InputResponseContent"/> class.
/// </summary>
/// <param name="requestId">The unique identifier that correlates this response with its corresponding request.</param>
/// <exception cref="ArgumentNullException"><paramref name="requestId"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="requestId"/> is empty or composed entirely of whitespace.</exception>
protected InputResponseContent(string requestId)
{
RequestId = Throw.IfNullOrWhitespace(requestId);
}

/// <summary>
/// Gets the unique identifier that correlates this response with its corresponding <see cref="InputRequestContent"/>.
/// </summary>
public string RequestId { get; }
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;

namespace Microsoft.Extensions.AI;
Expand All @@ -13,44 +10,35 @@ namespace Microsoft.Extensions.AI;
/// Represents a tool call request to a MCP server.
/// </summary>
/// <remarks>
/// <para>
/// This content type is used to represent an invocation of an MCP server tool by a hosted service.
/// It is informational only.
/// It is informational only and may appear as part of an approval request
/// to convey what is being approved, or as a record of which MCP server tool was invoked.
/// </para>
/// </remarks>
[Experimental(DiagnosticIds.Experiments.AIMcpServers, UrlFormat = DiagnosticIds.UrlFormat)]
public sealed class McpServerToolCallContent : AIContent
public sealed class McpServerToolCallContent : FunctionCallContent
{
/// <summary>
/// Initializes a new instance of the <see cref="McpServerToolCallContent"/> class.
/// </summary>
/// <param name="callId">The tool call ID.</param>
/// <param name="toolName">The tool name.</param>
/// <param name="name">The tool name.</param>
/// <param name="serverName">The MCP server name that hosts the tool.</param>
/// <exception cref="ArgumentNullException"><paramref name="callId"/> or <paramref name="toolName"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="callId"/> or <paramref name="toolName"/> is empty or composed entirely of whitespace.</exception>
public McpServerToolCallContent(string callId, string toolName, string? serverName)
/// <exception cref="ArgumentNullException"><paramref name="callId"/> or <paramref name="name"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="callId"/> or <paramref name="name"/> is empty or composed entirely of whitespace.</exception>
/// <remarks>
/// 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.
/// </remarks>
public McpServerToolCallContent(string callId, string name, string? serverName)
: base(Throw.IfNullOrWhitespace(callId), Throw.IfNullOrWhitespace(name))
{
CallId = Throw.IfNullOrWhitespace(callId);
ToolName = Throw.IfNullOrWhitespace(toolName);
ServerName = serverName;
InformationalOnly = true;
}

/// <summary>
/// Gets the tool call ID.
/// </summary>
public string CallId { get; }

/// <summary>
/// Gets the name of the tool called.
/// </summary>
public string ToolName { get; }

/// <summary>
/// Gets the name of the MCP server that hosts the tool.
/// </summary>
public string? ServerName { get; }

/// <summary>
/// Gets or sets the arguments used for the tool call.
/// </summary>
public IReadOnlyDictionary<string, object?>? Arguments { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;

namespace Microsoft.Extensions.AI;
Expand All @@ -16,8 +13,7 @@ namespace Microsoft.Extensions.AI;
/// This content type is used to represent the result of an invocation of an MCP server tool by a hosted service.
/// It is informational only.
/// </remarks>
[Experimental(DiagnosticIds.Experiments.AIMcpServers, UrlFormat = DiagnosticIds.UrlFormat)]
public sealed class McpServerToolResultContent : AIContent
public sealed class McpServerToolResultContent : FunctionResultContent
{
/// <summary>
/// Initializes a new instance of the <see cref="McpServerToolResultContent"/> class.
Expand All @@ -26,17 +22,7 @@ public sealed class McpServerToolResultContent : AIContent
/// <exception cref="ArgumentNullException"><paramref name="callId"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="callId"/> is empty or composed entirely of whitespace.</exception>
public McpServerToolResultContent(string callId)
: base(Throw.IfNullOrWhitespace(callId), result: null)
{
CallId = Throw.IfNullOrWhitespace(callId);
}

/// <summary>
/// Gets the tool call ID.
/// </summary>
public string CallId { get; }

/// <summary>
/// Gets or sets the output of the tool call.
/// </summary>
public IList<AIContent>? Output { get; set; }
}
Loading
Loading