diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs
index ec7f993a25f..3283c09a7ee 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs
@@ -23,14 +23,14 @@ public sealed class McpServerToolCallContent : AIContent
///
/// The tool call ID.
/// The tool name.
- /// The MCP server name.
- /// , , or is .
- /// , , or are empty or composed entirely of whitespace.
- public McpServerToolCallContent(string callId, string toolName, string serverName)
+ /// The MCP server name that hosts the tool.
+ /// or is .
+ /// or is empty or composed entirely of whitespace.
+ public McpServerToolCallContent(string callId, string toolName, string? serverName)
{
CallId = Throw.IfNullOrWhitespace(callId);
ToolName = Throw.IfNullOrWhitespace(toolName);
- ServerName = Throw.IfNullOrWhitespace(serverName);
+ ServerName = serverName;
}
///
@@ -44,9 +44,9 @@ public McpServerToolCallContent(string callId, string toolName, string serverNam
public string ToolName { get; }
///
- /// Gets the name of the MCP server.
+ /// Gets the name of the MCP server that hosts the tool.
///
- public string ServerName { get; }
+ public string? ServerName { get; }
///
/// Gets or sets the arguments used for the tool call.
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs
index d55ffb3788c..7bf7c5ae731 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs
@@ -18,25 +18,13 @@ 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 is .
- /// 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 is .
- /// is empty or composed entirely of whitespace.
- public HostedMcpServerTool(string serverName, Uri url)
+ /// The address of the remote MCP server. This may be a URL, or in the case of a service providing built-in MCP servers with known names, it can be such a name.
+ /// or is .
+ /// or is empty or composed entirely of whitespace.
+ public HostedMcpServerTool(string serverName, string serverAddress)
{
ServerName = Throw.IfNullOrWhitespace(serverName);
- Url = Throw.IfNull(url);
+ ServerAddress = Throw.IfNullOrWhitespace(serverAddress);
}
///
@@ -48,9 +36,14 @@ public HostedMcpServerTool(string serverName, Uri url)
public string ServerName { get; }
///
- /// Gets the URL of the remote MCP server.
+ /// Gets the address of the remote MCP server. This may be a URL, or in the case of a service providing built-in MCP servers with known names, it can be such a name.
///
- public Uri Url { get; }
+ public string ServerAddress { get; }
+
+ ///
+ /// Gets or sets the OAuth authorization token that the AI service should use when calling the remote MCP server.
+ ///
+ public string? AuthorizationToken { get; set; }
///
/// Gets or sets the description of the remote MCP server, used to provide more context to the AI service.
@@ -81,12 +74,4 @@ public HostedMcpServerTool(string serverName, Uri url)
///
///
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.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs
index b1e24461f84..cd7f1e46971 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs
@@ -534,11 +534,17 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt
break;
case HostedMcpServerTool mcpTool:
- McpTool responsesMcpTool = ResponseTool.CreateMcpTool(
- mcpTool.ServerName,
- mcpTool.Url,
- serverDescription: mcpTool.ServerDescription,
- headers: mcpTool.Headers);
+ McpTool responsesMcpTool = Uri.TryCreate(mcpTool.ServerAddress, UriKind.Absolute, out Uri? url) ?
+ ResponseTool.CreateMcpTool(
+ mcpTool.ServerName,
+ url,
+ mcpTool.AuthorizationToken,
+ mcpTool.ServerDescription) :
+ ResponseTool.CreateMcpTool(
+ mcpTool.ServerName,
+ new McpToolConnectorId(mcpTool.ServerAddress),
+ mcpTool.AuthorizationToken,
+ mcpTool.ServerDescription);
if (mcpTool.AllowedTools is not null)
{
@@ -657,7 +663,57 @@ internal static IEnumerable ToOpenAIResponseItems(IEnumerable parts = [];
+ foreach (AIContent item in input.Contents)
+ {
+ switch (item)
+ {
+ case AIContent when item.RawRepresentation is ResponseContentPart rawRep:
+ parts.Add(rawRep);
+ break;
+
+ case TextContent textContent:
+ parts.Add(ResponseContentPart.CreateInputTextPart(textContent.Text));
+ break;
+
+ case UriContent uriContent when uriContent.HasTopLevelMediaType("image"):
+ parts.Add(ResponseContentPart.CreateInputImagePart(uriContent.Uri));
+ break;
+
+ case DataContent dataContent when dataContent.HasTopLevelMediaType("image"):
+ parts.Add(ResponseContentPart.CreateInputImagePart(BinaryData.FromBytes(dataContent.Data), dataContent.MediaType));
+ break;
+
+ case DataContent dataContent when dataContent.MediaType.StartsWith("application/pdf", StringComparison.OrdinalIgnoreCase):
+ parts.Add(ResponseContentPart.CreateInputFilePart(BinaryData.FromBytes(dataContent.Data), dataContent.MediaType, dataContent.Name ?? $"{Guid.NewGuid():N}.pdf"));
+ break;
+
+ case HostedFileContent fileContent:
+ parts.Add(ResponseContentPart.CreateInputFilePart(fileContent.FileId));
+ break;
+
+ case ErrorContent errorContent when errorContent.ErrorCode == nameof(ResponseContentPartKind.Refusal):
+ parts.Add(ResponseContentPart.CreateRefusalPart(errorContent.Message));
+ break;
+
+ case McpServerToolApprovalResponseContent mcpApprovalResponseContent:
+ handleEmptyMessage = false;
+ yield return ResponseItem.CreateMcpApprovalResponseItem(mcpApprovalResponseContent.Id, mcpApprovalResponseContent.Approved);
+ break;
+ }
+ }
+
+ if (parts.Count == 0 && handleEmptyMessage)
+ {
+ parts.Add(ResponseContentPart.CreateInputTextPart(string.Empty));
+ }
+
+ if (parts.Count > 0)
+ {
+ yield return ResponseItem.CreateUserMessageItem(parts);
+ }
+
continue;
}
@@ -883,52 +939,6 @@ private static void PopulateAnnotations(ResponseContentPart source, AIContent de
}
}
- /// Convert a list of s to a list of .
- private static List ToResponseContentParts(IList contents)
- {
- List parts = [];
- foreach (var content in contents)
- {
- switch (content)
- {
- case AIContent when content.RawRepresentation is ResponseContentPart rawRep:
- parts.Add(rawRep);
- break;
-
- case TextContent textContent:
- parts.Add(ResponseContentPart.CreateInputTextPart(textContent.Text));
- break;
-
- case UriContent uriContent when uriContent.HasTopLevelMediaType("image"):
- parts.Add(ResponseContentPart.CreateInputImagePart(uriContent.Uri));
- break;
-
- case DataContent dataContent when dataContent.HasTopLevelMediaType("image"):
- parts.Add(ResponseContentPart.CreateInputImagePart(BinaryData.FromBytes(dataContent.Data), dataContent.MediaType));
- break;
-
- case DataContent dataContent when dataContent.MediaType.StartsWith("application/pdf", StringComparison.OrdinalIgnoreCase):
- parts.Add(ResponseContentPart.CreateInputFilePart(BinaryData.FromBytes(dataContent.Data), dataContent.MediaType, dataContent.Name ?? $"{Guid.NewGuid():N}.pdf"));
- break;
-
- case HostedFileContent fileContent:
- parts.Add(ResponseContentPart.CreateInputFilePart(fileContent.FileId));
- break;
-
- case ErrorContent errorContent when errorContent.ErrorCode == nameof(ResponseContentPartKind.Refusal):
- parts.Add(ResponseContentPart.CreateRefusalPart(errorContent.Message));
- break;
- }
- }
-
- if (parts.Count == 0)
- {
- parts.Add(ResponseContentPart.CreateInputTextPart(string.Empty));
- }
-
- return parts;
- }
-
/// Adds new for the specified into .
private static void AddMcpToolCallContent(McpToolCallItem mtci, IList contents)
{
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolCallContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolCallContentTests.cs
index ce6516124cd..d5c5b43ed0a 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolCallContentTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolCallContentTests.cs
@@ -12,15 +12,14 @@ public class McpServerToolCallContentTests
[Fact]
public void Constructor_PropsDefault()
{
- McpServerToolCallContent c = new("callId1", "toolName", "serverName");
+ McpServerToolCallContent c = new("callId1", "toolName", null);
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.ServerName);
Assert.Null(c.Arguments);
}
@@ -52,12 +51,10 @@ public void Constructor_PropsRoundtrip()
[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(string.Empty, "name", null));
+ Assert.Throws("toolName", () => new McpServerToolCallContent("callId1", string.Empty, null));
- Assert.Throws("callId", () => new McpServerToolCallContent(null!, "name", "serverName"));
- Assert.Throws("toolName", () => new McpServerToolCallContent("callId1", null!, "serverName"));
- Assert.Throws("serverName", () => new McpServerToolCallContent("callId1", "name", null!));
+ Assert.Throws("callId", () => new McpServerToolCallContent(null!, "name", null));
+ Assert.Throws("toolName", () => new McpServerToolCallContent("callId1", null!, null));
}
}
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs
index 6ab073e1dda..fe826a26820 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs
@@ -17,9 +17,11 @@ public void Constructor_PropsDefault()
Assert.Empty(tool.AdditionalProperties);
Assert.Equal("serverName", tool.ServerName);
- Assert.Equal("https://localhost/", tool.Url.ToString());
+ Assert.Equal("https://localhost/", tool.ServerAddress);
Assert.Empty(tool.Description);
+ Assert.Null(tool.AuthorizationToken);
+ Assert.Null(tool.ServerDescription);
Assert.Null(tool.AllowedTools);
Assert.Null(tool.ApprovalMode);
}
@@ -27,7 +29,7 @@ public void Constructor_PropsDefault()
[Fact]
public void Constructor_Roundtrips()
{
- HostedMcpServerTool tool = new("serverName", "https://localhost/");
+ HostedMcpServerTool tool = new("serverName", "connector_id");
Assert.Empty(tool.AdditionalProperties);
Assert.Empty(tool.Description);
@@ -35,9 +37,14 @@ public void Constructor_Roundtrips()
Assert.Equal(tool.Name, tool.ToString());
Assert.Equal("serverName", tool.ServerName);
- Assert.Equal("https://localhost/", tool.Url.ToString());
+ Assert.Equal("connector_id", tool.ServerAddress);
Assert.Empty(tool.Description);
+ Assert.Null(tool.AuthorizationToken);
+ string authToken = "Bearer token123";
+ tool.AuthorizationToken = authToken;
+ Assert.Equal(authToken, tool.AuthorizationToken);
+
Assert.Null(tool.ServerDescription);
string serverDescription = "This is a test server";
tool.ServerDescription = serverDescription;
@@ -58,20 +65,14 @@ public void Constructor_Roundtrips()
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));
+ Assert.Throws(() => new HostedMcpServerTool(string.Empty, "https://localhost/"));
+ Assert.Throws(() => new HostedMcpServerTool(null!, "https://localhost/"));
+ Assert.Throws(() => new HostedMcpServerTool("name", string.Empty));
+ Assert.Throws(() => new HostedMcpServerTool("name", null!));
}
}
diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs
index 35d72e09436..07e0cd94201 100644
--- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs
@@ -338,4 +338,63 @@ public async Task GetStreamingResponseAsync_BackgroundResponses_WithFunction()
Assert.Contains("5:43", responseText);
Assert.Equal(1, callCount);
}
+
+ [ConditionalFact]
+ public async Task RemoteMCP_Connector()
+ {
+ SkipIfNotEnabled();
+
+ if (TestRunnerConfiguration.Instance["RemoteMCP:ConnectorAccessToken"] is not string accessToken)
+ {
+ throw new SkipTestException(
+ "To run this test, set a value for RemoteMCP:ConnectorAccessToken. " +
+ "You can obtain one by following https://platform.openai.com/docs/guides/tools-connectors-mcp?quickstart-panels=connector#authorizing-a-connector.");
+ }
+
+ await RunAsync(false, false);
+ await RunAsync(true, true);
+
+ async Task RunAsync(bool streaming, bool approval)
+ {
+ ChatOptions chatOptions = new()
+ {
+ Tools = [new HostedMcpServerTool("calendar", "connector_googlecalendar")
+ {
+ ApprovalMode = approval ?
+ HostedMcpServerToolApprovalMode.AlwaysRequire :
+ HostedMcpServerToolApprovalMode.NeverRequire,
+ AuthorizationToken = accessToken
+ }
+ ],
+ };
+
+ using var client = CreateChatClient()!;
+
+ List input = [new ChatMessage(ChatRole.User, "What is on my calendar for today?")];
+
+ ChatResponse response = streaming ?
+ await client.GetStreamingResponseAsync(input, chatOptions).ToChatResponseAsync() :
+ await client.GetResponseAsync(input, chatOptions);
+
+ if (approval)
+ {
+ input.AddRange(response.Messages);
+ var approvalRequest = Assert.Single(response.Messages.SelectMany(m => m.Contents).OfType());
+ Assert.Equal("search_events", approvalRequest.ToolCall.ToolName);
+ input.Add(new ChatMessage(ChatRole.Tool, [approvalRequest.CreateResponse(true)]));
+
+ response = streaming ?
+ await client.GetStreamingResponseAsync(input, chatOptions).ToChatResponseAsync() :
+ await client.GetResponseAsync(input, chatOptions);
+ }
+
+ Assert.NotNull(response);
+ var toolCall = Assert.Single(response.Messages.SelectMany(m => m.Contents).OfType());
+ Assert.Equal("search_events", toolCall.ToolName);
+
+ var toolResult = Assert.Single(response.Messages.SelectMany(m => m.Contents).OfType());
+ var content = Assert.IsType(Assert.Single(toolResult.Output!));
+ Assert.Equal(@"{""events"": [], ""next_page_token"": null}", content.Text);
+ }
+ }
}
diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs
index c4c6f6b767d..fce0bab3ee5 100644
--- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs
@@ -826,6 +826,277 @@ public async Task MultipleOutputItems_NonStreaming()
Assert.Equal(36, response.Usage.TotalTokenCount);
}
+ [Theory]
+ [InlineData("user")]
+ [InlineData("tool")]
+ public async Task McpToolCall_ApprovalRequired_NonStreaming(string role)
+ {
+ string input = """
+ {
+ "model": "gpt-4o-mini",
+ "tools": [
+ {
+ "type": "mcp",
+ "server_label": "deepwiki",
+ "server_url": "https://mcp.deepwiki.com/mcp"
+ }
+ ],
+ "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"
+ }
+ ]
+ }
+ ]
+ }
+ """;
+
+ string output = """
+ {
+ "id": "resp_04e29d5bdd80bd9f0068e6b01f786081a29148febb92892aee",
+ "object": "response",
+ "created_at": 1759948831,
+ "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": "mcpr_04e29d5bdd80bd9f0068e6b022a9c081a2ae898104b7a75051",
+ "type": "mcp_approval_request",
+ "arguments": "{\"repoName\":\"dotnet/extensions\"}",
+ "name": "ask_question",
+ "server_label": "deepwiki"
+ }
+ ],
+ "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": "always",
+ "server_description": null,
+ "server_label": "deepwiki",
+ "server_url": "https://mcp.deepwiki.com/"
+ }
+ ],
+ "top_logprobs": 0,
+ "top_p": 1.0,
+ "truncation": "disabled",
+ "usage": {
+ "input_tokens": 193,
+ "input_tokens_details": {
+ "cached_tokens": 0
+ },
+ "output_tokens": 23,
+ "output_tokens_details": {
+ "reasoning_tokens": 0
+ },
+ "total_tokens": 216
+ },
+ "user": null,
+ "metadata": {}
+ }
+ """;
+
+ var chatOptions = new ChatOptions
+ {
+ Tools = [new HostedMcpServerTool("deepwiki", "https://mcp.deepwiki.com/mcp")]
+ };
+ McpServerToolApprovalRequestContent approvalRequest;
+
+ using (VerbatimHttpHandler handler = new(input, output))
+ using (HttpClient httpClient = new(handler))
+ using (IChatClient client = CreateResponseClient(httpClient, "gpt-4o-mini"))
+ {
+ 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);
+
+ approvalRequest = Assert.Single(response.Messages.SelectMany(m => m.Contents).OfType());
+ chatOptions.ConversationId = response.ConversationId;
+ }
+
+ input = $$"""
+ {
+ "previous_response_id": "resp_04e29d5bdd80bd9f0068e6b01f786081a29148febb92892aee",
+ "model": "gpt-4o-mini",
+ "tools": [
+ {
+ "type": "mcp",
+ "server_label": "deepwiki",
+ "server_url": "https://mcp.deepwiki.com/mcp"
+ }
+ ],
+ "tool_choice": "auto",
+ "input": [
+ {
+ "type": "mcp_approval_response",
+ "approval_request_id": "mcpr_04e29d5bdd80bd9f0068e6b022a9c081a2ae898104b7a75051",
+ "approve": true
+ }
+ ]
+ }
+ """;
+
+ output = """
+ {
+ "id": "resp_06ee3b1962eeb8470068e6b21c377081a3a20dbf60eee7a736",
+ "object": "response",
+ "created_at": 1759949340,
+ "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": "mcp_06ee3b1962eeb8470068e6b21cbaa081a3b5aa2a6c989f4c6f",
+ "type": "mcp_call",
+ "status": "completed",
+ "approval_request_id": "mcpr_06ee3b1962eeb8470068e6b192985c81a383a16059ecd8230e",
+ "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 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_06ee3b1962eeb8470068e6b226ab0081a39fccce9aa47aedbc",
+ "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 provides an overview of the `Microsoft.Extensions.AI.Abstractions` package, including installation instructions and usage examples for its core interfaces like `IChatClient` and `IEmbeddingGenerator`."
+ }
+ ],
+ "role": "assistant"
+ }
+ ],
+ "parallel_tool_calls": true,
+ "previous_response_id": "resp_06ee3b1962eeb8470068e6b18e0db881a3bdfd255a60327cdc",
+ "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": "always",
+ "server_description": null,
+ "server_label": "deepwiki",
+ "server_url": "https://mcp.deepwiki.com/"
+ }
+ ],
+ "top_logprobs": 0,
+ "top_p": 1.0,
+ "truncation": "disabled",
+ "usage": {
+ "input_tokens": 542,
+ "input_tokens_details": {
+ "cached_tokens": 0
+ },
+ "output_tokens": 72,
+ "output_tokens_details": {
+ "reasoning_tokens": 0
+ },
+ "total_tokens": 614
+ },
+ "user": null,
+ "metadata": {}
+ }
+ """;
+
+ using (VerbatimHttpHandler handler = new(input, output))
+ using (HttpClient httpClient = new(handler))
+ using (IChatClient client = CreateResponseClient(httpClient, "gpt-4o-mini"))
+ {
+ var response = await client.GetResponseAsync(
+ new ChatMessage(new ChatRole(role), [approvalRequest.CreateResponse(true)]), chatOptions);
+
+ Assert.NotNull(response);
+
+ Assert.Equal("resp_06ee3b1962eeb8470068e6b21c377081a3a20dbf60eee7a736", response.ResponseId);
+ Assert.Equal("resp_06ee3b1962eeb8470068e6b21c377081a3a20dbf60eee7a736", response.ConversationId);
+ Assert.Equal("gpt-4o-mini-2024-07-18", response.ModelId);
+ Assert.Equal(DateTimeOffset.FromUnixTimeSeconds(1_759_949_340), 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 provides an overview of the `Microsoft.Extensions.AI.Abstractions` package, including installation instructions and usage examples for its core interfaces like `IChatClient` and `IEmbeddingGenerator`.", response.Messages[0].Text);
+
+ Assert.Equal(3, message.Contents.Count);
+
+ var call = Assert.IsType(message.Contents[0]);
+ Assert.Equal("mcp_06ee3b1962eeb8470068e6b21cbaa081a3b5aa2a6c989f4c6f", call.CallId);
+ Assert.Equal("deepwiki", call.ServerName);
+ Assert.Equal("ask_question", call.ToolName);
+ Assert.NotNull(call.Arguments);
+ Assert.Equal(2, call.Arguments.Count);
+ Assert.Equal("dotnet/extensions", ((JsonElement)call.Arguments["repoName"]!).GetString());
+ Assert.Equal("What is the path to the README.md file for Microsoft.Extensions.AI.Abstractions?", ((JsonElement)call.Arguments["question"]!).GetString());
+
+ var result = Assert.IsType(message.Contents[1]);
+ Assert.Equal("mcp_06ee3b1962eeb8470068e6b21cbaa081a3b5aa2a6c989f4c6f", result.CallId);
+ Assert.NotNull(result.Output);
+ Assert.StartsWith("The `README.md` file for `Microsoft.Extensions.AI.Abstractions` is located at", Assert.IsType(Assert.Single(result.Output)).Text);
+
+ Assert.NotNull(response.Usage);
+ Assert.Equal(542, response.Usage.InputTokenCount);
+ Assert.Equal(72, response.Usage.OutputTokenCount);
+ Assert.Equal(614, response.Usage.TotalTokenCount);
+ }
+ }
+
[Theory]
[InlineData(false)]
[InlineData(true)]