diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml
index 993fd3d3ff0..a6a2edfc28c 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
@@ -64,13 +57,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
@@ -106,13 +92,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
@@ -148,13 +127,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
@@ -169,4 +141,4 @@
lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll
true
-
\ No newline at end of file
+
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs
index fbc80fe4d59..95d2739fd9f 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;
+ /// Lazily-initialized collection of headers to include when calling the remote MCP server.
+ private Dictionary? _headers;
+
///
/// Initializes a new instance of the class.
///
@@ -103,7 +109,35 @@ 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
+ {
+ if (_headers?.TryGetValue(AuthorizationHeaderName, out string? value) is true &&
+ value?.StartsWith("Bearer ", StringComparison.Ordinal) is true)
+ {
+ return value.Substring("Bearer ".Length);
+ }
+
+ return null;
+ }
+ set
+ {
+ if (value is not null)
+ {
+ Headers[AuthorizationHeaderName] = $"Bearer {value}";
+ }
+ else if (_headers is not null)
+ {
+ _ = _headers.Remove(AuthorizationHeaderName);
+ }
+ }
+ }
///
/// Gets or sets the description of the remote MCP server, used to provide more context to the AI service.
@@ -134,4 +168,14 @@ private static string ValidateUrl(Uri serverUrl)
///
///
public HostedMcpServerToolApprovalMode? ApprovalMode { get; set; }
+
+ ///
+ /// 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 => _headers ??= new Dictionary();
}
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs
index e6359cbdd7a..b9aca3161a0 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs
@@ -589,12 +589,26 @@ 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 (isUrl)
+ {
+ // For http: favor headers over authorization token.
+ if (mcpTool.Headers.Count > 0)
+ {
+ responsesMcpTool.Headers = mcpTool.Headers;
+ }
+ }
+ else
+ {
+ // 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 aa23cfd3ff4..56c04ce1dfa 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs
@@ -24,6 +24,8 @@ 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]
@@ -95,6 +97,91 @@ public void Constructor_Roundtrips()
var customApprovalMode = new HostedMcpServerToolRequireSpecificApprovalMode(["tool1"], ["tool2"]);
tool.ApprovalMode = customApprovalMode;
Assert.Same(customApprovalMode, tool.ApprovalMode);
+
+ Assert.NotNull(tool.Headers);
+ Assert.Single(tool.Headers);
+ tool.Headers["X-Custom-Header"] = "value1";
+ Assert.True(tool.Headers.Count == 2);
+ Assert.Equal("value1", tool.Headers["X-Custom-Header"]);
+ }
+
+ [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/"));
+ foreach (KeyValuePair keyValuePair in headers)
+ {
+ tool.Headers[keyValuePair.Key] = keyValuePair.Value;
+ }
+
+ Assert.Equal("serverName", tool.ServerName);
+ 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"]);
+ }
+
+ [Fact]
+ public void Constructor_WithNullHeaders_CreatesEmptyDictionary()
+ {
+ HostedMcpServerTool tool1 = new("serverName", "connector_id");
+ Assert.NotNull(tool1.Headers);
+ Assert.Empty(tool1.Headers);
+
+ HostedMcpServerTool tool2 = new("serverName", new Uri("https://localhost/"));
+ 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("token123", tool1.AuthorizationToken);
+ 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("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]
@@ -111,3 +198,4 @@ public void Constructor_Throws()
Assert.Throws("serverUrl", () => new HostedMcpServerTool("name", (Uri)null!));
}
}
+
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 1e19466ee7f..c84eba42fa1 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",
+ "headers": {
+ "Authorization": "Bearer test-auth-token-12345",
+ "X-Custom-Header": "custom-value"
+ },
+ "require_approval": "never"
+ }
+ ],
+ "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()
{