From 7eca7b1321195f7adf833c51cd1c7e5ccf5d6e6a Mon Sep 17 00:00:00 2001 From: Eric Chapman Date: Tue, 18 Nov 2025 23:11:38 +0800 Subject: [PATCH 01/17] Add support for custom headers in HostedMcpServerTool Introduces a Headers property to HostedMcpServerTool for specifying additional request headers. Updates OpenAIResponsesChatClient to pass these headers when creating MCP tools and adds unit tests to verify header roundtripping. --- .../Tools/HostedMcpServerTool.cs | 5 +++++ .../OpenAIResponsesChatClient.cs | 4 ++-- .../Tools/HostedMcpServerToolTests.cs | 11 +++++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs index aa33a581710..f50d66be48f 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs @@ -99,4 +99,9 @@ private static string ValidateUrl(Uri serverUrl) /// /// public HostedMcpServerToolApprovalMode? ApprovalMode { get; set; } + + /// + /// Gets or sets additional headers to include in requests to the remote 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 d14c0036358..6f06247ea3c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -537,12 +537,12 @@ void IDisposable.Dispose() mcpTool.ServerName, url, mcpTool.AuthorizationToken, - mcpTool.ServerDescription) : + mcpTool.ServerDescription, mcpTool.Headers) : ResponseTool.CreateMcpTool( mcpTool.ServerName, new McpToolConnectorId(mcpTool.ServerAddress), mcpTool.AuthorizationToken, - mcpTool.ServerDescription); + mcpTool.ServerDescription, mcpTool.Headers); if (mcpTool.AllowedTools is not null) { diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs index ec1dc407973..6461b4e7a9b 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs @@ -62,6 +62,17 @@ public void Constructor_Roundtrips() tool.ApprovalMode = HostedMcpServerToolApprovalMode.AlwaysRequire; Assert.Same(HostedMcpServerToolApprovalMode.AlwaysRequire, tool.ApprovalMode); + Assert.Null(tool.Headers); + IDictionary headers = new Dictionary + { + ["X-Custom-Header"] = "value1", + }; + tool.Headers = headers; + Assert.Same(headers, tool.Headers); + + tool.Headers = null; + Assert.Null(tool.Headers); + var customApprovalMode = new HostedMcpServerToolRequireSpecificApprovalMode(["tool1"], ["tool2"]); tool.ApprovalMode = customApprovalMode; Assert.Same(customApprovalMode, tool.ApprovalMode); From 6a19b2a49ecc5c5fcb860680a9ed1521465b4a97 Mon Sep 17 00:00:00 2001 From: Eric Chapman Date: Sun, 23 Nov 2025 06:57:09 +0800 Subject: [PATCH 02/17] nitpick correction on code structure Keep the bots happy --- .../Tools/HostedMcpServerToolTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 6461b4e7a9b..12b2147972c 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs @@ -62,6 +62,10 @@ public void Constructor_Roundtrips() 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); IDictionary headers = new Dictionary { @@ -72,10 +76,6 @@ public void Constructor_Roundtrips() tool.Headers = null; Assert.Null(tool.Headers); - - var customApprovalMode = new HostedMcpServerToolRequireSpecificApprovalMode(["tool1"], ["tool2"]); - tool.ApprovalMode = customApprovalMode; - Assert.Same(customApprovalMode, tool.ApprovalMode); } [Fact] From 2b07d094a9c9041087d97b378d8f584d928e3dc2 Mon Sep 17 00:00:00 2001 From: Eric Chapman Date: Wed, 3 Dec 2025 07:16:02 +0800 Subject: [PATCH 03/17] Remove obsolete CP0002 suppressions for HostedMcpServerTool Deleted baseline suppressions for the Headers property and its setter in HostedMcpServerTool from CompatibilitySuppressions.xml. --- .../CompatibilitySuppressions.xml | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml index 993fd3d3ff0..5404f37882f 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml @@ -22,13 +22,6 @@ lib/net462/Microsoft.Extensions.AI.Abstractions.dll true - - CP0002 - M:Microsoft.Extensions.AI.HostedMcpServerTool.get_Headers - lib/net462/Microsoft.Extensions.AI.Abstractions.dll - lib/net462/Microsoft.Extensions.AI.Abstractions.dll - true - CP0002 M:Microsoft.Extensions.AI.HostedMcpServerTool.get_Url @@ -36,13 +29,6 @@ lib/net462/Microsoft.Extensions.AI.Abstractions.dll true - - CP0002 - M:Microsoft.Extensions.AI.HostedMcpServerTool.set_Headers(System.Collections.Generic.IDictionary{System.String,System.String}) - lib/net462/Microsoft.Extensions.AI.Abstractions.dll - lib/net462/Microsoft.Extensions.AI.Abstractions.dll - true - CP0002 M:Microsoft.Extensions.AI.ChatOptions.get_ContinuationToken @@ -169,4 +155,4 @@ lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll true - \ No newline at end of file + From 65eaefc4a2d320268f78928835dc920bc0dec424 Mon Sep 17 00:00:00 2001 From: Eric Chapman Date: Wed, 3 Dec 2025 07:22:10 +0800 Subject: [PATCH 04/17] Improve documentation and formatting for MCP tool headers Expanded XML documentation for the Headers property in HostedMcpServerTool to clarify usage and behavior. Minor formatting adjustments in OpenAIResponsesChatClient for MCP tool creation calls. --- .../Tools/HostedMcpServerTool.cs | 13 ++++++++++++- .../OpenAIResponsesChatClient.cs | 6 ++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs index f50d66be48f..3d6b2f5a8c8 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs @@ -101,7 +101,18 @@ private static string ValidateUrl(Uri serverUrl) public HostedMcpServerToolApprovalMode? ApprovalMode { get; set; } /// - /// Gets or sets additional headers to include in requests to the remote MCP server. + /// Gets or sets the HTTP headers that the AI service could use when calling the remote MCP server. /// + /// + /// + /// The default value is , which indicates no additional headers should be included. + /// + /// + /// If specified, these headers will be included in addition to any authorization headers specified via . Headers conflicting with authorization headers may cause undefined behavior. + /// + /// + /// Providers are not guaranteed to support or honor these headers. + /// + /// 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 20bf364def7..bf4b0e3dda2 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -579,12 +579,14 @@ void IDisposable.Dispose() mcpTool.ServerName, url, mcpTool.AuthorizationToken, - mcpTool.ServerDescription, mcpTool.Headers) : + mcpTool.ServerDescription, + mcpTool.Headers) : ResponseTool.CreateMcpTool( mcpTool.ServerName, new McpToolConnectorId(mcpTool.ServerAddress), mcpTool.AuthorizationToken, - mcpTool.ServerDescription, mcpTool.Headers); + mcpTool.ServerDescription, + mcpTool.Headers); if (mcpTool.AllowedTools is not null) { From 2f86ba9e50dbcec66bc9125ca1f2a30d4da55895 Mon Sep 17 00:00:00 2001 From: Eric Chapman Date: Wed, 3 Dec 2025 09:04:52 +0800 Subject: [PATCH 05/17] Remove CP0002 suppressions for HostedMcpServerTool.Headers Deleted baseline suppressions for get_Headers and set_Headers members of HostedMcpServerTool across net8.0, net9.0, and netstandard2.0. This likely reflects removal or resolution of related compatibility issues. --- .../CompatibilitySuppressions.xml | 42 ------------------- 1 file changed, 42 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml index 5404f37882f..4990241bab5 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml @@ -50,13 +50,6 @@ lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll true - - CP0002 - M:Microsoft.Extensions.AI.HostedMcpServerTool.get_Headers - lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll - lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll - true - CP0002 M:Microsoft.Extensions.AI.HostedMcpServerTool.get_Url @@ -64,13 +57,6 @@ lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll true - - CP0002 - M:Microsoft.Extensions.AI.HostedMcpServerTool.set_Headers(System.Collections.Generic.IDictionary{System.String,System.String}) - lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll - lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll - true - CP0002 M:Microsoft.Extensions.AI.ChatOptions.get_ContinuationToken @@ -92,13 +78,6 @@ lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll true - - CP0002 - M:Microsoft.Extensions.AI.HostedMcpServerTool.get_Headers - lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll - lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll - true - CP0002 M:Microsoft.Extensions.AI.HostedMcpServerTool.get_Url @@ -106,13 +85,6 @@ lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll true - - CP0002 - M:Microsoft.Extensions.AI.HostedMcpServerTool.set_Headers(System.Collections.Generic.IDictionary{System.String,System.String}) - lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll - lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll - true - CP0002 M:Microsoft.Extensions.AI.ChatOptions.get_ContinuationToken @@ -134,13 +106,6 @@ lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll true - - CP0002 - M:Microsoft.Extensions.AI.HostedMcpServerTool.get_Headers - lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll - lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll - true - CP0002 M:Microsoft.Extensions.AI.HostedMcpServerTool.get_Url @@ -148,11 +113,4 @@ lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll true - - CP0002 - M:Microsoft.Extensions.AI.HostedMcpServerTool.set_Headers(System.Collections.Generic.IDictionary{System.String,System.String}) - lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll - lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll - true - From 3d24eaa9d53022397027abf1b3d3a1bd456452c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Thu, 4 Dec 2025 10:39:34 -0800 Subject: [PATCH 06/17] Update docs --- .../Tools/HostedMcpServerTool.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs index 3d6b2f5a8c8..15d98cbdeaf 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs @@ -105,13 +105,10 @@ private static string ValidateUrl(Uri serverUrl) /// /// /// - /// The default value is , which indicates no additional headers should be included. + /// The underlying provider may respond with an error if is set and an Authorization header is included. /// /// - /// If specified, these headers will be included in addition to any authorization headers specified via . Headers conflicting with authorization headers may cause undefined behavior. - /// - /// - /// Providers are not guaranteed to support or honor these headers. + /// The underlying provider is not guaranteed to support or honor the headers. /// /// public IDictionary? Headers { get; set; } From b323648601d0384282eaf191dcca874ac4712694 Mon Sep 17 00:00:00 2001 From: Eric Chapman Date: Mon, 8 Dec 2025 08:47:52 +0800 Subject: [PATCH 07/17] Clarify XML doc for HTTP headers property Updated the XML documentation for the HTTP headers property to clarify that these headers are included when calling the remote MCP server. --- .../Tools/HostedMcpServerTool.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs index 3d6b2f5a8c8..d1cad24c387 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs @@ -101,7 +101,7 @@ private static string ValidateUrl(Uri serverUrl) public HostedMcpServerToolApprovalMode? ApprovalMode { get; set; } /// - /// Gets or sets the HTTP headers that the AI service could use when calling the remote MCP server. + /// Gets or sets HTTP headers to include when calling the remote MCP server. /// /// /// From 22b074524c8afd738b3d8af0fe0049e21fd7a106 Mon Sep 17 00:00:00 2001 From: Eric Chapman Date: Fri, 12 Dec 2025 21:51:15 +0800 Subject: [PATCH 08/17] Copy Headers property in responsesMcpTool assignment Added assignment of the Headers property from mcpTool to responsesMcpTool to ensure all relevant data is copied. --- .../Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index e6359cbdd7a..3ac0b4aef50 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -595,6 +595,7 @@ void IDisposable.Dispose() responsesMcpTool.ServerDescription = mcpTool.ServerDescription; responsesMcpTool.AuthorizationToken = mcpTool.AuthorizationToken; + responsesMcpTool.Headers = mcpTool.Headers; if (mcpTool.AllowedTools is not null) { From f2501879afabb4b3b3265997ae672cdb6bf5bc8e Mon Sep 17 00:00:00 2001 From: Eric Chapman Date: Fri, 12 Dec 2025 22:27:56 +0800 Subject: [PATCH 09/17] Sync AuthorizationToken with Headers dictionary Updated the AuthorizationToken property to automatically manage the 'Authorization' header in the Headers dictionary. Setting the property adds or updates the header, and setting it to null removes the header, ensuring consistency between the property and the headers. --- .../Tools/HostedMcpServerTool.cs | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs index 290d45868c1..7f080ba5b6a 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs @@ -103,7 +103,29 @@ private static string ValidateUrl(Uri serverUrl) /// /// Gets or sets the OAuth authorization token that the AI service should use when calling the remote MCP server. /// - public string? AuthorizationToken { get; set; } + /// + /// When set, this value is automatically added to the dictionary with the key "Authorization" + /// and the value "Bearer {token}". Setting this property will overwrite any existing "Authorization" header in . + /// Setting this property to will remove the "Authorization" header from . + /// + public string? AuthorizationToken + { + get; + set + { + field = value; + + if (value is not null) + { + Headers ??= new Dictionary(); + Headers["Authorization"] = $"Bearer {value}"; + } + else if (Headers is not null) + { + _ = Headers.Remove("Authorization"); + } + } + } /// /// Gets or sets the description of the remote MCP server, used to provide more context to the AI service. From 1da522b12d00f4eaf3c4fc55ab67ef6b235eeaf7 Mon Sep 17 00:00:00 2001 From: Eric Chapman Date: Fri, 12 Dec 2025 23:54:55 +0800 Subject: [PATCH 10/17] Add test for MCP tool call with auth token and headers Introduces a test to verify that HostedMcpServerTool correctly includes an Authorization token and custom headers in the request. Also updates a test assertion to expect non-null headers by default. --- .../Tools/HostedMcpServerToolTests.cs | 2 +- .../OpenAIResponseClientTests.cs | 82 +++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) 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 a76a4f56463..f5193da208f 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs @@ -96,7 +96,7 @@ public void Constructor_Roundtrips() tool.ApprovalMode = customApprovalMode; Assert.Same(customApprovalMode, tool.ApprovalMode); - Assert.Null(tool.Headers); + Assert.NotNull(tool.Headers); IDictionary headers = new Dictionary { ["X-Custom-Header"] = "value1", diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs index 1e19466ee7f..0eb205d9f62 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs @@ -2130,6 +2130,88 @@ public async Task McpToolCall_ApprovalNotRequired_Streaming() Assert.Equal(1569, response.Usage.TotalTokenCount); } + [Fact] + public async Task McpToolCall_WithAuthorizationTokenAndCustomHeaders_IncludesInRequest() + { + const string Input = """ + { + "model": "gpt-4o-mini", + "tools": [ + { + "type": "mcp", + "server_label": "deepwiki", + "server_url": "https://mcp.deepwiki.com/mcp", + "require_approval": "never", + "headers": { + "Authorization": "Bearer test-auth-token-12345", + "X-Custom-Header": "custom-value" + } + } + ], + "input": [ + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": "hello" + } + ] + } + ] + } + """; + + const string Output = """ + { + "id": "resp_auth01", + "object": "response", + "created_at": 1757299043, + "status": "completed", + "model": "gpt-4o-mini-2024-07-18", + "output": [ + { + "id": "msg_auth01", + "type": "message", + "status": "completed", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "Hi!" + } + ] + } + ], + "usage": { + "input_tokens": 10, + "output_tokens": 2, + "total_tokens": 12 + } + } + """; + + using VerbatimHttpHandler handler = new(Input, Output); + using HttpClient httpClient = new(handler); + using IChatClient client = CreateResponseClient(httpClient, "gpt-4o-mini"); + + var mcpTool = new HostedMcpServerTool("deepwiki", new Uri("https://mcp.deepwiki.com/mcp")) + { + ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire, + AuthorizationToken = "test-auth-token-12345" + }; + + mcpTool.Headers!["X-Custom-Header"] = "custom-value"; + + var response = await client.GetResponseAsync("hello", new ChatOptions { Tools = [mcpTool] }); + + Assert.NotNull(response); + Assert.Equal("resp_auth01", response.ResponseId); + var message = Assert.Single(response.Messages); + Assert.Equal("Hi!", message.Text); + } + [Fact] public async Task GetResponseAsync_BackgroundResponses_FirstCall() { From a086a88376b267ea62e1a2d3176ec16840adcb74 Mon Sep 17 00:00:00 2001 From: Eric Chapman Date: Sat, 13 Dec 2025 00:11:35 +0800 Subject: [PATCH 11/17] Add headers support to HostedMcpServerTool constructors Extended HostedMcpServerTool to accept HTTP headers via constructors and made Headers a non-nullable, always-initialized property. Updated tests to cover new constructor overloads, header initialization, and interaction with AuthorizationToken. --- .../Tools/HostedMcpServerTool.cs | 29 ++-- .../Tools/HostedMcpServerToolTests.cs | 128 ++++++++++++++++-- 2 files changed, 135 insertions(+), 22 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs index 7f080ba5b6a..f57f4a017e6 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs @@ -22,12 +22,14 @@ public class HostedMcpServerTool : AITool /// /// The name of the remote MCP server. /// 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. + /// HTTP headers to include when calling the remote MCP server. If , an empty dictionary is created. /// or is . /// or is empty or composed entirely of whitespace. - public HostedMcpServerTool(string serverName, string serverAddress) + public HostedMcpServerTool(string serverName, string serverAddress, IDictionary? headers = null) { ServerName = Throw.IfNullOrWhitespace(serverName); ServerAddress = Throw.IfNullOrWhitespace(serverAddress); + Headers = headers ?? new Dictionary(); } /// @@ -35,11 +37,12 @@ public HostedMcpServerTool(string serverName, string serverAddress) /// /// The name of the remote MCP server. /// 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. + /// HTTP headers to include when calling the remote MCP server. If , an empty dictionary is created. /// Any additional properties associated with the tool. /// or is . /// or is empty or composed entirely of whitespace. - public HostedMcpServerTool(string serverName, string serverAddress, IReadOnlyDictionary? additionalProperties) - : this(serverName, serverAddress) + public HostedMcpServerTool(string serverName, string serverAddress, IDictionary? headers, IReadOnlyDictionary? additionalProperties) + : this(serverName, serverAddress, headers) { _additionalProperties = additionalProperties; } @@ -49,11 +52,12 @@ public HostedMcpServerTool(string serverName, string serverAddress, IReadOnlyDic /// /// The name of the remote MCP server. /// The URL of the remote MCP server. + /// HTTP headers to include when calling the remote MCP server. If , an empty dictionary is created. /// or is . /// is empty or composed entirely of whitespace. /// is not an absolute URL. - public HostedMcpServerTool(string serverName, Uri serverUrl) - : this(serverName, ValidateUrl(serverUrl)) + public HostedMcpServerTool(string serverName, Uri serverUrl, IDictionary? headers = null) + : this(serverName, ValidateUrl(serverUrl), headers) { } @@ -62,12 +66,13 @@ public HostedMcpServerTool(string serverName, Uri serverUrl) /// /// The name of the remote MCP server. /// The URL of the remote MCP server. + /// HTTP headers to include when calling the remote MCP server. If , an empty dictionary is created. /// Any additional properties associated with the tool. /// or is . /// is empty or composed entirely of whitespace. /// is not an absolute URL. - public HostedMcpServerTool(string serverName, Uri serverUrl, IReadOnlyDictionary? additionalProperties) - : this(serverName, ValidateUrl(serverUrl)) + public HostedMcpServerTool(string serverName, Uri serverUrl, IDictionary? headers, IReadOnlyDictionary? additionalProperties) + : this(serverName, ValidateUrl(serverUrl), headers) { _additionalProperties = additionalProperties; } @@ -117,10 +122,9 @@ public string? AuthorizationToken if (value is not null) { - Headers ??= new Dictionary(); Headers["Authorization"] = $"Bearer {value}"; } - else if (Headers is not null) + else { _ = Headers.Remove("Authorization"); } @@ -158,15 +162,12 @@ public string? AuthorizationToken public HostedMcpServerToolApprovalMode? ApprovalMode { get; set; } /// - /// Gets or sets HTTP headers to include when calling the remote MCP server. + /// Gets a mutable dictionary of HTTP headers to include when calling the remote MCP server. /// /// /// - /// The underlying provider may respond with an error if is set and an Authorization header is included. - /// - /// /// The underlying provider is not guaranteed to support or honor the headers. /// /// - public IDictionary? Headers { get; set; } + public IDictionary Headers { get; } } 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 f5193da208f..eab881b4582 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs @@ -24,13 +24,15 @@ public void Constructor_PropsDefault() Assert.Null(tool.ServerDescription); Assert.Null(tool.AllowedTools); Assert.Null(tool.ApprovalMode); + Assert.NotNull(tool.Headers); + Assert.Empty(tool.Headers); } [Fact] public void Constructor_AdditionalProperties_String_Roundtrips() { var props = new Dictionary { ["key"] = "value" }; - HostedMcpServerTool tool = new("serverName", "connector_id", props); + HostedMcpServerTool tool = new("serverName", "connector_id", null, props); Assert.Equal("serverName", tool.ServerName); Assert.Equal("connector_id", tool.ServerAddress); @@ -41,7 +43,7 @@ public void Constructor_AdditionalProperties_String_Roundtrips() public void Constructor_AdditionalProperties_Uri_Roundtrips() { var props = new Dictionary { ["key"] = "value" }; - HostedMcpServerTool tool = new("serverName", new Uri("https://localhost/"), props); + HostedMcpServerTool tool = new("serverName", new Uri("https://localhost/"), null, props); Assert.Equal("serverName", tool.ServerName); Assert.Equal("https://localhost/", tool.ServerAddress); @@ -51,7 +53,7 @@ public void Constructor_AdditionalProperties_Uri_Roundtrips() [Fact] public void Constructor_NullAdditionalProperties_UsesEmpty() { - HostedMcpServerTool tool = new("serverName", "connector_id", null); + HostedMcpServerTool tool = new("serverName", "connector_id", null, null); Assert.Empty(tool.AdditionalProperties); } @@ -97,15 +99,124 @@ public void Constructor_Roundtrips() Assert.Same(customApprovalMode, tool.ApprovalMode); Assert.NotNull(tool.Headers); - IDictionary headers = new Dictionary + Assert.Empty(tool.Headers); + tool.Headers["X-Custom-Header"] = "value1"; + Assert.Single(tool.Headers); + Assert.Equal("value1", tool.Headers["X-Custom-Header"]); + } + + [Fact] + public void Constructor_WithHeaders_String_Roundtrips() + { + var headers = new Dictionary { - ["X-Custom-Header"] = "value1", + ["Authorization"] = "Bearer token123", + ["X-Custom"] = "value1" }; - tool.Headers = headers; + HostedMcpServerTool tool = new("serverName", "connector_id", headers); + + Assert.Equal("serverName", tool.ServerName); + Assert.Equal("connector_id", tool.ServerAddress); Assert.Same(headers, tool.Headers); + Assert.Equal(2, tool.Headers.Count); + Assert.Equal("Bearer token123", tool.Headers["Authorization"]); + Assert.Equal("value1", tool.Headers["X-Custom"]); + } + + [Fact] + public void Constructor_WithHeaders_Uri_Roundtrips() + { + var headers = new Dictionary + { + ["Authorization"] = "Bearer token456", + ["X-Custom"] = "value2" + }; + HostedMcpServerTool tool = new("serverName", new Uri("https://localhost/"), headers); - tool.Headers = null; - Assert.Null(tool.Headers); + Assert.Equal("serverName", tool.ServerName); + Assert.Equal("https://localhost/", tool.ServerAddress); + Assert.Same(headers, tool.Headers); + Assert.Equal(2, tool.Headers.Count); + Assert.Equal("Bearer token456", tool.Headers["Authorization"]); + Assert.Equal("value2", tool.Headers["X-Custom"]); + } + + [Fact] + public void Constructor_WithHeaders_And_AdditionalProperties_String_Roundtrips() + { + var headers = new Dictionary + { + ["X-Header"] = "headerValue" + }; + var props = new Dictionary { ["key"] = "value" }; + HostedMcpServerTool tool = new("serverName", "connector_id", headers, props); + + Assert.Equal("serverName", tool.ServerName); + Assert.Equal("connector_id", tool.ServerAddress); + Assert.Same(headers, tool.Headers); + Assert.Same(props, tool.AdditionalProperties); + Assert.Single(tool.Headers); + Assert.Equal("headerValue", tool.Headers["X-Header"]); + } + + [Fact] + public void Constructor_WithHeaders_And_AdditionalProperties_Uri_Roundtrips() + { + var headers = new Dictionary + { + ["X-Header"] = "headerValue" + }; + var props = new Dictionary { ["key"] = "value" }; + HostedMcpServerTool tool = new("serverName", new Uri("https://localhost/"), headers, props); + + Assert.Equal("serverName", tool.ServerName); + Assert.Equal("https://localhost/", tool.ServerAddress); + Assert.Same(headers, tool.Headers); + Assert.Same(props, tool.AdditionalProperties); + Assert.Single(tool.Headers); + Assert.Equal("headerValue", tool.Headers["X-Header"]); + } + + [Fact] + public void Constructor_WithNullHeaders_CreatesEmptyDictionary() + { + HostedMcpServerTool tool1 = new("serverName", "connector_id", headers: null); + Assert.NotNull(tool1.Headers); + Assert.Empty(tool1.Headers); + + HostedMcpServerTool tool2 = new("serverName", new Uri("https://localhost/"), headers: null); + Assert.NotNull(tool2.Headers); + Assert.Empty(tool2.Headers); + } + + [Fact] + public void AuthorizationToken_And_Headers_NoOrderingIssues() + { + // Verify that setting AuthorizationToken followed by adding to Headers works + var tool1 = new HostedMcpServerTool("server", "https://localhost/") + { + AuthorizationToken = "token123" + }; + tool1.Headers["X-Custom"] = "value1"; + + Assert.Equal(2, tool1.Headers.Count); + Assert.Equal("Bearer token123", tool1.Headers["Authorization"]); + Assert.Equal("value1", tool1.Headers["X-Custom"]); + + // Verify that adding to Headers followed by setting AuthorizationToken works the same + var tool2 = new HostedMcpServerTool("server", "https://localhost/"); + tool2.Headers["X-Custom"] = "value1"; + tool2.AuthorizationToken = "token123"; + + Assert.Equal(2, tool2.Headers.Count); + Assert.Equal("Bearer token123", tool2.Headers["Authorization"]); + Assert.Equal("value1", tool2.Headers["X-Custom"]); + + // Verify setting AuthorizationToken to null removes only Authorization header + tool2.AuthorizationToken = null; + Assert.Single(tool2.Headers); + Assert.False(tool2.Headers.ContainsKey("Authorization")); + Assert.Equal("value1", tool2.Headers["X-Custom"]); } [Fact] @@ -122,3 +233,4 @@ public void Constructor_Throws() Assert.Throws("serverUrl", () => new HostedMcpServerTool("name", (Uri)null!)); } } + From a169c3164d1261520fe4c05be102f28468e3c2ba Mon Sep 17 00:00:00 2001 From: Eric Chapman Date: Sat, 13 Dec 2025 15:56:58 +0800 Subject: [PATCH 12/17] Refactor HostedMcpServerTool header handling and constructors Removed headers parameter from HostedMcpServerTool constructors and introduced a settable Headers property with internal backing field. Updated AuthorizationToken logic to interact with the Headers property. Adjusted usage in OpenAIResponsesChatClient and updated related unit tests to set headers via the property instead of constructor. --- .../Tools/HostedMcpServerTool.cs | 59 +++++++++++-------- .../OpenAIResponsesChatClient.cs | 5 +- .../Tools/HostedMcpServerToolTests.cs | 22 ++++--- 3 files changed, 50 insertions(+), 36 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs index f57f4a017e6..b03cff8d44d 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs @@ -16,49 +16,47 @@ public class HostedMcpServerTool : AITool { /// Any additional properties associated with the tool. private IReadOnlyDictionary? _additionalProperties; + // Backing field for the property. + private IDictionary? _headers; /// /// Initializes a new instance of the class. /// /// The name of the remote MCP server. /// 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. - /// HTTP headers to include when calling the remote MCP server. If , an empty dictionary is created. /// or is . /// or is empty or composed entirely of whitespace. - public HostedMcpServerTool(string serverName, string serverAddress, IDictionary? headers = null) + public HostedMcpServerTool(string serverName, string serverAddress) { ServerName = Throw.IfNullOrWhitespace(serverName); ServerAddress = Throw.IfNullOrWhitespace(serverAddress); - Headers = headers ?? new Dictionary(); } /// /// Initializes a new instance of the class. /// /// The name of the remote MCP server. - /// 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. - /// HTTP headers to include when calling the remote MCP server. If , an empty dictionary is created. - /// Any additional properties associated with the tool. - /// or is . - /// or is empty or composed entirely of whitespace. - public HostedMcpServerTool(string serverName, string serverAddress, IDictionary? headers, IReadOnlyDictionary? additionalProperties) - : this(serverName, serverAddress, headers) + /// The URL of the remote MCP server. + /// or is . + /// is empty or composed entirely of whitespace. + /// is not an absolute URL. + public HostedMcpServerTool(string serverName, Uri serverUrl) + : this(serverName, ValidateUrl(serverUrl)) { - _additionalProperties = additionalProperties; } - + /// /// Initializes a new instance of the class. /// /// The name of the remote MCP server. - /// The URL of the remote MCP server. - /// HTTP headers to include when calling the remote MCP server. If , an empty dictionary is created. - /// or is . - /// is empty or composed entirely of whitespace. - /// is not an absolute URL. - public HostedMcpServerTool(string serverName, Uri serverUrl, IDictionary? headers = null) - : this(serverName, ValidateUrl(serverUrl), headers) + /// 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. + /// Any additional properties associated with the tool. + /// or is . + /// or is empty or composed entirely of whitespace. + public HostedMcpServerTool(string serverName, string serverAddress, IReadOnlyDictionary? additionalProperties) + : this(serverName, serverAddress) { + _additionalProperties = additionalProperties; } /// @@ -66,13 +64,12 @@ public HostedMcpServerTool(string serverName, Uri serverUrl, IDictionary /// The name of the remote MCP server. /// The URL of the remote MCP server. - /// HTTP headers to include when calling the remote MCP server. If , an empty dictionary is created. /// Any additional properties associated with the tool. /// or is . /// is empty or composed entirely of whitespace. /// is not an absolute URL. - public HostedMcpServerTool(string serverName, Uri serverUrl, IDictionary? headers, IReadOnlyDictionary? additionalProperties) - : this(serverName, ValidateUrl(serverUrl), headers) + public HostedMcpServerTool(string serverName, Uri serverUrl, IReadOnlyDictionary? additionalProperties) + : this(serverName, ValidateUrl(serverUrl)) { _additionalProperties = additionalProperties; } @@ -115,11 +112,17 @@ private static string ValidateUrl(Uri serverUrl) /// public string? AuthorizationToken { - get; - set + get { - field = value; + if (Headers.TryGetValue("Authorization", out string? value) && value.StartsWith("Bearer ", StringComparison.Ordinal)) + { + return value.Substring("Bearer ".Length); + } + return null; + } + set + { if (value is not null) { Headers["Authorization"] = $"Bearer {value}"; @@ -169,5 +172,9 @@ public string? AuthorizationToken /// The underlying provider is not guaranteed to support or honor the headers. /// /// - public IDictionary Headers { get; } + public IDictionary Headers + { + get => _headers ??= new Dictionary(); + set => _headers = value; + } } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index 3ac0b4aef50..16cea49fc6b 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -595,7 +595,10 @@ void IDisposable.Dispose() responsesMcpTool.ServerDescription = mcpTool.ServerDescription; responsesMcpTool.AuthorizationToken = mcpTool.AuthorizationToken; - responsesMcpTool.Headers = mcpTool.Headers; + if (mcpTool.Headers.Count > 0) + { + responsesMcpTool.Headers = mcpTool.Headers; + } if (mcpTool.AllowedTools is not null) { diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs index eab881b4582..0676e62ba55 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs @@ -32,7 +32,7 @@ public void Constructor_PropsDefault() public void Constructor_AdditionalProperties_String_Roundtrips() { var props = new Dictionary { ["key"] = "value" }; - HostedMcpServerTool tool = new("serverName", "connector_id", null, props); + HostedMcpServerTool tool = new("serverName", "connector_id", props); Assert.Equal("serverName", tool.ServerName); Assert.Equal("connector_id", tool.ServerAddress); @@ -43,7 +43,7 @@ public void Constructor_AdditionalProperties_String_Roundtrips() public void Constructor_AdditionalProperties_Uri_Roundtrips() { var props = new Dictionary { ["key"] = "value" }; - HostedMcpServerTool tool = new("serverName", new Uri("https://localhost/"), null, props); + HostedMcpServerTool tool = new("serverName", new Uri("https://localhost/"), props); Assert.Equal("serverName", tool.ServerName); Assert.Equal("https://localhost/", tool.ServerAddress); @@ -53,7 +53,7 @@ public void Constructor_AdditionalProperties_Uri_Roundtrips() [Fact] public void Constructor_NullAdditionalProperties_UsesEmpty() { - HostedMcpServerTool tool = new("serverName", "connector_id", null, null); + HostedMcpServerTool tool = new("serverName", "connector_id", null); Assert.Empty(tool.AdditionalProperties); } @@ -113,7 +113,8 @@ public void Constructor_WithHeaders_String_Roundtrips() ["Authorization"] = "Bearer token123", ["X-Custom"] = "value1" }; - HostedMcpServerTool tool = new("serverName", "connector_id", headers); + HostedMcpServerTool tool = new("serverName", "connector_id"); + tool.Headers = headers; Assert.Equal("serverName", tool.ServerName); Assert.Equal("connector_id", tool.ServerAddress); @@ -131,7 +132,8 @@ public void Constructor_WithHeaders_Uri_Roundtrips() ["Authorization"] = "Bearer token456", ["X-Custom"] = "value2" }; - HostedMcpServerTool tool = new("serverName", new Uri("https://localhost/"), headers); + HostedMcpServerTool tool = new("serverName", new Uri("https://localhost/")); + tool.Headers = headers; Assert.Equal("serverName", tool.ServerName); Assert.Equal("https://localhost/", tool.ServerAddress); @@ -149,7 +151,8 @@ public void Constructor_WithHeaders_And_AdditionalProperties_String_Roundtrips() ["X-Header"] = "headerValue" }; var props = new Dictionary { ["key"] = "value" }; - HostedMcpServerTool tool = new("serverName", "connector_id", headers, props); + HostedMcpServerTool tool = new("serverName", "connector_id", props); + tool.Headers = headers; Assert.Equal("serverName", tool.ServerName); Assert.Equal("connector_id", tool.ServerAddress); @@ -167,7 +170,8 @@ public void Constructor_WithHeaders_And_AdditionalProperties_Uri_Roundtrips() ["X-Header"] = "headerValue" }; var props = new Dictionary { ["key"] = "value" }; - HostedMcpServerTool tool = new("serverName", new Uri("https://localhost/"), headers, props); + HostedMcpServerTool tool = new("serverName", new Uri("https://localhost/"), props); + tool.Headers = headers; Assert.Equal("serverName", tool.ServerName); Assert.Equal("https://localhost/", tool.ServerAddress); @@ -180,11 +184,11 @@ public void Constructor_WithHeaders_And_AdditionalProperties_Uri_Roundtrips() [Fact] public void Constructor_WithNullHeaders_CreatesEmptyDictionary() { - HostedMcpServerTool tool1 = new("serverName", "connector_id", headers: null); + HostedMcpServerTool tool1 = new("serverName", "connector_id"); Assert.NotNull(tool1.Headers); Assert.Empty(tool1.Headers); - HostedMcpServerTool tool2 = new("serverName", new Uri("https://localhost/"), headers: null); + HostedMcpServerTool tool2 = new("serverName", new Uri("https://localhost/")); Assert.NotNull(tool2.Headers); Assert.Empty(tool2.Headers); } From 898052a73c71ef1b68c8852a6af089611f24e12d Mon Sep 17 00:00:00 2001 From: Eric Chapman Date: Sun, 14 Dec 2025 07:30:55 +0800 Subject: [PATCH 13/17] Fix HostedMcpServerTool headers property and update tests Corrects the implementation of the Headers property in HostedMcpServerTool to properly support get/set semantics. Updates related unit tests to use object initializer syntax for setting headers and adjusts assertions to match the new behavior. Also updates OpenAIResponseClientTests to reflect the correct order and inclusion of authorization and headers in the test payload. --- .../Tools/HostedMcpServerTool.cs | 10 ++++------ .../Tools/HostedMcpServerToolTests.cs | 16 ++++++---------- .../OpenAIResponseClientTests.cs | 5 +++-- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs index b03cff8d44d..8023dbf31a7 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs @@ -16,8 +16,6 @@ public class HostedMcpServerTool : AITool { /// Any additional properties associated with the tool. private IReadOnlyDictionary? _additionalProperties; - // Backing field for the property. - private IDictionary? _headers; /// /// Initializes a new instance of the class. @@ -44,7 +42,7 @@ public HostedMcpServerTool(string serverName, Uri serverUrl) : this(serverName, ValidateUrl(serverUrl)) { } - + /// /// Initializes a new instance of the class. /// @@ -165,7 +163,7 @@ public string? AuthorizationToken public HostedMcpServerToolApprovalMode? ApprovalMode { get; set; } /// - /// Gets a mutable dictionary of HTTP headers to include when calling the remote MCP server. + /// Gets or sets a mutable dictionary of HTTP headers to include when calling the remote MCP server. /// /// /// @@ -174,7 +172,7 @@ public string? AuthorizationToken /// public IDictionary Headers { - get => _headers ??= new Dictionary(); - set => _headers = value; + get => field ??= new Dictionary(); + set; } } 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 0676e62ba55..767cb8de265 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs @@ -99,9 +99,9 @@ public void Constructor_Roundtrips() Assert.Same(customApprovalMode, tool.ApprovalMode); Assert.NotNull(tool.Headers); - Assert.Empty(tool.Headers); - tool.Headers["X-Custom-Header"] = "value1"; Assert.Single(tool.Headers); + tool.Headers["X-Custom-Header"] = "value1"; + Assert.True(tool.Headers.Count == 2); Assert.Equal("value1", tool.Headers["X-Custom-Header"]); } @@ -113,8 +113,7 @@ public void Constructor_WithHeaders_String_Roundtrips() ["Authorization"] = "Bearer token123", ["X-Custom"] = "value1" }; - HostedMcpServerTool tool = new("serverName", "connector_id"); - tool.Headers = headers; + HostedMcpServerTool tool = new("serverName", "connector_id") { Headers = headers }; Assert.Equal("serverName", tool.ServerName); Assert.Equal("connector_id", tool.ServerAddress); @@ -132,8 +131,7 @@ public void Constructor_WithHeaders_Uri_Roundtrips() ["Authorization"] = "Bearer token456", ["X-Custom"] = "value2" }; - HostedMcpServerTool tool = new("serverName", new Uri("https://localhost/")); - tool.Headers = headers; + HostedMcpServerTool tool = new("serverName", new Uri("https://localhost/")) { Headers = headers }; Assert.Equal("serverName", tool.ServerName); Assert.Equal("https://localhost/", tool.ServerAddress); @@ -151,8 +149,7 @@ public void Constructor_WithHeaders_And_AdditionalProperties_String_Roundtrips() ["X-Header"] = "headerValue" }; var props = new Dictionary { ["key"] = "value" }; - HostedMcpServerTool tool = new("serverName", "connector_id", props); - tool.Headers = headers; + HostedMcpServerTool tool = new("serverName", "connector_id", props) { Headers = headers }; Assert.Equal("serverName", tool.ServerName); Assert.Equal("connector_id", tool.ServerAddress); @@ -170,8 +167,7 @@ public void Constructor_WithHeaders_And_AdditionalProperties_Uri_Roundtrips() ["X-Header"] = "headerValue" }; var props = new Dictionary { ["key"] = "value" }; - HostedMcpServerTool tool = new("serverName", new Uri("https://localhost/"), props); - tool.Headers = headers; + HostedMcpServerTool tool = new("serverName", new Uri("https://localhost/"), props) { Headers = headers }; Assert.Equal("serverName", tool.ServerName); Assert.Equal("https://localhost/", tool.ServerAddress); diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs index 0eb205d9f62..d48931e82df 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs @@ -2141,11 +2141,12 @@ public async Task McpToolCall_WithAuthorizationTokenAndCustomHeaders_IncludesInR "type": "mcp", "server_label": "deepwiki", "server_url": "https://mcp.deepwiki.com/mcp", - "require_approval": "never", + "authorization": "test-auth-token-12345", "headers": { "Authorization": "Bearer test-auth-token-12345", "X-Custom-Header": "custom-value" - } + }, + "require_approval": "never" } ], "input": [ From 8ea1dfb029f923afa23f630c77c2645308f8b416 Mon Sep 17 00:00:00 2001 From: Eric Chapman Date: Sun, 14 Dec 2025 14:57:18 +0800 Subject: [PATCH 14/17] Refactor HostedMcpServerTool headers and update MCP tool conversion Refactored HostedMcpServerTool to make Headers a get-only property and removed support for setting headers via constructor. Updated OpenAIResponsesChatClient to prefer headers over AuthorizationToken for HTTP-based MCP tools, and only set AuthorizationToken for connector-based tools. Adjusted and added tests to reflect the new header handling and conversion logic. --- .../Tools/HostedMcpServerTool.cs | 51 +++++++++------- .../OpenAIResponsesChatClient.cs | 24 ++++++-- .../Tools/HostedMcpServerToolTests.cs | 61 ++----------------- .../OpenAIConversionTests.cs | 43 ++++++++++++- .../OpenAIResponseClientTests.cs | 1 - 5 files changed, 94 insertions(+), 86 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs index 8023dbf31a7..b006a9621ae 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs @@ -14,9 +14,15 @@ namespace Microsoft.Extensions.AI; [Experimental("MEAI001")] public class HostedMcpServerTool : AITool { + /// The name of the Authorization header. + private const string AuthorizationHeaderName = "Authorization"; + /// Any additional properties associated with the tool. private IReadOnlyDictionary? _additionalProperties; + /// Backing field for the Headers property. + private Dictionary? _headers; + /// /// Initializes a new instance of the class. /// @@ -30,19 +36,6 @@ public HostedMcpServerTool(string serverName, string serverAddress) ServerAddress = Throw.IfNullOrWhitespace(serverAddress); } - /// - /// 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. - /// is not an absolute URL. - public HostedMcpServerTool(string serverName, Uri serverUrl) - : this(serverName, ValidateUrl(serverUrl)) - { - } - /// /// Initializes a new instance of the class. /// @@ -57,6 +50,19 @@ public HostedMcpServerTool(string serverName, string serverAddress, IReadOnlyDic _additionalProperties = additionalProperties; } + /// + /// 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. + /// is not an absolute URL. + public HostedMcpServerTool(string serverName, Uri serverUrl) + : this(serverName, ValidateUrl(serverUrl)) + { + } + /// /// Initializes a new instance of the class. /// @@ -112,7 +118,10 @@ public string? AuthorizationToken { get { - if (Headers.TryGetValue("Authorization", out string? value) && value.StartsWith("Bearer ", StringComparison.Ordinal)) + if (_headers is not null && + _headers.TryGetValue(AuthorizationHeaderName, out string? value) && + value is not null && + value.StartsWith("Bearer ", StringComparison.Ordinal)) { return value.Substring("Bearer ".Length); } @@ -123,11 +132,11 @@ public string? AuthorizationToken { if (value is not null) { - Headers["Authorization"] = $"Bearer {value}"; + Headers[AuthorizationHeaderName] = $"Bearer {value}"; } - else + else if (_headers is not null) { - _ = Headers.Remove("Authorization"); + _ = _headers.Remove(AuthorizationHeaderName); } } } @@ -163,16 +172,12 @@ public string? AuthorizationToken public HostedMcpServerToolApprovalMode? ApprovalMode { get; set; } /// - /// Gets or sets a mutable dictionary of HTTP headers to include when calling the remote MCP server. + /// Gets a mutable dictionary of HTTP headers to include when calling the remote MCP server. /// /// /// /// The underlying provider is not guaranteed to support or honor the headers. /// /// - public IDictionary Headers - { - get => field ??= new Dictionary(); - set; - } + public IDictionary Headers => _headers ??= new Dictionary(); } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index 16cea49fc6b..f745a0208ca 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -589,15 +589,29 @@ void IDisposable.Dispose() }; case HostedMcpServerTool mcpTool: - McpTool responsesMcpTool = Uri.TryCreate(mcpTool.ServerAddress, UriKind.Absolute, out Uri? serverAddressUrl) ? - new McpTool(mcpTool.ServerName, serverAddressUrl) : + bool isUrl = Uri.TryCreate(mcpTool.ServerAddress, UriKind.Absolute, out Uri? serverAddressUrl); + McpTool responsesMcpTool = isUrl ? + new McpTool(mcpTool.ServerName, serverAddressUrl!) : new McpTool(mcpTool.ServerName, new McpToolConnectorId(mcpTool.ServerAddress)); responsesMcpTool.ServerDescription = mcpTool.ServerDescription; - responsesMcpTool.AuthorizationToken = mcpTool.AuthorizationToken; - if (mcpTool.Headers.Count > 0) + + if (isUrl) + { + // For http: favor headers over authorization token. + if (mcpTool.Headers.Count > 0) + { + responsesMcpTool.Headers = mcpTool.Headers; + } + else + { + responsesMcpTool.AuthorizationToken = mcpTool.AuthorizationToken; + } + } + else { - responsesMcpTool.Headers = mcpTool.Headers; + // For connectors: Only set AuthorizationToken, do not include headers. + responsesMcpTool.AuthorizationToken = mcpTool.AuthorizationToken; } if (mcpTool.AllowedTools is not null) diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs index 767cb8de265..c216400bda5 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs @@ -105,24 +105,6 @@ public void Constructor_Roundtrips() Assert.Equal("value1", tool.Headers["X-Custom-Header"]); } - [Fact] - public void Constructor_WithHeaders_String_Roundtrips() - { - var headers = new Dictionary - { - ["Authorization"] = "Bearer token123", - ["X-Custom"] = "value1" - }; - HostedMcpServerTool tool = new("serverName", "connector_id") { Headers = headers }; - - Assert.Equal("serverName", tool.ServerName); - Assert.Equal("connector_id", tool.ServerAddress); - Assert.Same(headers, tool.Headers); - Assert.Equal(2, tool.Headers.Count); - Assert.Equal("Bearer token123", tool.Headers["Authorization"]); - Assert.Equal("value1", tool.Headers["X-Custom"]); - } - [Fact] public void Constructor_WithHeaders_Uri_Roundtrips() { @@ -131,52 +113,19 @@ public void Constructor_WithHeaders_Uri_Roundtrips() ["Authorization"] = "Bearer token456", ["X-Custom"] = "value2" }; - HostedMcpServerTool tool = new("serverName", new Uri("https://localhost/")) { Headers = headers }; + HostedMcpServerTool tool = new("serverName", new Uri("https://localhost/")); + foreach (KeyValuePair keyValuePair in headers) + { + tool.Headers[keyValuePair.Key] = keyValuePair.Value; + } Assert.Equal("serverName", tool.ServerName); Assert.Equal("https://localhost/", tool.ServerAddress); - Assert.Same(headers, tool.Headers); Assert.Equal(2, tool.Headers.Count); Assert.Equal("Bearer token456", tool.Headers["Authorization"]); Assert.Equal("value2", tool.Headers["X-Custom"]); } - [Fact] - public void Constructor_WithHeaders_And_AdditionalProperties_String_Roundtrips() - { - var headers = new Dictionary - { - ["X-Header"] = "headerValue" - }; - var props = new Dictionary { ["key"] = "value" }; - HostedMcpServerTool tool = new("serverName", "connector_id", props) { Headers = headers }; - - Assert.Equal("serverName", tool.ServerName); - Assert.Equal("connector_id", tool.ServerAddress); - Assert.Same(headers, tool.Headers); - Assert.Same(props, tool.AdditionalProperties); - Assert.Single(tool.Headers); - Assert.Equal("headerValue", tool.Headers["X-Header"]); - } - - [Fact] - public void Constructor_WithHeaders_And_AdditionalProperties_Uri_Roundtrips() - { - var headers = new Dictionary - { - ["X-Header"] = "headerValue" - }; - var props = new Dictionary { ["key"] = "value" }; - HostedMcpServerTool tool = new("serverName", new Uri("https://localhost/"), props) { Headers = headers }; - - Assert.Equal("serverName", tool.ServerName); - Assert.Equal("https://localhost/", tool.ServerAddress); - Assert.Same(headers, tool.Headers); - Assert.Same(props, tool.AdditionalProperties); - Assert.Single(tool.Headers); - Assert.Equal("headerValue", tool.Headers["X-Header"]); - } - [Fact] public void Constructor_WithNullHeaders_CreatesEmptyDictionary() { diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs index 1a711b7417c..1aa7e1e4d0f 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs @@ -393,7 +393,30 @@ public void AsOpenAIResponseTool_WithHostedMcpServerToolWithAuthToken_ProducesVa Assert.NotNull(result); var tool = Assert.IsType(result); - Assert.Equal("test-token", tool.AuthorizationToken); + Assert.Null(tool.AuthorizationToken); + Assert.NotNull(tool.Headers); + Assert.Single(tool.Headers); + Assert.Equal("Bearer test-token", tool.Headers["Authorization"]); + } + + [Fact] + public void AsOpenAIResponseTool_WithHostedMcpServerToolWithAuthTokenAndCustomHeaders_ProducesValidMcpTool() + { + var mcpTool = new HostedMcpServerTool("test-server", "http://localhost:8000") + { + AuthorizationToken = "test-token" + }; + mcpTool.Headers["X-Custom-Header"] = "custom-value"; + + var result = mcpTool.AsOpenAIResponseTool(); + + Assert.NotNull(result); + var tool = Assert.IsType(result); + Assert.Null(tool.AuthorizationToken); + Assert.NotNull(tool.Headers); + Assert.Equal(2, tool.Headers.Count); + Assert.Equal("Bearer test-token", tool.Headers["Authorization"]); + Assert.Equal("custom-value", tool.Headers["X-Custom-Header"]); } [Fact] @@ -490,6 +513,24 @@ public void AsOpenAIResponseTool_WithHostedMcpServerToolWithRequireSpecificAppro Assert.Contains("tool3", tool.ToolCallApprovalPolicy.CustomPolicy.ToolsNeverRequiringApproval.ToolNames); } + [Fact] + public void AsOpenAIResponseTool_WithHostedMcpServerToolConnector_OnlySetsAuthToken() + { + var mcpTool = new HostedMcpServerTool("calendar", "connector_googlecalendar") + { + AuthorizationToken = "connector-token" + }; + + var result = mcpTool.AsOpenAIResponseTool(); + + Assert.NotNull(result); + var tool = Assert.IsType(result); + Assert.Equal("connector-token", tool.AuthorizationToken); + + // For connectors, headers should not be set even though AuthorizationToken adds to Headers internally + Assert.Empty(tool.Headers); + } + [Fact] public void AsOpenAIResponseTool_WithUnknownToolType_ReturnsNull() { diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs index d48931e82df..c84eba42fa1 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs @@ -2141,7 +2141,6 @@ public async Task McpToolCall_WithAuthorizationTokenAndCustomHeaders_IncludesInR "type": "mcp", "server_label": "deepwiki", "server_url": "https://mcp.deepwiki.com/mcp", - "authorization": "test-auth-token-12345", "headers": { "Authorization": "Bearer test-auth-token-12345", "X-Custom-Header": "custom-value" From ccf75dc2b2fae1529345ad26ae469b2a4f18f168 Mon Sep 17 00:00:00 2001 From: Eric Chapman Date: Mon, 15 Dec 2025 06:44:33 +0800 Subject: [PATCH 15/17] Add CP0002 suppressions for HostedMcpServerTool.set_Headers Added baseline suppressions for diagnostic CP0002 on the HostedMcpServerTool.set_Headers method across net462, net8.0, net9.0, and netstandard2.0 target frameworks to maintain compatibility. --- .../CompatibilitySuppressions.xml | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml index 4990241bab5..a6a2edfc28c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml @@ -29,6 +29,13 @@ lib/net462/Microsoft.Extensions.AI.Abstractions.dll true + + CP0002 + M:Microsoft.Extensions.AI.HostedMcpServerTool.set_Headers(System.Collections.Generic.IDictionary{System.String,System.String}) + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + CP0002 M:Microsoft.Extensions.AI.ChatOptions.get_ContinuationToken @@ -57,6 +64,13 @@ lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll true + + CP0002 + M:Microsoft.Extensions.AI.HostedMcpServerTool.set_Headers(System.Collections.Generic.IDictionary{System.String,System.String}) + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + CP0002 M:Microsoft.Extensions.AI.ChatOptions.get_ContinuationToken @@ -85,6 +99,13 @@ lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll true + + CP0002 + M:Microsoft.Extensions.AI.HostedMcpServerTool.set_Headers(System.Collections.Generic.IDictionary{System.String,System.String}) + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + CP0002 M:Microsoft.Extensions.AI.ChatOptions.get_ContinuationToken @@ -113,4 +134,11 @@ lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll true + + CP0002 + M:Microsoft.Extensions.AI.HostedMcpServerTool.set_Headers(System.Collections.Generic.IDictionary{System.String,System.String}) + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + true + From 7909717d44e74054062a2b696cf93b30ab884c92 Mon Sep 17 00:00:00 2001 From: Eric Chapman Date: Mon, 15 Dec 2025 13:51:59 +0800 Subject: [PATCH 16/17] Remove redundant AuthorizationToken assignment Eliminated unnecessary assignment of AuthorizationToken in OpenAIResponsesChatClient when copying MCP tool properties. Also improved a comment in HostedMcpServerTool for clarity. --- .../Tools/HostedMcpServerTool.cs | 2 +- .../OpenAIResponsesChatClient.cs | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs index b006a9621ae..f038e0aa994 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs @@ -20,7 +20,7 @@ public class HostedMcpServerTool : AITool /// Any additional properties associated with the tool. private IReadOnlyDictionary? _additionalProperties; - /// Backing field for the Headers property. + /// Lazily-initialized collection of headers to include when calling the remote MCP server. private Dictionary? _headers; /// diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index f745a0208ca..b9aca3161a0 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -603,10 +603,6 @@ void IDisposable.Dispose() { responsesMcpTool.Headers = mcpTool.Headers; } - else - { - responsesMcpTool.AuthorizationToken = mcpTool.AuthorizationToken; - } } else { From 0a43ed01a5c7b99da9ad64a50c5e4a0a0862378a Mon Sep 17 00:00:00 2001 From: Eric Chapman Date: Tue, 16 Dec 2025 05:43:28 +0800 Subject: [PATCH 17/17] Improve AuthorizationToken getter and add null header test Refactored the AuthorizationToken property to simplify null checks and logic. Added unit tests to verify correct behavior when the Authorization header is null and to assert the returned token value in various scenarios. --- .../Tools/HostedMcpServerTool.cs | 6 ++---- .../Tools/HostedMcpServerToolTests.cs | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs index f038e0aa994..95d2739fd9f 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs @@ -118,10 +118,8 @@ public string? AuthorizationToken { get { - if (_headers is not null && - _headers.TryGetValue(AuthorizationHeaderName, out string? value) && - value is not null && - value.StartsWith("Bearer ", StringComparison.Ordinal)) + if (_headers?.TryGetValue(AuthorizationHeaderName, out string? value) is true && + value?.StartsWith("Bearer ", StringComparison.Ordinal) is true) { return value.Substring("Bearer ".Length); } 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 c216400bda5..56c04ce1dfa 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs @@ -123,6 +123,7 @@ public void Constructor_WithHeaders_Uri_Roundtrips() Assert.Equal("https://localhost/", tool.ServerAddress); Assert.Equal(2, tool.Headers.Count); Assert.Equal("Bearer token456", tool.Headers["Authorization"]); + Assert.Equal("token456", tool.AuthorizationToken); Assert.Equal("value2", tool.Headers["X-Custom"]); } @@ -150,6 +151,7 @@ public void AuthorizationToken_And_Headers_NoOrderingIssues() Assert.Equal(2, tool1.Headers.Count); Assert.Equal("Bearer token123", tool1.Headers["Authorization"]); + Assert.Equal("token123", tool1.AuthorizationToken); Assert.Equal("value1", tool1.Headers["X-Custom"]); // Verify that adding to Headers followed by setting AuthorizationToken works the same @@ -159,15 +161,29 @@ public void AuthorizationToken_And_Headers_NoOrderingIssues() Assert.Equal(2, tool2.Headers.Count); Assert.Equal("Bearer token123", tool2.Headers["Authorization"]); + Assert.Equal("token123", tool2.AuthorizationToken); Assert.Equal("value1", tool2.Headers["X-Custom"]); // Verify setting AuthorizationToken to null removes only Authorization header tool2.AuthorizationToken = null; Assert.Single(tool2.Headers); Assert.False(tool2.Headers.ContainsKey("Authorization")); + Assert.Null(tool2.AuthorizationToken); Assert.Equal("value1", tool2.Headers["X-Custom"]); } + [Fact] + public void Headers_WithNullAuthorization() + { + var tool = new HostedMcpServerTool("server", "https://localhost/"); + tool.Headers["Authorization"] = null!; + tool.Headers["X-Custom"] = "value1"; + Assert.Equal(2, tool.Headers.Count); + Assert.Null(tool.Headers["Authorization"]); + Assert.Null(tool.AuthorizationToken); + Assert.Equal("value1", tool.Headers["X-Custom"]); + } + [Fact] public void Constructor_Throws() {