From f17e941b5e3fbcdb911038c3f5137ba71c4e0cf1 Mon Sep 17 00:00:00 2001 From: "mars.yu" Date: Thu, 27 Aug 2026 16:39:44 +0800 Subject: [PATCH] Let the host adjust the headers an MCP connection is opened with The headers came straight from MCP:McpServerConfigs, so every call to a server went out as whatever fixed credential was configured for it. A host that wants to call a server as the user driving the conversation had no way in: GetMcpClientAsync is not virtual, and the transport is built inline. IMcpClientHeaderProvider is an optional hook, resolved with GetService and asked about every server. Nothing registers it by default, and an implementation is free to answer with what it was given, so a host without one -- or with one that does not recognise a server -- gets the configured headers back untouched. The headers passed to the provider belong to the McpSettings instance captured for the lifetime of the process, which is why the contract asks implementations to copy rather than write in place. Co-Authored-By: Claude Opus 5 --- .../MCP/Services/IMcpClientHeaderProvider.cs | 30 +++++++++++++++++++ .../MCP/Managers/McpClientManager.cs | 19 ++++++++++-- 2 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 src/Infrastructure/BotSharp.Abstraction/MCP/Services/IMcpClientHeaderProvider.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/MCP/Services/IMcpClientHeaderProvider.cs b/src/Infrastructure/BotSharp.Abstraction/MCP/Services/IMcpClientHeaderProvider.cs new file mode 100644 index 000000000..fd591a660 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/MCP/Services/IMcpClientHeaderProvider.cs @@ -0,0 +1,30 @@ +namespace BotSharp.Abstraction.MCP.Services; + +/// +/// An optional host hook over the HTTP headers used to open one MCP server connection. +/// +/// +/// Nothing registers this by default. With no implementation registered the headers configured +/// under MCP:McpServerConfigs are used verbatim, which is the only behaviour there was +/// before the hook existed — a host that does not implement it sees no change at all. +/// +/// It exists so a host can call a server as whoever is driving the conversation instead of with +/// one fixed credential. That decision belongs to the host: it is the only side that knows what +/// a caller's credential is and which servers may be shown it. +/// +/// +public interface IMcpClientHeaderProvider +{ + /// + /// Answers the headers to send to . + /// + /// + /// The headers from configuration. This dictionary is shared for the lifetime of the process, + /// so an implementation that changes a header MUST copy it rather than write into it. + /// + /// + /// The headers to send. Returning unchanged is the no-op answer, + /// and is the answer expected for any server the implementation does not recognise. + /// + Dictionary? GetHeaders(string serverId, Dictionary? configured); +} diff --git a/src/Infrastructure/BotSharp.Core/MCP/Managers/McpClientManager.cs b/src/Infrastructure/BotSharp.Core/MCP/Managers/McpClientManager.cs index a07792919..8e21d74ef 100644 --- a/src/Infrastructure/BotSharp.Core/MCP/Managers/McpClientManager.cs +++ b/src/Infrastructure/BotSharp.Core/MCP/Managers/McpClientManager.cs @@ -34,7 +34,7 @@ public McpClientManager( { Name = config.Name, Endpoint = new Uri(config.HttpConfig.EndPoint), - AdditionalHeaders = config.HttpConfig.AdditionalHeaders, + AdditionalHeaders = ResolveHeaders(config.Id, config.HttpConfig.AdditionalHeaders), ConnectionTimeout = config.HttpConfig.ConnectionTimeout }); } @@ -44,7 +44,7 @@ public McpClientManager( { Name = config.Name, Endpoint = new Uri(config.SseConfig.EndPoint), - AdditionalHeaders = config.SseConfig.AdditionalHeaders, + AdditionalHeaders = ResolveHeaders(config.Id, config.SseConfig.AdditionalHeaders), ConnectionTimeout = config.SseConfig.ConnectionTimeout }); } @@ -74,6 +74,21 @@ public McpClientManager( } } + /// + /// The headers to open a connection with: the ones from configuration, unless the host has + /// registered an that wants to adjust them. + /// + /// + /// No provider is registered by default, and a provider is free to answer with what it was + /// given, so a host without one — or with one that does not recognise this server — gets the + /// configured headers back untouched. + /// + private Dictionary? ResolveHeaders(string serverId, Dictionary? configured) + { + var provider = _services.GetService(); + return provider == null ? configured : provider.GetHeaders(serverId, configured); + } + public void Dispose() {