diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/RequiredChatToolMode.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/RequiredChatToolMode.cs
index 59ce51e7ef3..899ba04251e 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/RequiredChatToolMode.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/RequiredChatToolMode.cs
@@ -41,12 +41,6 @@ public RequiredChatToolMode(string? requiredFunctionName)
RequiredFunctionName = requiredFunctionName;
}
- // The reason for not overriding Equals/GetHashCode (e.g., so two instances are equal if they
- // have the same RequiredFunctionName) is to leave open the option to unseal the type in the
- // future. If we did define equality based on RequiredFunctionName but a subclass added further
- // fields, this would lead to wrong behavior unless the subclass author remembers to re-override
- // Equals/GetHashCode as well, which they likely won't.
-
/// Gets a string representing this instance to display in the debugger.
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private string DebuggerDisplay => $"Required: {RequiredFunctionName ?? "Any"}";
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs
index e3ee10ad50a..a0e240be991 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs
@@ -24,6 +24,10 @@ namespace Microsoft.Extensions.AI;
// experimental types in its source generated files.
// [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")]
public class AIContent
{
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolApprovalRequestContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolApprovalRequestContent.cs
new file mode 100644
index 00000000000..8f302d901b4
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolApprovalRequestContent.cs
@@ -0,0 +1,41 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Extensions.AI;
+
+///
+/// Represents a request for user approval of an MCP server tool call.
+///
+[Experimental("MEAI001")]
+public sealed class McpServerToolApprovalRequestContent : UserInputRequestContent
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The ID that uniquely identifies the MCP server tool approval request/response pair.
+ /// The tool call that requires user approval.
+ /// is .
+ /// is empty or composed entirely of whitespace.
+ /// is .
+ public McpServerToolApprovalRequestContent(string id, McpServerToolCallContent toolCall)
+ : base(id)
+ {
+ ToolCall = Throw.IfNull(toolCall);
+ }
+
+ ///
+ /// Gets the tool call that pre-invoke approval is required for.
+ ///
+ public McpServerToolCallContent ToolCall { get; }
+
+ ///
+ /// Creates a to indicate whether the function call is approved or rejected based on the value of .
+ ///
+ /// if the function call is approved; otherwise, .
+ /// The representing the approval response.
+ public McpServerToolApprovalResponseContent CreateResponse(bool approved) => new(Id, approved);
+}
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolApprovalResponseContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolApprovalResponseContent.cs
new file mode 100644
index 00000000000..0e239a79d7f
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolApprovalResponseContent.cs
@@ -0,0 +1,32 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Microsoft.Extensions.AI;
+
+///
+/// Represents a response to an MCP server tool approval request.
+///
+[Experimental("MEAI001")]
+public sealed class McpServerToolApprovalResponseContent : UserInputResponseContent
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The ID that uniquely identifies the MCP server tool approval request/response pair.
+ /// if the MCP server tool call is approved; otherwise, .
+ /// is .
+ /// is empty or composed entirely of whitespace.
+ public McpServerToolApprovalResponseContent(string id, bool approved)
+ : base(id)
+ {
+ Approved = approved;
+ }
+
+ ///
+ /// Gets a value indicating whether the user approved the request.
+ ///
+ public bool Approved { get; }
+}
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs
new file mode 100644
index 00000000000..5ed6385789c
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs
@@ -0,0 +1,55 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Shared.Diagnostics;
+
+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.
+///
+[Experimental("MEAI001")]
+public sealed class McpServerToolCallContent : AIContent
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The tool call ID.
+ /// The tool name.
+ /// The MCP server name.
+ /// , , or are .
+ /// , , or are empty or composed entirely of whitespace.
+ public McpServerToolCallContent(string callId, string toolName, string serverName)
+ {
+ CallId = Throw.IfNullOrWhitespace(callId);
+ ToolName = Throw.IfNullOrWhitespace(toolName);
+ ServerName = Throw.IfNullOrWhitespace(serverName);
+ }
+
+ ///
+ /// 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.
+ ///
+ 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
new file mode 100644
index 00000000000..b8329c74d99
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs
@@ -0,0 +1,41 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Extensions.AI;
+
+///
+/// Represents the result of a MCP server tool call.
+///
+///
+/// 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("MEAI001")]
+public sealed class McpServerToolResultContent : AIContent
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The tool call ID.
+ /// is .
+ /// is empty or composed entirely of whitespace.
+ public McpServerToolResultContent(string callId)
+ {
+ 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/HostedMcpServerToolAlwaysRequireApprovalMode.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolAlwaysRequireApprovalMode.cs
new file mode 100644
index 00000000000..388ffbc2f7f
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolAlwaysRequireApprovalMode.cs
@@ -0,0 +1,30 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Microsoft.Extensions.AI;
+
+///
+/// Indicates that approval is always required for tool calls to a hosted MCP server.
+///
+///
+/// Use to get an instance of .
+///
+[Experimental("MEAI001")]
+[DebuggerDisplay(nameof(AlwaysRequire))]
+public sealed class HostedMcpServerToolAlwaysRequireApprovalMode : HostedMcpServerToolApprovalMode
+{
+ /// Initializes a new instance of the class.
+ /// Use to get an instance of .
+ public HostedMcpServerToolAlwaysRequireApprovalMode()
+ {
+ }
+
+ ///
+ public override bool Equals(object? obj) => obj is HostedMcpServerToolAlwaysRequireApprovalMode;
+
+ ///
+ public override int GetHashCode() => typeof(HostedMcpServerToolAlwaysRequireApprovalMode).GetHashCode();
+}
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolApprovalMode.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolApprovalMode.cs
new file mode 100644
index 00000000000..47c3e3e48db
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolApprovalMode.cs
@@ -0,0 +1,48 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// 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;
+
+namespace Microsoft.Extensions.AI;
+
+///
+/// Describes how approval is required for tool calls to a hosted MCP server.
+///
+///
+/// The predefined values , and are provided to specify handling for all tools.
+/// To specify approval behavior for individual tool names, use .
+///
+[Experimental("MEAI001")]
+[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]
+[JsonDerivedType(typeof(HostedMcpServerToolNeverRequireApprovalMode), typeDiscriminator: "never")]
+[JsonDerivedType(typeof(HostedMcpServerToolAlwaysRequireApprovalMode), typeDiscriminator: "always")]
+[JsonDerivedType(typeof(HostedMcpServerToolRequireSpecificApprovalMode), typeDiscriminator: "requireSpecific")]
+#pragma warning disable CA1052 // Static holder types should be Static or NotInheritable
+public class HostedMcpServerToolApprovalMode
+#pragma warning restore CA1052
+{
+ ///
+ /// Gets a predefined indicating that all tool calls to a hosted MCP server always require approval.
+ ///
+ public static HostedMcpServerToolAlwaysRequireApprovalMode AlwaysRequire { get; } = new();
+
+ ///
+ /// Gets a predefined indicating that all tool calls to a hosted MCP server never require approval.
+ ///
+ public static HostedMcpServerToolNeverRequireApprovalMode NeverRequire { get; } = new();
+
+ private protected HostedMcpServerToolApprovalMode()
+ {
+ }
+
+ ///
+ /// Instantiates a that specifies approval behavior for individual tool names.
+ ///
+ /// The list of tools names that always require approval.
+ /// The list of tools names that never require approval.
+ /// An instance of for the specified tool names.
+ public static HostedMcpServerToolRequireSpecificApprovalMode RequireSpecific(IList? alwaysRequireApprovalToolNames, IList? neverRequireApprovalToolNames)
+ => new(alwaysRequireApprovalToolNames, neverRequireApprovalToolNames);
+}
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolNeverRequireApprovalMode.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolNeverRequireApprovalMode.cs
new file mode 100644
index 00000000000..bca80649f0d
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolNeverRequireApprovalMode.cs
@@ -0,0 +1,30 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Microsoft.Extensions.AI;
+
+///
+/// Indicates that approval is never required for tool calls to a hosted MCP server.
+///
+///
+/// Use to get an instance of .
+///
+[Experimental("MEAI001")]
+[DebuggerDisplay(nameof(NeverRequire))]
+public sealed class HostedMcpServerToolNeverRequireApprovalMode : HostedMcpServerToolApprovalMode
+{
+ /// Initializes a new instance of the class.
+ /// Use to get an instance of .
+ public HostedMcpServerToolNeverRequireApprovalMode()
+ {
+ }
+
+ ///
+ public override bool Equals(object? obj) => obj is HostedMcpServerToolNeverRequireApprovalMode;
+
+ ///
+ public override int GetHashCode() => typeof(HostedMcpServerToolNeverRequireApprovalMode).GetHashCode();
+}
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolRequireSpecificApprovalMode.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolRequireSpecificApprovalMode.cs
new file mode 100644
index 00000000000..a5e870af7e7
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolRequireSpecificApprovalMode.cs
@@ -0,0 +1,66 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+
+namespace Microsoft.Extensions.AI;
+
+///
+/// Represents a mode where approval behavior is specified for individual tool names.
+///
+[Experimental("MEAI001")]
+public sealed class HostedMcpServerToolRequireSpecificApprovalMode : HostedMcpServerToolApprovalMode
+{
+ ///
+ /// Initializes a new instance of the class that specifies approval behavior for individual tool names.
+ ///
+ /// The list of tools names that always require approval.
+ /// The list of tools names that never require approval.
+ public HostedMcpServerToolRequireSpecificApprovalMode(IList? alwaysRequireApprovalToolNames, IList? neverRequireApprovalToolNames)
+ {
+ AlwaysRequireApprovalToolNames = alwaysRequireApprovalToolNames;
+ NeverRequireApprovalToolNames = neverRequireApprovalToolNames;
+ }
+
+ ///
+ /// Gets or sets the list of tool names that always require approval.
+ ///
+ public IList? AlwaysRequireApprovalToolNames { get; set; }
+
+ ///
+ /// Gets or sets the list of tool names that never require approval.
+ ///
+ public IList? NeverRequireApprovalToolNames { get; set; }
+
+ ///
+ public override bool Equals(object? obj) => obj is HostedMcpServerToolRequireSpecificApprovalMode other &&
+ ListEquals(AlwaysRequireApprovalToolNames, other.AlwaysRequireApprovalToolNames) &&
+ ListEquals(NeverRequireApprovalToolNames, other.NeverRequireApprovalToolNames);
+
+ ///
+ public override int GetHashCode() =>
+ HashCode.Combine(GetListHashCode(AlwaysRequireApprovalToolNames), GetListHashCode(NeverRequireApprovalToolNames));
+
+ private static bool ListEquals(IList? list1, IList? list2) =>
+ ReferenceEquals(list1, list2) ||
+ (list1 is not null && list2 is not null && list1.SequenceEqual(list2));
+
+ private static int GetListHashCode(IList? list)
+ {
+ if (list is null)
+ {
+ return 0;
+ }
+
+ HashCode hc = default;
+ foreach (string item in list)
+ {
+ hc.Add(item);
+ }
+
+ return hc.ToHashCode();
+ }
+}
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj
index f5472854def..25aa2f4c49d 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj
@@ -32,6 +32,7 @@
+
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/AITool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/AITool.cs
similarity index 100%
rename from src/Libraries/Microsoft.Extensions.AI.Abstractions/AITool.cs
rename to src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/AITool.cs
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedCodeInterpreterTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedCodeInterpreterTool.cs
similarity index 100%
rename from src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedCodeInterpreterTool.cs
rename to src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedCodeInterpreterTool.cs
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedFileSearchTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedFileSearchTool.cs
similarity index 100%
rename from src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedFileSearchTool.cs
rename to src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedFileSearchTool.cs
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs
new file mode 100644
index 00000000000..54e760c78dd
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs
@@ -0,0 +1,89 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Extensions.AI;
+
+///
+/// Represents a hosted MCP server tool that can be specified to an AI service.
+///
+[Experimental("MEAI001")]
+public class HostedMcpServerTool : AITool
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The name of the remote MCP server.
+ /// The URL of the remote MCP server.
+ /// or are .
+ /// is empty or composed entirely of whitespace.
+ public HostedMcpServerTool(string serverName, [StringSyntax(StringSyntaxAttribute.Uri)] string url)
+ : this(serverName, new Uri(Throw.IfNull(url)))
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The name of the remote MCP server.
+ /// The URL of the remote MCP server.
+ /// or are .
+ /// is empty or composed entirely of whitespace.
+ public HostedMcpServerTool(string serverName, Uri url)
+ {
+ ServerName = Throw.IfNullOrWhitespace(serverName);
+ Url = Throw.IfNull(url);
+ }
+
+ ///
+ /// Gets the name of the remote MCP server that is used to identify it.
+ ///
+ public string ServerName { get; }
+
+ ///
+ /// Gets the URL of the remote MCP server.
+ ///
+ public Uri Url { get; }
+
+ ///
+ /// Gets or sets the description of the remote MCP server, used to provide more context to the AI service.
+ ///
+ public string? ServerDescription { get; set; }
+
+ ///
+ /// Gets or sets the list of tools allowed to be used by the AI service.
+ ///
+ ///
+ /// The default value is , which allows any tool to be used.
+ ///
+ public IList? AllowedTools { get; set; }
+
+ ///
+ /// Gets or sets the approval mode that indicates when the AI service should require user approval for tool calls to the remote MCP server.
+ ///
+ ///
+ ///
+ /// You can set this property to to require approval for all tool calls,
+ /// or to to never require approval.
+ ///
+ ///
+ /// The default value is , which some providers may treat the same as .
+ ///
+ ///
+ /// The underlying provider is not guaranteed to support or honor the approval mode.
+ ///
+ ///
+ public HostedMcpServerToolApprovalMode? ApprovalMode { get; set; }
+
+ ///
+ /// Gets or sets the HTTP headers that the AI service should use when calling 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; }
+}
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedWebSearchTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedWebSearchTool.cs
similarity index 100%
rename from src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedWebSearchTool.cs
rename to src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedWebSearchTool.cs
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIJsonContext.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIJsonContext.cs
index b4f75fe2c94..ab770568f69 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIJsonContext.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIJsonContext.cs
@@ -14,6 +14,7 @@ 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 0acfef52eb0..c025ae14d8a 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs
@@ -159,6 +159,25 @@ internal static IEnumerable ToChatMessages(IEnumerable FromOpenAIStreamingRe
string? modelId = null;
string? lastMessageId = null;
ChatRole? lastRole = null;
- Dictionary outputIndexToMessages = [];
- Dictionary? functionCallItems = null;
+ bool anyFunctions = false;
await foreach (var streamingUpdate in streamingResponseUpdates.WithCancellation(cancellationToken).ConfigureAwait(false))
{
@@ -229,7 +247,7 @@ ChatResponseUpdate CreateUpdate(AIContent? content = null) =>
var update = CreateUpdate(ToUsageDetails(completedUpdate.Response) is { } usage ? new UsageContent(usage) : null);
update.FinishReason =
ToFinishReason(completedUpdate.Response?.IncompleteStatusDetails?.Reason) ??
- (functionCallItems is not null ? ChatFinishReason.ToolCalls :
+ (anyFunctions ? ChatFinishReason.ToolCalls :
ChatFinishReason.Stop);
yield return update;
break;
@@ -239,65 +257,59 @@ ChatResponseUpdate CreateUpdate(AIContent? content = null) =>
switch (outputItemAddedUpdate.Item)
{
case MessageResponseItem mri:
- outputIndexToMessages[outputItemAddedUpdate.OutputIndex] = mri;
+ lastMessageId = outputItemAddedUpdate.Item.Id;
+ lastRole = ToChatRole(mri.Role);
break;
case FunctionCallResponseItem fcri:
- (functionCallItems ??= [])[outputItemAddedUpdate.OutputIndex] = fcri;
+ anyFunctions = true;
+ lastRole = ChatRole.Assistant;
break;
}
goto default;
- case StreamingResponseOutputItemDoneUpdate outputItemDoneUpdate:
- _ = outputIndexToMessages.Remove(outputItemDoneUpdate.OutputIndex);
-
- if (outputItemDoneUpdate.Item is MessageResponseItem item &&
- item.Content is { Count: > 0 } content &&
- content.Any(c => c.OutputTextAnnotations is { Count: > 0 }))
- {
- AIContent annotatedContent = new();
- foreach (var c in content)
- {
- PopulateAnnotations(c, annotatedContent);
- }
-
- yield return CreateUpdate(annotatedContent);
- break;
- }
-
- goto default;
-
case StreamingResponseOutputTextDeltaUpdate outputTextDeltaUpdate:
- {
- _ = outputIndexToMessages.TryGetValue(outputTextDeltaUpdate.OutputIndex, out MessageResponseItem? messageItem);
- lastMessageId = messageItem?.Id;
- lastRole = ToChatRole(messageItem?.Role);
-
yield return CreateUpdate(new TextContent(outputTextDeltaUpdate.Delta));
break;
- }
- case StreamingResponseFunctionCallArgumentsDoneUpdate functionCallOutputDoneUpdate:
- {
- if (functionCallItems?.TryGetValue(functionCallOutputDoneUpdate.OutputIndex, out FunctionCallResponseItem? callInfo) is true)
- {
- _ = functionCallItems.Remove(functionCallOutputDoneUpdate.OutputIndex);
+ case StreamingResponseOutputItemDoneUpdate outputItemDoneUpdate when outputItemDoneUpdate.Item is FunctionCallResponseItem fcri:
+ yield return CreateUpdate(OpenAIClientExtensions.ParseCallContent(fcri.FunctionArguments.ToString(), fcri.CallId, fcri.FunctionName));
+ break;
- var fcc = OpenAIClientExtensions.ParseCallContent(
- functionCallOutputDoneUpdate.FunctionArguments.ToString(),
- callInfo.CallId,
- callInfo.FunctionName);
+ case StreamingResponseOutputItemDoneUpdate outputItemDoneUpdate when outputItemDoneUpdate.Item is McpToolCallItem mtci:
+ var mcpUpdate = CreateUpdate();
+ AddMcpToolCallContent(mtci, mcpUpdate.Contents);
+ yield return mcpUpdate;
+ break;
- lastMessageId = callInfo.Id;
- lastRole = ChatRole.Assistant;
+ case StreamingResponseOutputItemDoneUpdate outputItemDoneUpdate when outputItemDoneUpdate.Item is McpToolDefinitionListItem mtdli:
+ yield return CreateUpdate(new AIContent { RawRepresentation = mtdli });
+ break;
- yield return CreateUpdate(fcc);
- break;
+ case StreamingResponseOutputItemDoneUpdate outputItemDoneUpdate when outputItemDoneUpdate.Item is McpToolCallApprovalRequestItem mtcari:
+ yield return CreateUpdate(new McpServerToolApprovalRequestContent(mtcari.Id, new(mtcari.Id, mtcari.ToolName, mtcari.ServerLabel)
+ {
+ Arguments = JsonSerializer.Deserialize(mtcari.ToolArguments.ToMemory().Span, OpenAIJsonContext.Default.IReadOnlyDictionaryStringObject)!,
+ RawRepresentation = mtcari,
+ })
+ {
+ RawRepresentation = mtcari,
+ });
+ break;
+
+ case StreamingResponseOutputItemDoneUpdate outputItemDoneUpdate when
+ outputItemDoneUpdate.Item is MessageResponseItem mri &&
+ mri.Content is { Count: > 0 } content &&
+ content.Any(c => c.OutputTextAnnotations is { Count: > 0 }):
+ AIContent annotatedContent = new();
+ foreach (var c in content)
+ {
+ PopulateAnnotations(c, annotatedContent);
}
- goto default;
- }
+ yield return CreateUpdate(annotatedContent);
+ break;
case StreamingResponseErrorUpdate errorUpdate:
yield return CreateUpdate(new ErrorContent(errorUpdate.Message)
@@ -440,6 +452,49 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt
result.Tools.Add(ModelReaderWriter.Read(BinaryData.FromString(json)));
break;
+
+ case HostedMcpServerTool mcpTool:
+ McpTool responsesMcpTool = ResponseTool.CreateMcpTool(
+ mcpTool.ServerName,
+ mcpTool.Url,
+ mcpTool.Headers);
+
+ if (mcpTool.AllowedTools is not null)
+ {
+ responsesMcpTool.AllowedTools = new();
+ AddAllMcpFilters(mcpTool.AllowedTools, responsesMcpTool.AllowedTools);
+ }
+
+ switch (mcpTool.ApprovalMode)
+ {
+ case HostedMcpServerToolAlwaysRequireApprovalMode:
+ responsesMcpTool.ToolCallApprovalPolicy = new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.AlwaysRequireApproval);
+ break;
+
+ case HostedMcpServerToolNeverRequireApprovalMode:
+ responsesMcpTool.ToolCallApprovalPolicy = new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.NeverRequireApproval);
+ break;
+
+ case HostedMcpServerToolRequireSpecificApprovalMode specificMode:
+ responsesMcpTool.ToolCallApprovalPolicy = new McpToolCallApprovalPolicy(new CustomMcpToolCallApprovalPolicy());
+
+ if (specificMode.AlwaysRequireApprovalToolNames is { Count: > 0 } alwaysRequireToolNames)
+ {
+ responsesMcpTool.ToolCallApprovalPolicy.CustomPolicy.ToolsAlwaysRequiringApproval = new();
+ AddAllMcpFilters(alwaysRequireToolNames, responsesMcpTool.ToolCallApprovalPolicy.CustomPolicy.ToolsAlwaysRequiringApproval);
+ }
+
+ if (specificMode.NeverRequireApprovalToolNames is { Count: > 0 } neverRequireToolNames)
+ {
+ responsesMcpTool.ToolCallApprovalPolicy.CustomPolicy.ToolsNeverRequiringApproval = new();
+ AddAllMcpFilters(neverRequireToolNames, responsesMcpTool.ToolCallApprovalPolicy.CustomPolicy.ToolsNeverRequiringApproval);
+ }
+
+ break;
+ }
+
+ result.Tools.Add(responsesMcpTool);
+ break;
}
}
@@ -497,6 +552,8 @@ internal static IEnumerable ToOpenAIResponseItems(IEnumerable? idToContentMapping = null;
+
foreach (ChatMessage input in inputs)
{
if (input.Role == ChatRole.System ||
@@ -545,6 +602,10 @@ internal static IEnumerable ToOpenAIResponseItems(IEnumerable ToOpenAIResponseItems(IEnumerable)))));
break;
+
+ case McpServerToolApprovalRequestContent mcpApprovalRequestContent:
+ // BUG https://github.com/openai/openai-dotnet/issues/664: Needs to be able to set an approvalRequestId
+ yield return ResponseItem.CreateMcpApprovalRequestItem(
+ mcpApprovalRequestContent.ToolCall.ServerName,
+ mcpApprovalRequestContent.ToolCall.ToolName,
+ BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(mcpApprovalRequestContent.ToolCall.Arguments!, OpenAIJsonContext.Default.IReadOnlyDictionaryStringObject)));
+ break;
+
+ case McpServerToolCallContent mstcc:
+ (idToContentMapping ??= [])[mstcc.CallId] = mstcc;
+ break;
+
+ case McpServerToolResultContent mstrc:
+ if (idToContentMapping?.TryGetValue(mstrc.CallId, out AIContent? callContentFromMapping) is true &&
+ callContentFromMapping is McpServerToolCallContent associatedCall)
+ {
+ _ = 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)
+ {
+ mtci.Error = BinaryData.FromString(errorContent.Message);
+ }
+ else
+ {
+ mtci.ToolOutput = string.Concat(mstrc.Output?.OfType() ?? []);
+ }
+
+ yield return mtci;
+ }
+
+ break;
}
}
@@ -745,4 +841,34 @@ private static List ToResponseContentParts(IList
return parts;
}
+
+ /// Adds new for the specified into .
+ private static void AddMcpToolCallContent(McpToolCallItem mtci, IList contents)
+ {
+ contents.Add(new McpServerToolCallContent(mtci.Id, mtci.ToolName, mtci.ServerLabel)
+ {
+ Arguments = JsonSerializer.Deserialize(mtci.ToolArguments.ToMemory().Span, OpenAIJsonContext.Default.IReadOnlyDictionaryStringObject)!,
+
+ // 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
+ // McpToolCallItem sent back for the pair.
+ });
+
+ contents.Add(new McpServerToolResultContent(mtci.Id)
+ {
+ RawRepresentation = mtci,
+ Output = [mtci.Error is not null ?
+ new ErrorContent(mtci.Error.ToString()) :
+ new TextContent(mtci.ToolOutput)],
+ });
+ }
+
+ /// Adds all of the tool names from to .
+ private static void AddAllMcpFilters(IList toolNames, McpToolFilter filter)
+ {
+ foreach (var toolName in toolNames)
+ {
+ filter.ToolNames.Add(toolName);
+ }
+ }
}
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests..cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests.cs
similarity index 100%
rename from test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests..cs
rename to test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests.cs
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolCallContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolCallContentTests.cs
new file mode 100644
index 00000000000..ce6516124cd
--- /dev/null
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolCallContentTests.cs
@@ -0,0 +1,63 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using Xunit;
+
+namespace Microsoft.Extensions.AI;
+
+public class McpServerToolCallContentTests
+{
+ [Fact]
+ public void Constructor_PropsDefault()
+ {
+ McpServerToolCallContent c = new("callId1", "toolName", "serverName");
+
+ Assert.Null(c.RawRepresentation);
+ Assert.Null(c.AdditionalProperties);
+
+ Assert.Equal("callId1", c.CallId);
+ Assert.Equal("toolName", c.ToolName);
+ Assert.Equal("serverName", c.ServerName);
+
+ Assert.Null(c.Arguments);
+ }
+
+ [Fact]
+ public void Constructor_PropsRoundtrip()
+ {
+ McpServerToolCallContent c = new("callId1", "toolName", "serverName");
+
+ Assert.Null(c.RawRepresentation);
+ object raw = new();
+ c.RawRepresentation = raw;
+ Assert.Same(raw, c.RawRepresentation);
+
+ Assert.Null(c.AdditionalProperties);
+ AdditionalPropertiesDictionary props = new() { { "key", "value" } };
+ c.AdditionalProperties = props;
+ Assert.Same(props, c.AdditionalProperties);
+
+ Assert.Null(c.Arguments);
+ IReadOnlyDictionary args = new Dictionary();
+ c.Arguments = args;
+ Assert.Same(args, c.Arguments);
+
+ Assert.Equal("callId1", c.CallId);
+ Assert.Equal("toolName", c.ToolName);
+ Assert.Equal("serverName", c.ServerName);
+ }
+
+ [Fact]
+ public void Constructor_Throws()
+ {
+ Assert.Throws("callId", () => new McpServerToolCallContent(string.Empty, "name", "serverName"));
+ Assert.Throws("toolName", () => new McpServerToolCallContent("callId1", string.Empty, "serverName"));
+ Assert.Throws("serverName", () => new McpServerToolCallContent("callId1", "name", string.Empty));
+
+ Assert.Throws("callId", () => new McpServerToolCallContent(null!, "name", "serverName"));
+ Assert.Throws("toolName", () => new McpServerToolCallContent("callId1", null!, "serverName"));
+ Assert.Throws("serverName", () => new McpServerToolCallContent("callId1", "name", null!));
+ }
+}
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolResultContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolResultContentTests.cs
new file mode 100644
index 00000000000..ec20b37c9e2
--- /dev/null
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolResultContentTests.cs
@@ -0,0 +1,51 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using Xunit;
+
+namespace Microsoft.Extensions.AI;
+
+public class McpServerToolResultContentTests
+{
+ [Fact]
+ public void Constructor_PropsDefault()
+ {
+ McpServerToolResultContent c = new("callId");
+ Assert.Equal("callId", c.CallId);
+ Assert.Null(c.RawRepresentation);
+ Assert.Null(c.AdditionalProperties);
+ Assert.Null(c.Output);
+ }
+
+ [Fact]
+ public void Constructor_PropsRoundtrip()
+ {
+ McpServerToolResultContent c = new("callId");
+
+ Assert.Null(c.RawRepresentation);
+ object raw = new();
+ c.RawRepresentation = raw;
+ Assert.Same(raw, c.RawRepresentation);
+
+ Assert.Null(c.AdditionalProperties);
+ AdditionalPropertiesDictionary props = new() { { "key", "value" } };
+ c.AdditionalProperties = props;
+ Assert.Same(props, c.AdditionalProperties);
+
+ Assert.Equal("callId", c.CallId);
+
+ Assert.Null(c.Output);
+ IList output = [];
+ c.Output = output;
+ Assert.Same(output, c.Output);
+ }
+
+ [Fact]
+ public void Constructor_Throws()
+ {
+ Assert.Throws("callId", () => new McpServerToolResultContent(string.Empty));
+ Assert.Throws("callId", () => new McpServerToolResultContent(null!));
+ }
+}
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/HostedMcpServerToolApprovalModeTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/HostedMcpServerToolApprovalModeTests.cs
new file mode 100644
index 00000000000..3ad4690130a
--- /dev/null
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/HostedMcpServerToolApprovalModeTests.cs
@@ -0,0 +1,48 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Text.Json;
+using Xunit;
+
+namespace Microsoft.Extensions.AI;
+
+public class HostedMcpServerToolApprovalModeTests
+{
+ [Fact]
+ public void Singletons_Idempotent()
+ {
+ Assert.Same(HostedMcpServerToolApprovalMode.AlwaysRequire, HostedMcpServerToolApprovalMode.AlwaysRequire);
+ Assert.Same(HostedMcpServerToolApprovalMode.NeverRequire, HostedMcpServerToolApprovalMode.NeverRequire);
+ }
+
+ [Fact]
+ public void Serialization_NeverRequire_Roundtrips()
+ {
+ string json = JsonSerializer.Serialize(HostedMcpServerToolApprovalMode.NeverRequire, TestJsonSerializerContext.Default.HostedMcpServerToolApprovalMode);
+ Assert.Equal("""{"$type":"never"}""", json);
+
+ HostedMcpServerToolApprovalMode? result = JsonSerializer.Deserialize(json, TestJsonSerializerContext.Default.HostedMcpServerToolApprovalMode);
+ Assert.Equal(HostedMcpServerToolApprovalMode.NeverRequire, result);
+ }
+
+ [Fact]
+ public void Serialization_AlwaysRequire_Roundtrips()
+ {
+ string json = JsonSerializer.Serialize(HostedMcpServerToolApprovalMode.AlwaysRequire, TestJsonSerializerContext.Default.HostedMcpServerToolApprovalMode);
+ Assert.Equal("""{"$type":"always"}""", json);
+
+ HostedMcpServerToolApprovalMode? result = JsonSerializer.Deserialize(json, TestJsonSerializerContext.Default.HostedMcpServerToolApprovalMode);
+ Assert.Equal(HostedMcpServerToolApprovalMode.AlwaysRequire, result);
+ }
+
+ [Fact]
+ public void Serialization_RequireSpecific_Roundtrips()
+ {
+ var requireSpecific = HostedMcpServerToolApprovalMode.RequireSpecific(["ToolA", "ToolB"], ["ToolC"]);
+ string json = JsonSerializer.Serialize(requireSpecific, TestJsonSerializerContext.Default.HostedMcpServerToolApprovalMode);
+ Assert.Equal("""{"$type":"requireSpecific","alwaysRequireApprovalToolNames":["ToolA","ToolB"],"neverRequireApprovalToolNames":["ToolC"]}""", json);
+
+ HostedMcpServerToolApprovalMode? result = JsonSerializer.Deserialize(json, TestJsonSerializerContext.Default.HostedMcpServerToolApprovalMode);
+ Assert.Equal(requireSpecific, result);
+ }
+}
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs
index 609dac264eb..01de984d949 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs
@@ -35,4 +35,5 @@ namespace Microsoft.Extensions.AI;
[JsonSerializable(typeof(DayOfWeek[]))] // Used in Content tests
[JsonSerializable(typeof(Guid))] // Used in Content tests
[JsonSerializable(typeof(decimal))] // Used in Content tests
+[JsonSerializable(typeof(HostedMcpServerToolApprovalMode))]
internal sealed partial class TestJsonSerializerContext : JsonSerializerContext;
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/AIToolTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/AIToolTests.cs
similarity index 100%
rename from test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/AIToolTests.cs
rename to test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/AIToolTests.cs
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/HostedCodeInterpreterToolTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedCodeInterpreterToolTests.cs
similarity index 100%
rename from test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/HostedCodeInterpreterToolTests.cs
rename to test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedCodeInterpreterToolTests.cs
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/HostedFileSearchToolTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedFileSearchToolTests.cs
similarity index 100%
rename from test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/HostedFileSearchToolTests.cs
rename to test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedFileSearchToolTests.cs
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs
new file mode 100644
index 00000000000..c77e59e3307
--- /dev/null
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs
@@ -0,0 +1,76 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using Xunit;
+
+namespace Microsoft.Extensions.AI;
+
+public class HostedMcpServerToolTests
+{
+ [Fact]
+ public void Constructor_PropsDefault()
+ {
+ HostedMcpServerTool tool = new("serverName", "https://localhost/");
+
+ Assert.Empty(tool.AdditionalProperties);
+
+ Assert.Equal("serverName", tool.ServerName);
+ Assert.Equal("https://localhost/", tool.Url.ToString());
+
+ Assert.Empty(tool.Description);
+ Assert.Null(tool.AllowedTools);
+ Assert.Null(tool.ApprovalMode);
+ }
+
+ [Fact]
+ public void Constructor_Roundtrips()
+ {
+ HostedMcpServerTool tool = new("serverName", "https://localhost/");
+
+ Assert.Empty(tool.AdditionalProperties);
+ Assert.Empty(tool.Description);
+ Assert.Equal(nameof(HostedMcpServerTool), tool.Name);
+
+ Assert.Equal("serverName", tool.ServerName);
+ Assert.Equal("https://localhost/", tool.Url.ToString());
+ Assert.Empty(tool.Description);
+
+ Assert.Null(tool.ServerDescription);
+ string serverDescription = "This is a test server";
+ tool.ServerDescription = serverDescription;
+ Assert.Equal(serverDescription, tool.ServerDescription);
+
+ Assert.Null(tool.AllowedTools);
+ List allowedTools = ["tool1", "tool2"];
+ tool.AllowedTools = allowedTools;
+ Assert.Same(allowedTools, tool.AllowedTools);
+
+ Assert.Null(tool.ApprovalMode);
+ tool.ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire;
+ Assert.Same(HostedMcpServerToolApprovalMode.NeverRequire, tool.ApprovalMode);
+
+ tool.ApprovalMode = HostedMcpServerToolApprovalMode.AlwaysRequire;
+ Assert.Same(HostedMcpServerToolApprovalMode.AlwaysRequire, tool.ApprovalMode);
+
+ var customApprovalMode = new HostedMcpServerToolRequireSpecificApprovalMode(["tool1"], ["tool2"]);
+ tool.ApprovalMode = customApprovalMode;
+ Assert.Same(customApprovalMode, tool.ApprovalMode);
+
+ Assert.Null(tool.Headers);
+ Dictionary headers = [];
+ tool.Headers = headers;
+ Assert.Same(headers, tool.Headers);
+ }
+
+ [Fact]
+ public void Constructor_Throws()
+ {
+ Assert.Throws(() => new HostedMcpServerTool(string.Empty, new Uri("https://localhost/")));
+ Assert.Throws(() => new HostedMcpServerTool(null!, new Uri("https://localhost/")));
+ Assert.Throws(() => new HostedMcpServerTool("name", (Uri)null!));
+ Assert.Throws(() => new HostedMcpServerTool("name", (string)null!));
+ Assert.Throws(() => new HostedMcpServerTool("name", string.Empty));
+ }
+}
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/HostedWebSearchToolTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedWebSearchToolTests.cs
similarity index 100%
rename from test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/HostedWebSearchToolTests.cs
rename to test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedWebSearchToolTests.cs
diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs
index 007e86288f5..a46a06bb770 100644
--- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs
@@ -18,7 +18,7 @@ public class OpenAIResponseClientIntegrationTests : ChatClientIntegrationTests
public override bool FunctionInvokingChatClientSetsConversationId => true;
- // Test structure doesn't make sense with Respones.
+ // Test structure doesn't make sense with Responses.
public override Task Caching_AfterFunctionInvocation_FunctionOutputUnchangedAsync() => Task.CompletedTask;
[ConditionalFact]
@@ -49,4 +49,128 @@ public async Task UseWebSearch_AnnotationsReflectResults()
Assert.NotEmpty(ca.Title);
});
}
+
+ [ConditionalFact]
+ public async Task RemoteMCP_ListTools()
+ {
+ SkipIfNotEnabled();
+
+ ChatOptions chatOptions = new()
+ {
+ Tools = [new HostedMcpServerTool("deepwiki", "https://mcp.deepwiki.com/mcp") { ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire }],
+ };
+
+ ChatResponse response = await CreateChatClient()!.GetResponseAsync("Which tools are available on the wiki_tools MCP server?", chatOptions);
+
+ Assert.Contains("read_wiki_structure", response.Text);
+ Assert.Contains("read_wiki_contents", response.Text);
+ Assert.Contains("ask_question", response.Text);
+ }
+
+ [ConditionalFact]
+ public async Task RemoteMCP_CallTool_ApprovalNeverRequired()
+ {
+ SkipIfNotEnabled();
+
+ await RunAsync(false, false);
+ await RunAsync(true, true);
+
+ async Task RunAsync(bool streaming, bool requireSpecific)
+ {
+ ChatOptions chatOptions = new()
+ {
+ Tools = [new HostedMcpServerTool("deepwiki", "https://mcp.deepwiki.com/mcp")
+ {
+ ApprovalMode = requireSpecific ?
+ HostedMcpServerToolApprovalMode.RequireSpecific(null, ["read_wiki_structure", "ask_question"]) :
+ HostedMcpServerToolApprovalMode.NeverRequire,
+ }
+ ],
+ };
+
+ using var client = CreateChatClient()!;
+
+ const string Prompt = "Tell me the path to the README.md file for Microsoft.Extensions.AI.Abstractions in the dotnet/extensions repository";
+
+ ChatResponse response = streaming ?
+ await client.GetStreamingResponseAsync(Prompt, chatOptions).ToChatResponseAsync() :
+ await client.GetResponseAsync(Prompt, chatOptions);
+
+ 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.Contains("src/Libraries/Microsoft.Extensions.AI.Abstractions/README.md", response.Text);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task RemoteMCP_CallTool_ApprovalRequired()
+ {
+ SkipIfNotEnabled();
+
+ await RunAsync(false, false, false);
+ await RunAsync(true, true, false);
+ await RunAsync(false, false, true);
+ await RunAsync(true, true, true);
+
+ async Task RunAsync(bool streaming, bool requireSpecific, bool useConversationId)
+ {
+ ChatOptions chatOptions = new()
+ {
+ Tools = [new HostedMcpServerTool("deepwiki", "https://mcp.deepwiki.com/mcp")
+ {
+ ApprovalMode = requireSpecific ?
+ HostedMcpServerToolApprovalMode.RequireSpecific(["read_wiki_structure", "ask_question"], null) :
+ HostedMcpServerToolApprovalMode.AlwaysRequire,
+ }
+ ],
+ };
+
+ using var client = CreateChatClient()!;
+
+ // Initial request
+ List input = [new ChatMessage(ChatRole.User, "Tell me the path to the README.md file for Microsoft.Extensions.AI.Abstractions in the dotnet/extensions repository")];
+ ChatResponse response = streaming ?
+ await client.GetStreamingResponseAsync(input, chatOptions).ToChatResponseAsync() :
+ await client.GetResponseAsync(input, chatOptions);
+
+ // Handle approvals of up to two rounds of tool calls
+ int approvalsCount = 0;
+ for (int i = 0; i < 2; i++)
+ {
+ if (useConversationId)
+ {
+ chatOptions.ConversationId = response.ConversationId;
+ input.Clear();
+ }
+ else
+ {
+ input.AddRange(response.Messages);
+ }
+
+ var approvalResponse = new ChatMessage(ChatRole.Tool,
+ response.Messages
+ .SelectMany(m => m.Contents)
+ .OfType()
+ .Select(c => new McpServerToolApprovalResponseContent(c.ToolCall.CallId, true))
+ .ToArray());
+ if (approvalResponse.Contents.Count == 0)
+ {
+ break;
+ }
+
+ approvalsCount += approvalResponse.Contents.Count;
+ input.Add(approvalResponse);
+ response = streaming ?
+ await client.GetStreamingResponseAsync(input, chatOptions).ToChatResponseAsync() :
+ await client.GetResponseAsync(input, chatOptions);
+ }
+
+ // Validate final response
+ Assert.Equal(2, approvalsCount);
+ Assert.Contains("src/Libraries/Microsoft.Extensions.AI.Abstractions/README.md", response.Text);
+ }
+ }
}
diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs
index 014fae0d39f..9175b6afd57 100644
--- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs
@@ -8,6 +8,7 @@
using System.ComponentModel;
using System.Linq;
using System.Net.Http;
+using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Memory;
@@ -824,6 +825,682 @@ public async Task MultipleOutputItems_NonStreaming()
Assert.Equal(36, response.Usage.TotalTokenCount);
}
+ [Fact]
+ public async Task McpToolCall_ApprovalNotRequired_NonStreaming()
+ {
+ const string Input = """
+ {
+ "model": "gpt-4o-mini",
+ "tools": [
+ {
+ "type": "mcp",
+ "server_label": "deepwiki",
+ "server_url": "https://mcp.deepwiki.com/mcp",
+ "require_approval": "never"
+ }
+ ],
+ "tool_choice": "auto",
+ "input": [
+ {
+ "type": "message",
+ "role": "user",
+ "content": [
+ {
+ "type": "input_text",
+ "text": "Tell me the path to the README.md file for Microsoft.Extensions.AI.Abstractions in the dotnet/extensions repository"
+ }
+ ]
+ }
+ ]
+ }
+ """;
+
+ const string Output = """
+ {
+ "id": "resp_68be416397ec81918c48ef286530b8140384f747588fc3f5",
+ "object": "response",
+ "created_at": 1757299043,
+ "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_68be4163aa80819185e792abdcde71670384f747588fc3f5",
+ "type": "mcp_list_tools",
+ "server_label": "deepwiki",
+ "tools": [
+ {
+ "annotations": {
+ "read_only": false
+ },
+ "description": "Get a list of documentation topics for a GitHub repository",
+ "input_schema": {
+ "type": "object",
+ "properties": {
+ "repoName": {
+ "type": "string",
+ "description": "GitHub repository: owner/repo (e.g. \"facebook/react\")"
+ }
+ },
+ "required": [
+ "repoName"
+ ],
+ "additionalProperties": false,
+ "$schema": "http://json-schema.org/draft-07/schema#"
+ },
+ "name": "read_wiki_structure"
+ },
+ {
+ "annotations": {
+ "read_only": false
+ },
+ "description": "View documentation about a GitHub repository",
+ "input_schema": {
+ "type": "object",
+ "properties": {
+ "repoName": {
+ "type": "string",
+ "description": "GitHub repository: owner/repo (e.g. \"facebook/react\")"
+ }
+ },
+ "required": [
+ "repoName"
+ ],
+ "additionalProperties": false,
+ "$schema": "http://json-schema.org/draft-07/schema#"
+ },
+ "name": "read_wiki_contents"
+ },
+ {
+ "annotations": {
+ "read_only": false
+ },
+ "description": "Ask any question about a GitHub repository",
+ "input_schema": {
+ "type": "object",
+ "properties": {
+ "repoName": {
+ "type": "string",
+ "description": "GitHub repository: owner/repo (e.g. \"facebook/react\")"
+ },
+ "question": {
+ "type": "string",
+ "description": "The question to ask about the repository"
+ }
+ },
+ "required": [
+ "repoName",
+ "question"
+ ],
+ "additionalProperties": false,
+ "$schema": "http://json-schema.org/draft-07/schema#"
+ },
+ "name": "ask_question"
+ }
+ ]
+ },
+ {
+ "id": "mcp_68be4166acfc8191bc5e0a751eed358b0384f747588fc3f5",
+ "type": "mcp_call",
+ "approval_request_id": null,
+ "arguments": "{\"repoName\":\"dotnet/extensions\"}",
+ "error": null,
+ "name": "read_wiki_structure",
+ "output": "Available pages for dotnet/extensions:\n\n- 1 Overview\n- 2 Build System and CI/CD\n- 3 AI Extensions Framework\n - 3.1 Core Abstractions\n - 3.2 AI Function System\n - 3.3 Chat Completion\n - 3.4 Caching System\n - 3.5 Evaluation and Reporting\n- 4 HTTP Resilience and Diagnostics\n - 4.1 Standard Resilience\n - 4.2 Hedging Strategies\n- 5 Telemetry and Compliance\n- 6 Testing Infrastructure\n - 6.1 AI Service Integration Testing\n - 6.2 Time Provider Testing",
+ "server_label": "deepwiki"
+ },
+ {
+ "id": "mcp_68be416900f88191837ae0718339a4ce0384f747588fc3f5",
+ "type": "mcp_call",
+ "approval_request_id": null,
+ "arguments": "{\"repoName\":\"dotnet/extensions\",\"question\":\"What is the path to the README.md file for Microsoft.Extensions.AI.Abstractions?\"}",
+ "error": null,
+ "name": "ask_question",
+ "output": "The `README.md` file for `Microsoft.Extensions.AI.Abstractions` is located at `src/Libraries/Microsoft.Extensions.AI.Abstractions/README.md` within the `dotnet/extensions` repository. This file provides an overview of the `Microsoft.Extensions.AI.Abstractions` package, including installation instructions and usage examples for its core interfaces like `IChatClient` and `IEmbeddingGenerator`. \n\n## Path to README.md\n\nThe specific path to the `README.md` file for the `Microsoft.Extensions.AI.Abstractions` project is `src/Libraries/Microsoft.Extensions.AI.Abstractions/README.md`. This path is also referenced in the `AI Extensions Framework` wiki page as a relevant source file. \n\n## Notes\n\nThe `Packaging.targets` file in the `eng/MSBuild` directory indicates that `README.md` files are included in packages when `IsPackable` and `IsShipping` properties are true. This suggests that the `README.md` file located at `src/Libraries/Microsoft.Extensions.AI.Abstractions/README.md` is intended to be part of the distributed NuGet package for `Microsoft.Extensions.AI.Abstractions`. \n\nWiki pages you might want to explore:\n- [AI Extensions Framework (dotnet/extensions)](/wiki/dotnet/extensions#3)\n- [Chat Completion (dotnet/extensions)](/wiki/dotnet/extensions#3.3)\n\nView this search on DeepWiki: https://deepwiki.com/search/what-is-the-path-to-the-readme_315595bd-9b39-4f04-9fa3-42dc778fa9f3\n",
+ "server_label": "deepwiki"
+ },
+ {
+ "id": "msg_68be416fb43c819194a1d4ace2643a7e0384f747588fc3f5",
+ "type": "message",
+ "status": "completed",
+ "content": [
+ {
+ "type": "output_text",
+ "annotations": [],
+ "logprobs": [],
+ "text": "The `README.md` file for `Microsoft.Extensions.AI.Abstractions` is located at:\n\n```\nsrc/Libraries/Microsoft.Extensions.AI.Abstractions/README.md\n```\n\nThis file includes an overview, installation instructions, and usage examples related to the package."
+ }
+ ],
+ "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": "deepwiki",
+ "server_url": "https://mcp.deepwiki.com/"
+ }
+ ],
+ "top_logprobs": 0,
+ "top_p": 1,
+ "truncation": "disabled",
+ "usage": {
+ "input_tokens": 1329,
+ "input_tokens_details": {
+ "cached_tokens": 0
+ },
+ "output_tokens": 123,
+ "output_tokens_details": {
+ "reasoning_tokens": 0
+ },
+ "total_tokens": 1452
+ },
+ "user": null,
+ "metadata": {}
+ }
+ """;
+
+ using VerbatimHttpHandler handler = new(Input, Output);
+ using HttpClient httpClient = new(handler);
+ using IChatClient client = CreateResponseClient(httpClient, "gpt-4o-mini");
+
+ ChatOptions chatOptions = new()
+ {
+ Tools = [new HostedMcpServerTool("deepwiki", "https://mcp.deepwiki.com/mcp")
+ {
+ ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire,
+ }
+ ],
+ };
+
+ var response = await client.GetResponseAsync("Tell me the path to the README.md file for Microsoft.Extensions.AI.Abstractions in the dotnet/extensions repository", chatOptions);
+ Assert.NotNull(response);
+
+ Assert.Equal("resp_68be416397ec81918c48ef286530b8140384f747588fc3f5", response.ResponseId);
+ Assert.Equal("resp_68be416397ec81918c48ef286530b8140384f747588fc3f5", response.ConversationId);
+ Assert.Equal("gpt-4o-mini-2024-07-18", response.ModelId);
+ Assert.Equal(DateTimeOffset.FromUnixTimeSeconds(1_757_299_043), response.CreatedAt);
+ Assert.Null(response.FinishReason);
+
+ var message = Assert.Single(response.Messages);
+ Assert.Equal(ChatRole.Assistant, response.Messages[0].Role);
+ Assert.Equal("The `README.md` file for `Microsoft.Extensions.AI.Abstractions` is located at:\n\n```\nsrc/Libraries/Microsoft.Extensions.AI.Abstractions/README.md\n```\n\nThis file includes an overview, installation instructions, and usage examples related to the package.", response.Messages[0].Text);
+
+ Assert.Equal(6, message.Contents.Count);
+
+ 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.NotNull(firstCall.Arguments);
+ Assert.Single(firstCall.Arguments);
+ Assert.Equal("dotnet/extensions", ((JsonElement)firstCall.Arguments["repoName"]!).GetString());
+
+ var firstResult = Assert.IsType(message.Contents[2]);
+ Assert.Equal("mcp_68be4166acfc8191bc5e0a751eed358b0384f747588fc3f5", firstResult.CallId);
+ Assert.NotNull(firstResult.Output);
+ Assert.StartsWith("Available pages for dotnet/extensions", Assert.IsType(Assert.Single(firstResult.Output)).Text);
+
+ 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.NotNull(secondCall.Arguments);
+ Assert.Equal("dotnet/extensions", ((JsonElement)secondCall.Arguments["repoName"]!).GetString());
+ Assert.Equal("What is the path to the README.md file for Microsoft.Extensions.AI.Abstractions?", ((JsonElement)secondCall.Arguments["question"]!).GetString());
+
+ var secondResult = Assert.IsType(message.Contents[4]);
+ Assert.Equal("mcp_68be416900f88191837ae0718339a4ce0384f747588fc3f5", secondResult.CallId);
+ Assert.NotNull(secondResult.Output);
+ Assert.StartsWith("The `README.md` file for `Microsoft.Extensions.AI.Abstractions` is located at", Assert.IsType(Assert.Single(secondResult.Output)).Text);
+
+ Assert.NotNull(response.Usage);
+ Assert.Equal(1329, response.Usage.InputTokenCount);
+ Assert.Equal(123, response.Usage.OutputTokenCount);
+ Assert.Equal(1452, response.Usage.TotalTokenCount);
+ }
+
+ [Fact]
+ public async Task McpToolCall_ApprovalNotRequired_Streaming()
+ {
+ const string Input = """
+ {
+ "model": "gpt-4o-mini",
+ "tools": [
+ {
+ "type": "mcp",
+ "server_label": "deepwiki",
+ "server_url": "https://mcp.deepwiki.com/mcp",
+ "require_approval": "never"
+ }
+ ],
+ "tool_choice": "auto",
+ "input": [
+ {
+ "type": "message",
+ "role": "user",
+ "content": [
+ {
+ "type": "input_text",
+ "text": "Tell me the path to the README.md file for Microsoft.Extensions.AI.Abstractions in the dotnet/extensions repository"
+ }
+ ]
+ }
+ ],
+ "stream": true
+ }
+ """;
+
+ const string Output = """
+ event: response.created
+ data: {"type":"response.created","sequence_number":0,"response":{"id":"resp_68be44fd7298819e82fd82c8516e970d03a2537be0e84a54","object":"response","created_at":1757299965,"status":"in_progress","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":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"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":"deepwiki","server_url":"https://mcp.deepwiki.com/"}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}
+
+ event: response.in_progress
+ data: {"type":"response.in_progress","sequence_number":1,"response":{"id":"resp_68be44fd7298819e82fd82c8516e970d03a2537be0e84a54","object":"response","created_at":1757299965,"status":"in_progress","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":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"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":"deepwiki","server_url":"https://mcp.deepwiki.com/"}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}
+
+ event: response.output_item.added
+ data: {"type":"response.output_item.added","sequence_number":2,"output_index":0,"item":{"id":"mcpl_68be44fd8f68819eba7a74a2f6d27a5a03a2537be0e84a54","type":"mcp_list_tools","server_label":"deepwiki","tools":[]}}
+
+ event: response.mcp_list_tools.in_progress
+ data: {"type":"response.mcp_list_tools.in_progress","sequence_number":3,"output_index":0,"item_id":"mcpl_68be44fd8f68819eba7a74a2f6d27a5a03a2537be0e84a54"}
+
+ event: response.mcp_list_tools.completed
+ data: {"type":"response.mcp_list_tools.completed","sequence_number":4,"output_index":0,"item_id":"mcpl_68be44fd8f68819eba7a74a2f6d27a5a03a2537be0e84a54"}
+
+ event: response.output_item.done
+ data: {"type":"response.output_item.done","sequence_number":5,"output_index":0,"item":{"id":"mcpl_68be44fd8f68819eba7a74a2f6d27a5a03a2537be0e84a54","type":"mcp_list_tools","server_label":"deepwiki","tools":[{"annotations":{"read_only":false},"description":"Get a list of documentation topics for a GitHub repository","input_schema":{"type":"object","properties":{"repoName":{"type":"string","description":"GitHub repository: owner/repo (e.g. \"facebook/react\")"}},"required":["repoName"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"},"name":"read_wiki_structure"},{"annotations":{"read_only":false},"description":"View documentation about a GitHub repository","input_schema":{"type":"object","properties":{"repoName":{"type":"string","description":"GitHub repository: owner/repo (e.g. \"facebook/react\")"}},"required":["repoName"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"},"name":"read_wiki_contents"},{"annotations":{"read_only":false},"description":"Ask any question about a GitHub repository","input_schema":{"type":"object","properties":{"repoName":{"type":"string","description":"GitHub repository: owner/repo (e.g. \"facebook/react\")"},"question":{"type":"string","description":"The question to ask about the repository"}},"required":["repoName","question"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"},"name":"ask_question"}]}}
+
+ event: response.output_item.added
+ data: {"type":"response.output_item.added","sequence_number":6,"output_index":1,"item":{"id":"mcp_68be4503d45c819e89cb574361c8eba003a2537be0e84a54","type":"mcp_call","approval_request_id":null,"arguments":"","error":null,"name":"read_wiki_structure","output":null,"server_label":"deepwiki"}}
+
+ event: response.mcp_call.in_progress
+ data: {"type":"response.mcp_call.in_progress","sequence_number":7,"output_index":1,"item_id":"mcp_68be4503d45c819e89cb574361c8eba003a2537be0e84a54"}
+
+ event: response.mcp_call_arguments.delta
+ data: {"type":"response.mcp_call_arguments.delta","sequence_number":8,"output_index":1,"item_id":"mcp_68be4503d45c819e89cb574361c8eba003a2537be0e84a54","delta":"{\"repoName\":\"dotnet/extensions\"}","obfuscation":""}
+
+ event: response.mcp_call_arguments.done
+ data: {"type":"response.mcp_call_arguments.done","sequence_number":9,"output_index":1,"item_id":"mcp_68be4503d45c819e89cb574361c8eba003a2537be0e84a54","arguments":"{\"repoName\":\"dotnet/extensions\"}"}
+
+ event: response.mcp_call.completed
+ data: {"type":"response.mcp_call.completed","sequence_number":10,"output_index":1,"item_id":"mcp_68be4503d45c819e89cb574361c8eba003a2537be0e84a54"}
+
+ event: response.output_item.done
+ data: {"type":"response.output_item.done","sequence_number":11,"output_index":1,"item":{"id":"mcp_68be4503d45c819e89cb574361c8eba003a2537be0e84a54","type":"mcp_call","approval_request_id":null,"arguments":"{\"repoName\":\"dotnet/extensions\"}","error":null,"name":"read_wiki_structure","output":"Available pages for dotnet/extensions:\n\n- 1 Overview\n- 2 Build System and CI/CD\n- 3 AI Extensions Framework\n - 3.1 Core Abstractions\n - 3.2 AI Function System\n - 3.3 Chat Completion\n - 3.4 Caching System\n - 3.5 Evaluation and Reporting\n- 4 HTTP Resilience and Diagnostics\n - 4.1 Standard Resilience\n - 4.2 Hedging Strategies\n- 5 Telemetry and Compliance\n- 6 Testing Infrastructure\n - 6.1 AI Service Integration Testing\n - 6.2 Time Provider Testing","server_label":"deepwiki"}}
+
+ event: response.output_item.added
+ data: {"type":"response.output_item.added","sequence_number":12,"output_index":2,"item":{"id":"mcp_68be4505f134819e806c002f27cce0c303a2537be0e84a54","type":"mcp_call","approval_request_id":null,"arguments":"","error":null,"name":"ask_question","output":null,"server_label":"deepwiki"}}
+
+ event: response.mcp_call.in_progress
+ data: {"type":"response.mcp_call.in_progress","sequence_number":13,"output_index":2,"item_id":"mcp_68be4505f134819e806c002f27cce0c303a2537be0e84a54"}
+
+ event: response.mcp_call_arguments.delta
+ data: {"type":"response.mcp_call_arguments.delta","sequence_number":14,"output_index":2,"item_id":"mcp_68be4505f134819e806c002f27cce0c303a2537be0e84a54","delta":"{\"repoName\":\"dotnet/extensions\",\"question\":\"What is the path to the README.md file for Microsoft.Extensions.AI.Abstractions?\"}","obfuscation":"IT"}
+
+ event: response.mcp_call_arguments.done
+ data: {"type":"response.mcp_call_arguments.done","sequence_number":15,"output_index":2,"item_id":"mcp_68be4505f134819e806c002f27cce0c303a2537be0e84a54","arguments":"{\"repoName\":\"dotnet/extensions\",\"question\":\"What is the path to the README.md file for Microsoft.Extensions.AI.Abstractions?\"}"}
+
+ event: response.mcp_call.completed
+ data: {"type":"response.mcp_call.completed","sequence_number":16,"output_index":2,"item_id":"mcp_68be4505f134819e806c002f27cce0c303a2537be0e84a54"}
+
+ event: response.output_item.done
+ data: {"type":"response.output_item.done","sequence_number":17,"output_index":2,"item":{"id":"mcp_68be4505f134819e806c002f27cce0c303a2537be0e84a54","type":"mcp_call","approval_request_id":null,"arguments":"{\"repoName\":\"dotnet/extensions\",\"question\":\"What is the path to the README.md file for Microsoft.Extensions.AI.Abstractions?\"}","error":null,"name":"ask_question","output":"The path to the `README.md` file for `Microsoft.Extensions.AI.Abstractions` is `src/Libraries/Microsoft.Extensions.AI.Abstractions/README.md` . This file provides an overview of the `Microsoft.Extensions.AI.Abstractions` library, including installation instructions and usage examples for its core components like `IChatClient` and `IEmbeddingGenerator` .\n\n## README.md Content Overview\nThe `README.md` file for `Microsoft.Extensions.AI.Abstractions` details the purpose of the library, which is to provide abstractions for generative AI components . It includes instructions on how to install the NuGet package `Microsoft.Extensions.AI.Abstractions` .\n\nThe document also provides usage examples for the `IChatClient` interface, which defines methods for interacting with AI services that offer \"chat\" capabilities . This includes examples for requesting both complete and streaming chat responses .\n\nFurthermore, the `README.md` explains the `IEmbeddingGenerator` interface, which is used for generating vector embeddings from input values . It demonstrates how to use `GenerateAsync` to create embeddings . The file also discusses how both `IChatClient` and `IEmbeddingGenerator` implementations can be layered to create pipelines of functionality, incorporating features like caching and telemetry .\n\nNotes:\nThe user's query specifically asked for the path to the `README.md` file for `Microsoft.Extensions.AI.Abstractions`. The provided codebase context, particularly the wiki page for \"AI Extensions Framework\", directly lists this file as a relevant source file . The content of the `README.md` file itself further confirms its relevance to the `Microsoft.Extensions.AI.Abstractions` library.\n\nWiki pages you might want to explore:\n- [AI Extensions Framework (dotnet/extensions)](/wiki/dotnet/extensions#3)\n- [Chat Completion (dotnet/extensions)](/wiki/dotnet/extensions#3.3)\n\nView this search on DeepWiki: https://deepwiki.com/search/what-is-the-path-to-the-readme_bb6bee43-3136-4b21-bc5d-02ca1611d857\n","server_label":"deepwiki"}}
+
+ event: response.output_item.added
+ data: {"type":"response.output_item.added","sequence_number":18,"output_index":3,"item":{"id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","type":"message","status":"in_progress","content":[],"role":"assistant"}}
+
+ event: response.content_part.added
+ data: {"type":"response.content_part.added","sequence_number":19,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":20,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"The","logprobs":[],"obfuscation":"a5sNdjeWpJXIK"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":21,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" path","logprobs":[],"obfuscation":"2oWbALsHrtv"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":22,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" to","logprobs":[],"obfuscation":"K8lRBCaiusvjP"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":23,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"LP7Xp4jDWA5w"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":24,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" `","logprobs":[],"obfuscation":"2rUNEj0h3wLlee"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":25,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"README","logprobs":[],"obfuscation":"PSbOrCj8y6"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":26,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":".md","logprobs":[],"obfuscation":"Do0BCY4kJ6wQW"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":27,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"`","logprobs":[],"obfuscation":"3fTPkjHu1Oq83DT"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":28,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" file","logprobs":[],"obfuscation":"CI9PXx3sH06"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":29,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" for","logprobs":[],"obfuscation":"fJuaoSPsMge8"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":30,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" `","logprobs":[],"obfuscation":"O1h4Q0T72OM4e7"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":31,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"Microsoft","logprobs":[],"obfuscation":"E2YPgfE"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":32,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":".Extensions","logprobs":[],"obfuscation":"vfVX8"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":33,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":".A","logprobs":[],"obfuscation":"EwDmSMHqymBRl1"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":34,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"I","logprobs":[],"obfuscation":"QQfjze1z7QhvcJE"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":35,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":".A","logprobs":[],"obfuscation":"7fLbFXKbxOMkBi"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":36,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"bst","logprobs":[],"obfuscation":"3p1svK7Jd1N7C"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":37,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"ractions","logprobs":[],"obfuscation":"Cl2xCwTC"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":38,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"`","logprobs":[],"obfuscation":"ObDOKE72QOlXSx9"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":39,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" in","logprobs":[],"obfuscation":"FJwPbDYgh4XjL"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":40,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"e8cV5qt7hEsz"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":41,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" `","logprobs":[],"obfuscation":"Hf8ZQDFLfImh3e"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":42,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"dot","logprobs":[],"obfuscation":"0lh2vLiYye2JI"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":43,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"net","logprobs":[],"obfuscation":"g5fzb2qtk4Piz"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":44,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"/extensions","logprobs":[],"obfuscation":"egpos"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":45,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"`","logprobs":[],"obfuscation":"gXw3bKveEVIKXux"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":46,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" repository","logprobs":[],"obfuscation":"rqhlC"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":47,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" is","logprobs":[],"obfuscation":"YZq9zsRja0g2M"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":48,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":":\n\n","logprobs":[],"obfuscation":"mhDAmaHJUvLGl"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":49,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"``","logprobs":[],"obfuscation":"3XmO5YTsWjzHHf"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":50,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"`\n","logprobs":[],"obfuscation":"4fmXZmdkPxNn8K"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":51,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"src","logprobs":[],"obfuscation":"ifGf4yLEg5pMZ"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":52,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"/L","logprobs":[],"obfuscation":"C1k1toBElpgxyW"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":53,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"ibraries","logprobs":[],"obfuscation":"fdOTYTyp"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":54,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"/M","logprobs":[],"obfuscation":"DyscJIQYaPJugC"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":55,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"icrosoft","logprobs":[],"obfuscation":"PQxU7muP"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":56,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":".Extensions","logprobs":[],"obfuscation":"RCJB8"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":57,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":".A","logprobs":[],"obfuscation":"i92CWxnAkwS4C9"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":58,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"I","logprobs":[],"obfuscation":"qfH8wVJN74vCfBM"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":59,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":".A","logprobs":[],"obfuscation":"LcuBP89lZVCCH9"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":60,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"bst","logprobs":[],"obfuscation":"I8rKDbKN0zylv"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":61,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"ractions","logprobs":[],"obfuscation":"tOgiCPs5"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":62,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"/","logprobs":[],"obfuscation":"jgJjLruTbFJGDhU"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":63,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"README","logprobs":[],"obfuscation":"D5VSEFNde7"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":64,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":".md","logprobs":[],"obfuscation":"7ZGJO5sZOTPBs"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":65,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"\n","logprobs":[],"obfuscation":"7Sv80haKTTwfEWj"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":66,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"``","logprobs":[],"obfuscation":"m1JSvZ8rrpJnH5"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":67,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"`\n\n","logprobs":[],"obfuscation":"U93PMKtCB5Pb5"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":68,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"This","logprobs":[],"obfuscation":"f5veTGedo9nM"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":69,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" file","logprobs":[],"obfuscation":"oEBwvP5FnPK"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":70,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" provides","logprobs":[],"obfuscation":"IVNCYwr"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":71,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" an","logprobs":[],"obfuscation":"3x6WquURIJ3ld"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":72,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" overview","logprobs":[],"obfuscation":"VR9yeiD"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":73,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"z46dC1o2FC8Rs"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":74,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"YfZGabvmgyoI"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":75,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" library","logprobs":[],"obfuscation":"TamElgEp"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":76,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":",","logprobs":[],"obfuscation":"VfVfqbnHAfsJyJn"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":77,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" installation","logprobs":[],"obfuscation":"CGR"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":78,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" instructions","logprobs":[],"obfuscation":"xst"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":79,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":",","logprobs":[],"obfuscation":"3u5wqRA2RXh2QP8"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":80,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"tD4WZmOhepzQ"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":81,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" usage","logprobs":[],"obfuscation":"SadOK826mZ"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":82,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" examples","logprobs":[],"obfuscation":"5VpLKav"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":83,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" for","logprobs":[],"obfuscation":"xPvtjDSUic9E"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":84,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" its","logprobs":[],"obfuscation":"6duK61DX14vx"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":85,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" core","logprobs":[],"obfuscation":"Cz8trPLsCWu"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":86,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" components","logprobs":[],"obfuscation":"Gexuy"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":87,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":".","logprobs":[],"obfuscation":"HVeWkHoX1cc6hVh"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":88,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" If","logprobs":[],"obfuscation":"G1TOxxwvSEq4L"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":89,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" you","logprobs":[],"obfuscation":"xQlKeOixd1hv"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":90,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" have","logprobs":[],"obfuscation":"bX6P0qgFPnR"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":91,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" any","logprobs":[],"obfuscation":"KxH8EiMzXa1N"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":92,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" more","logprobs":[],"obfuscation":"kA0kxRPPqru"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":93,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" questions","logprobs":[],"obfuscation":"9HRCyD"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":94,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" about","logprobs":[],"obfuscation":"yYFZhtsSfc"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":95,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" it","logprobs":[],"obfuscation":"zpyEAwPWl8Ozh"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":96,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":",","logprobs":[],"obfuscation":"ivjn00lbmzDHiFU"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":97,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" feel","logprobs":[],"obfuscation":"O2edXDmkBqt"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":98,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" free","logprobs":[],"obfuscation":"MlpWh7p0P1F"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":99,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" to","logprobs":[],"obfuscation":"uMNfozGkKe6xW"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":100,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" ask","logprobs":[],"obfuscation":"6rMOxwXhR8RY"}
+
+ event: response.output_text.delta
+ data: {"type":"response.output_text.delta","sequence_number":101,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"!","logprobs":[],"obfuscation":"QPZMdhS0e5vYuRl"}
+
+ event: response.output_text.done
+ data: {"type":"response.output_text.done","sequence_number":102,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"text":"The path to the `README.md` file for `Microsoft.Extensions.AI.Abstractions` in the `dotnet/extensions` repository is:\n\n```\nsrc/Libraries/Microsoft.Extensions.AI.Abstractions/README.md\n```\n\nThis file provides an overview of the library, installation instructions, and usage examples for its core components. If you have any more questions about it, feel free to ask!","logprobs":[]}
+
+ event: response.content_part.done
+ data: {"type":"response.content_part.done","sequence_number":103,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"The path to the `README.md` file for `Microsoft.Extensions.AI.Abstractions` in the `dotnet/extensions` repository is:\n\n```\nsrc/Libraries/Microsoft.Extensions.AI.Abstractions/README.md\n```\n\nThis file provides an overview of the library, installation instructions, and usage examples for its core components. If you have any more questions about it, feel free to ask!"}}
+
+ event: response.output_item.done
+ data: {"type":"response.output_item.done","sequence_number":104,"output_index":3,"item":{"id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"The path to the `README.md` file for `Microsoft.Extensions.AI.Abstractions` in the `dotnet/extensions` repository is:\n\n```\nsrc/Libraries/Microsoft.Extensions.AI.Abstractions/README.md\n```\n\nThis file provides an overview of the library, installation instructions, and usage examples for its core components. If you have any more questions about it, feel free to ask!"}],"role":"assistant"}}
+
+ event: response.completed
+ data: {"type":"response.completed","sequence_number":105,"response":{"id":"resp_68be44fd7298819e82fd82c8516e970d03a2537be0e84a54","object":"response","created_at":1757299965,"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_68be44fd8f68819eba7a74a2f6d27a5a03a2537be0e84a54","type":"mcp_list_tools","server_label":"deepwiki","tools":[{"annotations":{"read_only":false},"description":"Get a list of documentation topics for a GitHub repository","input_schema":{"type":"object","properties":{"repoName":{"type":"string","description":"GitHub repository: owner/repo (e.g. \"facebook/react\")"}},"required":["repoName"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"},"name":"read_wiki_structure"},{"annotations":{"read_only":false},"description":"View documentation about a GitHub repository","input_schema":{"type":"object","properties":{"repoName":{"type":"string","description":"GitHub repository: owner/repo (e.g. \"facebook/react\")"}},"required":["repoName"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"},"name":"read_wiki_contents"},{"annotations":{"read_only":false},"description":"Ask any question about a GitHub repository","input_schema":{"type":"object","properties":{"repoName":{"type":"string","description":"GitHub repository: owner/repo (e.g. \"facebook/react\")"},"question":{"type":"string","description":"The question to ask about the repository"}},"required":["repoName","question"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"},"name":"ask_question"}]},{"id":"mcp_68be4503d45c819e89cb574361c8eba003a2537be0e84a54","type":"mcp_call","approval_request_id":null,"arguments":"{\"repoName\":\"dotnet/extensions\"}","error":null,"name":"read_wiki_structure","output":"Available pages for dotnet/extensions:\n\n- 1 Overview\n- 2 Build System and CI/CD\n- 3 AI Extensions Framework\n - 3.1 Core Abstractions\n - 3.2 AI Function System\n - 3.3 Chat Completion\n - 3.4 Caching System\n - 3.5 Evaluation and Reporting\n- 4 HTTP Resilience and Diagnostics\n - 4.1 Standard Resilience\n - 4.2 Hedging Strategies\n- 5 Telemetry and Compliance\n- 6 Testing Infrastructure\n - 6.1 AI Service Integration Testing\n - 6.2 Time Provider Testing","server_label":"deepwiki"},{"id":"mcp_68be4505f134819e806c002f27cce0c303a2537be0e84a54","type":"mcp_call","approval_request_id":null,"arguments":"{\"repoName\":\"dotnet/extensions\",\"question\":\"What is the path to the README.md file for Microsoft.Extensions.AI.Abstractions?\"}","error":null,"name":"ask_question","output":"The path to the `README.md` file for `Microsoft.Extensions.AI.Abstractions` is `src/Libraries/Microsoft.Extensions.AI.Abstractions/README.md` . This file provides an overview of the `Microsoft.Extensions.AI.Abstractions` library, including installation instructions and usage examples for its core components like `IChatClient` and `IEmbeddingGenerator` .\n\n## README.md Content Overview\nThe `README.md` file for `Microsoft.Extensions.AI.Abstractions` details the purpose of the library, which is to provide abstractions for generative AI components . It includes instructions on how to install the NuGet package `Microsoft.Extensions.AI.Abstractions` .\n\nThe document also provides usage examples for the `IChatClient` interface, which defines methods for interacting with AI services that offer \"chat\" capabilities . This includes examples for requesting both complete and streaming chat responses .\n\nFurthermore, the `README.md` explains the `IEmbeddingGenerator` interface, which is used for generating vector embeddings from input values . It demonstrates how to use `GenerateAsync` to create embeddings . The file also discusses how both `IChatClient` and `IEmbeddingGenerator` implementations can be layered to create pipelines of functionality, incorporating features like caching and telemetry .\n\nNotes:\nThe user's query specifically asked for the path to the `README.md` file for `Microsoft.Extensions.AI.Abstractions`. The provided codebase context, particularly the wiki page for \"AI Extensions Framework\", directly lists this file as a relevant source file . The content of the `README.md` file itself further confirms its relevance to the `Microsoft.Extensions.AI.Abstractions` library.\n\nWiki pages you might want to explore:\n- [AI Extensions Framework (dotnet/extensions)](/wiki/dotnet/extensions#3)\n- [Chat Completion (dotnet/extensions)](/wiki/dotnet/extensions#3.3)\n\nView this search on DeepWiki: https://deepwiki.com/search/what-is-the-path-to-the-readme_bb6bee43-3136-4b21-bc5d-02ca1611d857\n","server_label":"deepwiki"},{"id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"The path to the `README.md` file for `Microsoft.Extensions.AI.Abstractions` in the `dotnet/extensions` repository is:\n\n```\nsrc/Libraries/Microsoft.Extensions.AI.Abstractions/README.md\n```\n\nThis file provides an overview of the library, installation instructions, and usage examples for its core components. If you have any more questions about it, feel free to ask!"}],"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.0,"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":"deepwiki","server_url":"https://mcp.deepwiki.com/"}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":1420,"input_tokens_details":{"cached_tokens":0},"output_tokens":149,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":1569},"user":null,"metadata":{}}}
+
+
+ """;
+
+ using VerbatimHttpHandler handler = new(Input, Output);
+ using HttpClient httpClient = new(handler);
+ using IChatClient client = CreateResponseClient(httpClient, "gpt-4o-mini");
+
+ ChatOptions chatOptions = new()
+ {
+ Tools = [new HostedMcpServerTool("deepwiki", "https://mcp.deepwiki.com/mcp")
+ {
+ ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire,
+ }
+ ],
+ };
+
+ var response = await client.GetStreamingResponseAsync("Tell me the path to the README.md file for Microsoft.Extensions.AI.Abstractions in the dotnet/extensions repository", chatOptions)
+ .ToChatResponseAsync();
+ Assert.NotNull(response);
+
+ Assert.Equal("resp_68be44fd7298819e82fd82c8516e970d03a2537be0e84a54", response.ResponseId);
+ Assert.Equal("resp_68be44fd7298819e82fd82c8516e970d03a2537be0e84a54", response.ConversationId);
+ Assert.Equal("gpt-4o-mini-2024-07-18", response.ModelId);
+ Assert.Equal(DateTimeOffset.FromUnixTimeSeconds(1_757_299_965), response.CreatedAt);
+ Assert.Equal(ChatFinishReason.Stop, response.FinishReason);
+
+ var message = Assert.Single(response.Messages);
+ Assert.Equal(ChatRole.Assistant, response.Messages[0].Role);
+ Assert.StartsWith("The path to the `README.md` file", response.Messages[0].Text);
+
+ Assert.Equal(6, message.Contents.Count);
+
+ 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.NotNull(firstCall.Arguments);
+ Assert.Single(firstCall.Arguments);
+ Assert.Equal("dotnet/extensions", ((JsonElement)firstCall.Arguments["repoName"]!).GetString());
+
+ var firstResult = Assert.IsType(message.Contents[2]);
+ Assert.Equal("mcp_68be4503d45c819e89cb574361c8eba003a2537be0e84a54", firstResult.CallId);
+ Assert.NotNull(firstResult.Output);
+ Assert.StartsWith("Available pages for dotnet/extensions", Assert.IsType(Assert.Single(firstResult.Output)).Text);
+
+ 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.NotNull(secondCall.Arguments);
+ Assert.Equal("dotnet/extensions", ((JsonElement)secondCall.Arguments["repoName"]!).GetString());
+ Assert.Equal("What is the path to the README.md file for Microsoft.Extensions.AI.Abstractions?", ((JsonElement)secondCall.Arguments["question"]!).GetString());
+
+ var secondResult = Assert.IsType(message.Contents[4]);
+ Assert.Equal("mcp_68be4505f134819e806c002f27cce0c303a2537be0e84a54", secondResult.CallId);
+ Assert.NotNull(secondResult.Output);
+ Assert.StartsWith("The path to the `README.md` file", Assert.IsType(Assert.Single(secondResult.Output)).Text);
+
+ Assert.NotNull(response.Usage);
+ Assert.Equal(1420, response.Usage.InputTokenCount);
+ Assert.Equal(149, response.Usage.OutputTokenCount);
+ Assert.Equal(1569, response.Usage.TotalTokenCount);
+ }
+
private static IChatClient CreateResponseClient(HttpClient httpClient, string modelId) =>
new OpenAIClient(
new ApiKeyCredential("apikey"),