diff --git a/src/ModelContextProtocol/McpChatClientBuilderExtensions.cs b/src/ModelContextProtocol/McpChatClientBuilderExtensions.cs
new file mode 100644
index 000000000..7d04bfb00
--- /dev/null
+++ b/src/ModelContextProtocol/McpChatClientBuilderExtensions.cs
@@ -0,0 +1,230 @@
+using System.Collections.Concurrent;
+using System.Runtime.CompilerServices;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using ModelContextProtocol.Client;
+#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
+
+namespace ModelContextProtocol;
+
+///
+/// Extension methods for adding MCP client support to chat clients.
+///
+public static class McpChatClientBuilderExtensions
+{
+ ///
+ /// Adds a chat client to the chat client pipeline that creates an for each
+ /// in and augments it with the tools from MCP servers as instances.
+ ///
+ /// The to configure.
+ /// The to use, or to create a new instance.
+ /// The to use, or to resolve from services.
+ /// The for method chaining.
+ ///
+ ///
+ /// When a HostedMcpServerTool is encountered in the tools collection, the client
+ /// connects to the MCP server, retrieves available tools, and expands them into callable AI functions.
+ /// Connections are cached by server address to avoid redundant connections.
+ ///
+ ///
+ /// Use this method as an alternative when working with chat providers that don't have built-in support for hosted MCP servers.
+ ///
+ ///
+ public static ChatClientBuilder UseMcpClient(
+ this ChatClientBuilder builder,
+ HttpClient? httpClient = null,
+ ILoggerFactory? loggerFactory = null)
+ {
+ return builder.Use((innerClient, services) =>
+ {
+ loggerFactory ??= (ILoggerFactory)services.GetService(typeof(ILoggerFactory))!;
+ var chatClient = new McpChatClient(innerClient, httpClient, loggerFactory);
+ return chatClient;
+ });
+ }
+
+ private class McpChatClient : DelegatingChatClient
+ {
+ private readonly ILoggerFactory? _loggerFactory;
+ private readonly ILogger _logger;
+ private readonly HttpClient _httpClient;
+ private readonly bool _ownsHttpClient;
+ private ConcurrentDictionary>? _mcpClientTasks = null;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The underlying , or the next instance in a chain of clients.
+ /// An optional to use when connecting to MCP servers. If not provided, a new instance will be created.
+ /// An to use for logging information about function invocation.
+ public McpChatClient(IChatClient innerClient, HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null)
+ : base(innerClient)
+ {
+ _loggerFactory = loggerFactory;
+ _logger = (ILogger?)loggerFactory?.CreateLogger() ?? NullLogger.Instance;
+ _httpClient = httpClient ?? new HttpClient();
+ _ownsHttpClient = httpClient is null;
+ }
+
+ ///
+ public override async Task GetResponseAsync(
+ IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ if (options?.Tools is { Count: > 0 })
+ {
+ var downstreamTools = await BuildDownstreamAIToolsAsync(options.Tools, cancellationToken).ConfigureAwait(false);
+ options = options.Clone();
+ options.Tools = downstreamTools;
+ }
+
+ return await base.GetResponseAsync(messages, options, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ public override async IAsyncEnumerable GetStreamingResponseAsync(IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ if (options?.Tools is { Count: > 0 })
+ {
+ var downstreamTools = await BuildDownstreamAIToolsAsync(options.Tools, cancellationToken).ConfigureAwait(false);
+ options = options.Clone();
+ options.Tools = downstreamTools;
+ }
+
+ await foreach (var update in base.GetStreamingResponseAsync(messages, options, cancellationToken).ConfigureAwait(false))
+ {
+ yield return update;
+ }
+ }
+
+ private async Task?> BuildDownstreamAIToolsAsync(IList? inputTools, CancellationToken cancellationToken)
+ {
+ List? downstreamTools = null;
+ foreach (var tool in inputTools ?? [])
+ {
+ if (tool is not HostedMcpServerTool mcpTool)
+ {
+ // For other tools, we want to keep them in the list of tools.
+ downstreamTools ??= new List();
+ downstreamTools.Add(tool);
+ continue;
+ }
+
+ if (!Uri.TryCreate(mcpTool.ServerAddress, UriKind.Absolute, out var parsedAddress) ||
+ (parsedAddress.Scheme != Uri.UriSchemeHttp && parsedAddress.Scheme != Uri.UriSchemeHttps))
+ {
+ throw new InvalidOperationException(
+ $"MCP server address must be an absolute HTTP or HTTPS URI. Invalid address: '{mcpTool.ServerAddress}'");
+ }
+
+ // List all MCP functions from the specified MCP server.
+ // This will need some caching in a real-world scenario to avoid repeated calls.
+ var mcpClient = await CreateMcpClientAsync(parsedAddress, mcpTool.ServerName, mcpTool.AuthorizationToken).ConfigureAwait(false);
+ var mcpFunctions = await mcpClient.ListToolsAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
+
+ // Add the listed functions to our list of tools we'll pass to the inner client.
+ foreach (var mcpFunction in mcpFunctions)
+ {
+ if (mcpTool.AllowedTools is not null && !mcpTool.AllowedTools.Contains(mcpFunction.Name))
+ {
+ _logger.LogInformation("MCP function '{FunctionName}' is not allowed by the tool configuration.", mcpFunction.Name);
+ continue;
+ }
+
+ downstreamTools ??= new List();
+ switch (mcpTool.ApprovalMode)
+ {
+ case HostedMcpServerToolAlwaysRequireApprovalMode alwaysRequireApproval:
+ downstreamTools.Add(new ApprovalRequiredAIFunction(mcpFunction));
+ break;
+ case HostedMcpServerToolNeverRequireApprovalMode neverRequireApproval:
+ downstreamTools.Add(mcpFunction);
+ break;
+ case HostedMcpServerToolRequireSpecificApprovalMode specificApprovalMode when specificApprovalMode.AlwaysRequireApprovalToolNames?.Contains(mcpFunction.Name) is true:
+ downstreamTools.Add(new ApprovalRequiredAIFunction(mcpFunction));
+ break;
+ case HostedMcpServerToolRequireSpecificApprovalMode specificApprovalMode when specificApprovalMode.NeverRequireApprovalToolNames?.Contains(mcpFunction.Name) is true:
+ downstreamTools.Add(mcpFunction);
+ break;
+ default:
+ // Default to always require approval if no specific mode is set.
+ downstreamTools.Add(new ApprovalRequiredAIFunction(mcpFunction));
+ break;
+ }
+ }
+ }
+
+ return downstreamTools;
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ // Dispose of the HTTP client if it was created by this client.
+ if (_ownsHttpClient)
+ {
+ _httpClient?.Dispose();
+ }
+
+ if (_mcpClientTasks is not null)
+ {
+ // Dispose of all cached MCP clients.
+ foreach (var clientTask in _mcpClientTasks.Values)
+ {
+#if NETSTANDARD2_0
+ if (clientTask.Status == TaskStatus.RanToCompletion)
+#else
+ if (clientTask.IsCompletedSuccessfully)
+#endif
+ {
+ _ = clientTask.Result.DisposeAsync();
+ }
+ }
+
+ _mcpClientTasks.Clear();
+ }
+ }
+
+ base.Dispose(disposing);
+ }
+
+ private Task CreateMcpClientAsync(Uri serverAddress, string serverName, string? authorizationToken)
+ {
+ if (_mcpClientTasks is null)
+ {
+ _mcpClientTasks = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase);
+ }
+
+ // Note: We don't pass cancellationToken to the factory because the cached task should not be tied to any single caller's cancellation token.
+ // Instead, callers can cancel waiting for the task, but the connection attempt itself will complete independently.
+ return _mcpClientTasks.GetOrAdd(serverAddress.ToString(), _ => CreateMcpClientCoreAsync(serverAddress, serverName, authorizationToken, CancellationToken.None));
+ }
+
+ private async Task CreateMcpClientCoreAsync(Uri serverAddress, string serverName, string? authorizationToken, CancellationToken cancellationToken)
+ {
+ var serverAddressKey = serverAddress.ToString();
+ try
+ {
+ var transport = new HttpClientTransport(new HttpClientTransportOptions
+ {
+ Endpoint = serverAddress,
+ Name = serverName,
+ AdditionalHeaders = authorizationToken is not null
+ // Update to pass all headers once https://github.com/dotnet/extensions/pull/7053 is available.
+ ? new Dictionary() { { "Authorization", $"Bearer {authorizationToken}" } }
+ : null,
+ }, _httpClient, _loggerFactory);
+
+ return await McpClient.CreateAsync(transport, cancellationToken: cancellationToken).ConfigureAwait(false);
+ }
+ catch
+ {
+ // Remove the failed task from cache so subsequent requests can retry
+ _mcpClientTasks?.TryRemove(serverAddressKey, out _);
+ throw;
+ }
+ }
+ }
+}
diff --git a/src/ModelContextProtocol/ModelContextProtocol.csproj b/src/ModelContextProtocol/ModelContextProtocol.csproj
index b69108ab2..fe394a056 100644
--- a/src/ModelContextProtocol/ModelContextProtocol.csproj
+++ b/src/ModelContextProtocol/ModelContextProtocol.csproj
@@ -23,6 +23,7 @@
+
diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs
index ce4f3b56a..562bedae8 100644
--- a/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs
+++ b/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs
@@ -4,11 +4,11 @@
namespace ModelContextProtocol.AspNetCore.Tests;
-public abstract class HttpServerIntegrationTests : LoggedTest, IClassFixture
+public abstract class HttpServerIntegrationTests : LoggedTest, IClassFixture
{
- protected readonly SseServerIntegrationTestFixture _fixture;
+ protected readonly SseServerWithXunitLoggerFixture _fixture;
- public HttpServerIntegrationTests(SseServerIntegrationTestFixture fixture, ITestOutputHelper testOutputHelper)
+ public HttpServerIntegrationTests(SseServerWithXunitLoggerFixture fixture, ITestOutputHelper testOutputHelper)
: base(testOutputHelper)
{
_fixture = fixture;
diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/SseServerIntegrationTestFixture.cs b/tests/ModelContextProtocol.AspNetCore.Tests/SseServerIntegrationTestFixture.cs
index c382c4385..7044acd30 100644
--- a/tests/ModelContextProtocol.AspNetCore.Tests/SseServerIntegrationTestFixture.cs
+++ b/tests/ModelContextProtocol.AspNetCore.Tests/SseServerIntegrationTestFixture.cs
@@ -7,23 +7,18 @@
namespace ModelContextProtocol.AspNetCore.Tests;
-public class SseServerIntegrationTestFixture : IAsyncDisposable
+public abstract class SseServerIntegrationTestFixture : IAsyncDisposable
{
private readonly KestrelInMemoryTransport _inMemoryTransport = new();
-
private readonly Task _serverTask;
private readonly CancellationTokenSource _stopCts = new();
- // XUnit's ITestOutputHelper is created per test, while this fixture is used for
- // multiple tests, so this dispatches the output to the current test.
- private readonly DelegatingTestOutputHelper _delegatingTestOutputHelper = new();
-
private HttpClientTransportOptions DefaultTransportOptions { get; set; } = new()
{
Endpoint = new("http://localhost:5000/"),
};
- public SseServerIntegrationTestFixture()
+ protected SseServerIntegrationTestFixture()
{
var socketsHttpHandler = new SocketsHttpHandler
{
@@ -39,8 +34,10 @@ public SseServerIntegrationTestFixture()
BaseAddress = new("http://localhost:5000/"),
};
- _serverTask = Program.MainAsync([], new XunitLoggerProvider(_delegatingTestOutputHelper), _inMemoryTransport, _stopCts.Token);
+ _serverTask = Program.MainAsync([], CreateLoggerProvider(), _inMemoryTransport, _stopCts.Token);
}
+
+ protected abstract ILoggerProvider CreateLoggerProvider();
public HttpClient HttpClient { get; }
@@ -53,21 +50,17 @@ public Task ConnectMcpClientAsync(McpClientOptions? options, ILoggerF
TestContext.Current.CancellationToken);
}
- public void Initialize(ITestOutputHelper output, HttpClientTransportOptions clientTransportOptions)
+ public virtual void Initialize(ITestOutputHelper output, HttpClientTransportOptions clientTransportOptions)
{
- _delegatingTestOutputHelper.CurrentTestOutputHelper = output;
DefaultTransportOptions = clientTransportOptions;
}
- public void TestCompleted()
+ public virtual void TestCompleted()
{
- _delegatingTestOutputHelper.CurrentTestOutputHelper = null;
}
- public async ValueTask DisposeAsync()
+ public virtual async ValueTask DisposeAsync()
{
- _delegatingTestOutputHelper.CurrentTestOutputHelper = null;
-
HttpClient.Dispose();
_stopCts.Cancel();
@@ -82,3 +75,49 @@ public async ValueTask DisposeAsync()
_stopCts.Dispose();
}
}
+
+///
+/// SSE server fixture that routes logs to xUnit test output.
+///
+public class SseServerWithXunitLoggerFixture : SseServerIntegrationTestFixture
+{
+ // XUnit's ITestOutputHelper is created per test, while this fixture is used for
+ // multiple tests, so this dispatches the output to the current test.
+ private readonly DelegatingTestOutputHelper _delegatingTestOutputHelper = new();
+
+ protected override ILoggerProvider CreateLoggerProvider()
+ => new XunitLoggerProvider(_delegatingTestOutputHelper);
+
+ public override void Initialize(ITestOutputHelper output, HttpClientTransportOptions clientTransportOptions)
+ {
+ _delegatingTestOutputHelper.CurrentTestOutputHelper = output;
+ base.Initialize(output, clientTransportOptions);
+ }
+
+ public override void TestCompleted()
+ {
+ _delegatingTestOutputHelper.CurrentTestOutputHelper = null;
+ base.TestCompleted();
+ }
+
+ public override async ValueTask DisposeAsync()
+ {
+ _delegatingTestOutputHelper.CurrentTestOutputHelper = null;
+ await base.DisposeAsync();
+ }
+}
+
+///
+/// Fixture for tests that need to inspect server logs using MockLoggerProvider.
+/// Use for tests that just need xUnit output.
+///
+public class SseServerWithMockLoggerFixture : SseServerIntegrationTestFixture
+{
+ private readonly MockLoggerProvider _mockLoggerProvider = new();
+
+ protected override ILoggerProvider CreateLoggerProvider()
+ => _mockLoggerProvider;
+
+ public IEnumerable<(string Category, LogLevel LogLevel, EventId EventId, string Message, Exception? Exception)> ServerLogs
+ => _mockLoggerProvider.LogMessages;
+}
diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/SseServerIntegrationTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/SseServerIntegrationTests.cs
index eb7db0110..5339235af 100644
--- a/tests/ModelContextProtocol.AspNetCore.Tests/SseServerIntegrationTests.cs
+++ b/tests/ModelContextProtocol.AspNetCore.Tests/SseServerIntegrationTests.cs
@@ -4,7 +4,7 @@
namespace ModelContextProtocol.AspNetCore.Tests;
-public class SseServerIntegrationTests(SseServerIntegrationTestFixture fixture, ITestOutputHelper testOutputHelper)
+public class SseServerIntegrationTests(SseServerWithXunitLoggerFixture fixture, ITestOutputHelper testOutputHelper)
: HttpServerIntegrationTests(fixture, testOutputHelper)
{
diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/StatelessServerIntegrationTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/StatelessServerIntegrationTests.cs
index 2ce63a1bc..6937e4be6 100644
--- a/tests/ModelContextProtocol.AspNetCore.Tests/StatelessServerIntegrationTests.cs
+++ b/tests/ModelContextProtocol.AspNetCore.Tests/StatelessServerIntegrationTests.cs
@@ -2,7 +2,7 @@
namespace ModelContextProtocol.AspNetCore.Tests;
-public class StatelessServerIntegrationTests(SseServerIntegrationTestFixture fixture, ITestOutputHelper testOutputHelper)
+public class StatelessServerIntegrationTests(SseServerWithXunitLoggerFixture fixture, ITestOutputHelper testOutputHelper)
: StreamableHttpServerIntegrationTests(fixture, testOutputHelper)
{
protected override HttpClientTransportOptions ClientTransportOptions => new()
diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerIntegrationTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerIntegrationTests.cs
index b2b0b5499..63c6dc77b 100644
--- a/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerIntegrationTests.cs
+++ b/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerIntegrationTests.cs
@@ -3,7 +3,7 @@
namespace ModelContextProtocol.AspNetCore.Tests;
-public class StreamableHttpServerIntegrationTests(SseServerIntegrationTestFixture fixture, ITestOutputHelper testOutputHelper)
+public class StreamableHttpServerIntegrationTests(SseServerWithXunitLoggerFixture fixture, ITestOutputHelper testOutputHelper)
: HttpServerIntegrationTests(fixture, testOutputHelper)
{
diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/UseMcpClientWithTestSseServerTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/UseMcpClientWithTestSseServerTests.cs
new file mode 100644
index 000000000..f4a3ae023
--- /dev/null
+++ b/tests/ModelContextProtocol.AspNetCore.Tests/UseMcpClientWithTestSseServerTests.cs
@@ -0,0 +1,332 @@
+using System.Runtime.CompilerServices;
+using Microsoft.Extensions.AI;
+using ModelContextProtocol.Client;
+using ModelContextProtocol.Tests.Utils;
+using Moq;
+#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
+
+namespace ModelContextProtocol.AspNetCore.Tests;
+
+public class UseMcpClientWithTestSseServerTests : LoggedTest, IClassFixture
+{
+ private readonly HttpClientTransportOptions _transportOptions;
+ private readonly SseServerWithMockLoggerFixture _fixture;
+
+ public UseMcpClientWithTestSseServerTests(SseServerWithMockLoggerFixture fixture, ITestOutputHelper testOutputHelper)
+ : base(testOutputHelper)
+ {
+ _transportOptions = new HttpClientTransportOptions()
+ {
+ Endpoint = new("http://localhost:5000/sse"),
+ Name = "TestSseServer",
+ };
+
+ _fixture = fixture;
+ _fixture.Initialize(testOutputHelper, _transportOptions);
+ }
+
+ public override void Dispose()
+ {
+ _fixture.TestCompleted();
+ base.Dispose();
+ }
+
+ private sealed class CallbackState
+ {
+ public ChatOptions? CapturedOptions { get; set; }
+ }
+
+ private IChatClient CreateTestChatClient(out CallbackState callbackState)
+ {
+ var state = new CallbackState();
+
+ var mockInnerClient = new Mock();
+ mockInnerClient
+ .Setup(c => c.GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny()))
+ .Callback, ChatOptions?, CancellationToken>(
+ (msgs, opts, ct) => state.CapturedOptions = opts)
+ .ReturnsAsync(new ChatResponse([new ChatMessage(ChatRole.Assistant, "Dummy response")]));
+
+ mockInnerClient
+ .Setup(c => c.GetStreamingResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny()))
+ .Callback, ChatOptions?, CancellationToken>(
+ (msgs, opts, ct) => state.CapturedOptions = opts)
+ .Returns(GetStreamingResponseAsync());
+
+ callbackState = state;
+ return mockInnerClient.Object.AsBuilder()
+ .UseMcpClient(_fixture.HttpClient, LoggerFactory)
+ .Build();
+
+ static async IAsyncEnumerable GetStreamingResponseAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ yield return new ChatResponseUpdate(ChatRole.Assistant, "Dummy response");
+ }
+ }
+
+ private async Task GetResponseAsync(IChatClient client, ChatOptions options, bool streaming)
+ {
+ if (streaming)
+ {
+ await foreach (var _ in client.GetStreamingResponseAsync("Test message", options, TestContext.Current.CancellationToken))
+ { }
+ }
+ else
+ {
+ _ = await client.GetResponseAsync("Test message", options, TestContext.Current.CancellationToken);
+ }
+ }
+
+ [Theory]
+ [InlineData(false, false)]
+ [InlineData(false, true)]
+ [InlineData(true, false)]
+ [InlineData(true, true)]
+ public async Task UseMcpClient_ShouldProduceTools(bool streaming, bool useUrl)
+ {
+ // Arrange
+ IChatClient sut = CreateTestChatClient(out var callbackState);
+ var mcpTool = useUrl ?
+ new HostedMcpServerTool(_transportOptions.Name!, _transportOptions.Endpoint) :
+ new HostedMcpServerTool(_transportOptions.Name!, _transportOptions.Endpoint.ToString());
+ var options = new ChatOptions { Tools = [mcpTool] };
+
+ // Act
+ await GetResponseAsync(sut, options, streaming);
+
+ // Assert
+ Assert.NotNull(callbackState.CapturedOptions);
+ Assert.NotNull(callbackState.CapturedOptions.Tools);
+ var toolNames = callbackState.CapturedOptions.Tools.Select(t => t.Name).ToList();
+ Assert.Equal(3, toolNames.Count);
+ Assert.Contains("echo", toolNames);
+ Assert.Contains("echoSessionId", toolNames);
+ Assert.Contains("sampleLLM", toolNames);
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task UseMcpClient_DoesNotConflictWithRegularTools(bool streaming)
+ {
+ // Arrange
+ IChatClient sut = CreateTestChatClient(out var callbackState);
+ var regularTool = AIFunctionFactory.Create(() => "regular tool result", "RegularTool");
+ var mcpTool = new HostedMcpServerTool(_transportOptions.Name!, _transportOptions.Endpoint);
+ var options = new ChatOptions
+ {
+ Tools =
+ [
+ regularTool,
+ mcpTool
+ ]
+ };
+
+ // Act
+ await GetResponseAsync(sut, options, streaming);
+
+ // Assert
+ Assert.NotNull(callbackState.CapturedOptions);
+ Assert.NotNull(callbackState.CapturedOptions.Tools);
+ var toolNames = callbackState.CapturedOptions.Tools.Select(t => t.Name).ToList();
+ Assert.Equal(4, toolNames.Count);
+ Assert.Contains("RegularTool", toolNames);
+ Assert.Contains("echo", toolNames);
+ Assert.Contains("echoSessionId", toolNames);
+ Assert.Contains("sampleLLM", toolNames);
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task UseMcpClient_AuthorizationTokenHeaderFlowsCorrectly(bool streaming)
+ {
+ // Arrange
+ const string testToken = "test-bearer-token-12345";
+ IChatClient sut = CreateTestChatClient(out var callbackState);
+ var mcpTool = new HostedMcpServerTool(_transportOptions.Name!, _transportOptions.Endpoint)
+ {
+ AuthorizationToken = testToken
+ };
+ var options = new ChatOptions
+ {
+ Tools = [mcpTool]
+ };
+
+ // Act
+ await GetResponseAsync(sut, options, streaming);
+
+ // Assert
+ Assert.NotNull(callbackState.CapturedOptions);
+ Assert.NotNull(callbackState.CapturedOptions.Tools);
+ var toolNames = callbackState.CapturedOptions.Tools.Select(t => t.Name).ToList();
+ Assert.Equal(3, toolNames.Count);
+ Assert.Contains("echo", toolNames);
+ Assert.Contains("echoSessionId", toolNames);
+ Assert.Contains("sampleLLM", toolNames);
+ // We set TestSseServer to log IHeaderDictionary as json.
+ Assert.Contains(_fixture.ServerLogs, log => log.Message.Contains(@"""Authorization"":[""Bearer test-bearer-token-12345""]"));
+ }
+
+ public static IEnumerable