Skip to content

Refactor MCP token provider selection and fallback logic#251

Open
MattB-msft wants to merge 3 commits into
mainfrom
users/mbarbour/updateToolsDisco
Open

Refactor MCP token provider selection and fallback logic#251
MattB-msft wants to merge 3 commits into
mainfrom
users/mbarbour/updateToolsDisco

Conversation

@MattB-msft
Copy link
Copy Markdown
Member

Rename DevMcpTokenProvider to EnvMcpTokenProvider and update all usages. Introduce TokenProviderCollection to allow chaining multiple token providers, enabling fallback from environment-based to OBO-based authentication. Update token acquisition logic to handle failures more robustly by removing servers that fail token acquisition and logging warnings. Update tests and remove obsolete aliases. These changes improve flexibility, reliability, and diagnostics for MCP token handling.

This pull request introduces a new TokenProviderCollection to support chaining multiple token providers, improves error handling during token acquisition, and standardizes the naming of the environment-based token provider. The changes enhance flexibility and robustness in how tokens are acquired for MCP servers, especially across different environments (development and production).

Token Provider Refactoring and Chaining:

  • Introduced the TokenProviderCollection class, which allows chaining multiple IMcpTokenProvider implementations and tries each in order until a valid token is obtained. This enables seamless fallback between environment-based and agentic token providers. (TokenProviderCollection.cs src/Tooling/Core/Services/TokenProviderCollection.csR1-R44)
  • Updated all usages of the old DevMcpTokenProvider to use the new EnvMcpTokenProvider, and replaced direct instantiations with the new TokenProviderCollection to allow for provider chaining in all registration services. [1] [2] [3] [4] [5] [6]

Error Handling Improvements:

  • Enhanced error handling in McpToolServerConfigurationService.AttachPerAudienceTokensAsync by logging token acquisition failures per server, aggregating failed servers, and removing them from the list to prevent downstream authentication errors. [1] [2]

Code Cleanup and Renaming:

  • Renamed DevMcpTokenProvider to EnvMcpTokenProvider throughout the codebase for clarity and consistency, and updated related references and comments. [1] [2]
  • Removed obsolete using statements for the old provider aliases in all affected files. [1] [2] [3]

These changes collectively make the token acquisition process more robust, maintainable, and adaptable to different deployment scenarios.

OTEL telemetry indicating continue after tool lookup failure :
image

Rename DevMcpTokenProvider to EnvMcpTokenProvider and update all usages. Introduce TokenProviderCollection to allow chaining multiple token providers, enabling fallback from environment-based to OBO-based authentication. Update token acquisition logic to handle failures more robustly by removing servers that fail token acquisition and logging warnings. Update tests and remove obsolete aliases. These changes improve flexibility, reliability, and diagnostics for MCP token handling.
Copilot AI review requested due to automatic review settings May 14, 2026 00:22
@MattB-msft MattB-msft requested a review from a team as a code owner May 14, 2026 00:22
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 14, 2026

⚠️ Deprecation Warning: The deny-licenses option is deprecated for possible removal in the next major release. For more information, see issue 997.

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

Comment thread src/Tooling/Core/Services/TokenProviderCollection.cs Fixed
Comment thread src/Tooling/Core/Services/TokenProviderCollection.cs Fixed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors MCP token acquisition to support chaining/fallback across multiple IMcpTokenProvider implementations (environment-based first, then OBO/agentic), and improves robustness by removing servers that fail per-audience token attachment.

Changes:

  • Introduces TokenProviderCollection to try multiple token providers in order until a token is obtained.
  • Renames DevMcpTokenProvider to EnvMcpTokenProvider and updates extension registration services to use the chained provider approach.
  • Updates per-audience token attachment to log acquisition failures and remove servers that cannot be authenticated.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
src/Tooling/Extensions/SemanticKernel/Services/McpToolRegistrationService.cs Switches to chained token providers for MCP tool enumeration.
src/Tooling/Extensions/AzureAIFoundry/Services/McpToolRegistrationService.cs Uses chained token providers; adds dev/prod selection logic (with a nullability concern noted).
src/Tooling/Extensions/AgentFramework/Services/McpToolRegistrationService.cs Switches to chained token providers for MCP tool enumeration.
src/Tooling/Core/Services/TokenProviderCollection.cs Adds new chaining token provider implementation (several issues noted).
src/Tooling/Core/Services/McpToolServerConfigurationService.cs Adds failure handling during token attachment by removing failing servers (cancellation and perf concerns noted).
src/Tooling/Core/Services/EnvMcpTokenProvider.cs Renames the dev env-var provider class to EnvMcpTokenProvider.
src/Tests/Microsoft.Agents.A365.Tooling.Core.Tests/DevMcpTokenProviderTests.cs Updates tests to target EnvMcpTokenProvider (naming consistency noted).

Comment thread src/Tooling/Core/Services/TokenProviderCollection.cs Outdated
Comment on lines +1 to +5
using Microsoft.Agents.A365.Tooling.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
Comment thread src/Tooling/Core/Services/TokenProviderCollection.cs
Comment on lines +13 to +20
public TokenProviderCollection(params IMcpTokenProvider[] providers)
{
_providers = new SortedDictionary<int, IMcpTokenProvider>();
for (int i = 0; i < providers.Length; i++)
{
_providers.Add(i, providers[i]);
}
}
Comment on lines 176 to +181
if (ToolingUtility.IsDevScenario(_configuration))
{
IMcpTokenProvider tokenProvider = new DevMcpTokenProvider(_configuration, _logger);
IMcpTokenProvider tokenProvider = new TokenProviderCollection(
new EnvMcpTokenProvider(_configuration, _logger),
new AgenticMcpTokenProvider(userAuthorization!, authHandlerName!, turnContext, _configuration, _logger));

Comment on lines 172 to 176
// Select token provider:
// Dev scenario → DevMcpTokenProvider reads per-server env vars (no OBO flow needed).
// Production → AgenticMcpTokenProvider performs OBO when auth objects are supplied;
// falls back to the V1 shared-token path when they are absent.
if (ToolingUtility.IsDevScenario(_configuration))
// Sequential acquisition avoids throttling the OBO endpoint.
var tokenByScope = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

List<MCPServerConfig> failedToAquireServers = new List<MCPServerConfig>();
Comment on lines 558 to 578
cancellationToken.ThrowIfCancellationRequested();
try
{
var scope = Utils.Utility.ResolveTokenScopeForServer(server, _configuration);
if (!tokenByScope.TryGetValue(scope, out var token))
{
token = await tokenProvider.GetTokenAsync(server, cancellationToken).ConfigureAwait(false);
tokenByScope[scope] = token;
_logger.LogDebug(
"Acquired token for scope '{Scope}' (server '{ServerName}')",
scope, server.mcpServerName);
}

var scope = Utils.Utility.ResolveTokenScopeForServer(server, _configuration);
if (!tokenByScope.TryGetValue(scope, out var token))
server.Headers ??= new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
server.Headers[Constants.Headers.Authorization] = $"{Constants.Headers.BearerPrefix} {token}";
}
catch (Exception ex)
{
token = await tokenProvider.GetTokenAsync(server, cancellationToken).ConfigureAwait(false);
tokenByScope[scope] = token;
_logger.LogDebug(
"Acquired token for scope '{Scope}' (server '{ServerName}')",
scope, server.mcpServerName);
failedToAquireServers.Add(server);
_logger.LogError(ex, "Failed to acquire token for server '{ServerName}': {Message}", server.mcpServerName, ex.Message);
}
Comment on lines +581 to +587
if (failedToAquireServers.Count > 0)
{
_logger.LogWarning("Failed to acquire tokens for {Count} MCP servers: {ServerNames}",
failedToAquireServers.Count, string.Join(", ", failedToAquireServers.Select(s => s.mcpServerName)));
// remove servers we failed to acquire tokens for, since they'll likely fail authentication anyway
servers.RemoveAll(s => failedToAquireServers.Contains(s));
failedToAquireServers.Clear();
Comment on lines +16 to 24
/// Tests for <see cref="EnvMcpTokenProvider"/> and <see cref="Utility.IsDevScenario"/>.
/// </summary>
public class DevMcpTokenProviderTests
{
private static MCPServerConfig Server(string name) =>
new() { mcpServerName = name, id = $"id-{name}", url = "http://test" };

private static DevMcpTokenProvider Provider(IConfiguration config) =>
private static EnvMcpTokenProvider Provider(IConfiguration config) =>
new(config, Mock.Of<ILogger>());
biswapm
biswapm previously approved these changes May 14, 2026
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 15, 2026 17:01
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Comment on lines +39 to +42
catch(Exception ex)
{
exceptions.Add(new Exception($"Provider {provider.GetType().Name} failed to obtain a token." , ex));
}
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 11 comments.

Comments suppressed due to low confidence (2)

src/Tooling/Extensions/AgentFramework/Services/McpToolRegistrationService.cs:141

  • Same issue as above: this TokenProviderCollection ordering will typically trigger an exception path on every token acquisition in production when env vars aren't set. Consider choosing provider order based on IsDevScenario, or avoiding exception-driven fallback.

            IMcpTokenProvider tokenProvider = new TokenProviderCollection(
                    new EnvMcpTokenProvider(_configuration, _logger),
                    new AgenticMcpTokenProvider(userAuthorization, authHandlerName, turnContext, _configuration, _logger));

src/Tooling/Core/Services/McpToolServerConfigurationService.cs:587

  • servers.RemoveAll(s => failedToAquireServers.Contains(s)) is O(n^2) due to List.Contains inside RemoveAll. Consider using a HashSet (or removing by index) to avoid quadratic behavior if the server list grows.
                _logger.LogWarning("Failed to acquire tokens for {Count} MCP servers: {ServerNames}",
                    failedToAquireServers.Count, string.Join(", ", failedToAquireServers.Select(s => s.mcpServerName)));
                // remove servers we failed to acquire tokens for, since they'll likely fail authentication anyway
                servers.RemoveAll(s => failedToAquireServers.Contains(s));
                failedToAquireServers.Clear(); 

Comment on lines +1 to +2
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
Comment on lines +7 to +8
using System.Linq;
using System.Text;
Comment on lines +22 to +42
}
}

public async Task<string> GetTokenAsync(MCPServerConfig server, CancellationToken cancellationToken = default)
{
List<Exception> exceptions = new List<Exception>();
// Try each provider in turn, then return the first successful token. If no providers can return a token, throw an exception.
foreach (var provider in _providers.Values)
{
try
{
var token = await provider.GetTokenAsync(server, cancellationToken);
if (!string.IsNullOrWhiteSpace(token))
{
return token;
}
}
catch(Exception ex)
{
exceptions.Add(new Exception($"Provider {provider.GetType().Name} failed to obtain a token." , ex));
}
Comment on lines 73 to +78
// Use per-audience token provider so V2 servers receive audience-scoped tokens.
// In dev scenarios tokens come from environment variables; in production from OBO flow.
IMcpTokenProvider tokenProvider = ToolingUtility.IsDevScenario(_configuration)
? new DevMcpTokenProvider(_configuration, _logger)
: new AgenticMcpTokenProvider(userAuthorization, authHandlerName, turnContext, _configuration, _logger);

IMcpTokenProvider tokenProvider =
new TokenProviderCollection(
new EnvMcpTokenProvider(_configuration, _logger),
new AgenticMcpTokenProvider(userAuthorization, authHandlerName, turnContext, _configuration, _logger));
Comment on lines 84 to 89
// Use per-audience token provider so V2 servers receive audience-scoped tokens.
// In dev scenarios tokens come from environment variables; in production from OBO flow.
IMcpTokenProvider tokenProvider = ToolingUtility.IsDevScenario(_configuration)
? new DevMcpTokenProvider(_configuration, _logger)
: new AgenticMcpTokenProvider(userAuthorization, authHandlerName, turnContext, _configuration, _logger);
IMcpTokenProvider tokenProvider = new TokenProviderCollection(
new EnvMcpTokenProvider(_configuration, _logger),
new AgenticMcpTokenProvider(userAuthorization, authHandlerName, turnContext, _configuration, _logger));

Comment on lines 176 to +181
if (ToolingUtility.IsDevScenario(_configuration))
{
IMcpTokenProvider tokenProvider = new DevMcpTokenProvider(_configuration, _logger);
IMcpTokenProvider tokenProvider = new TokenProviderCollection(
new EnvMcpTokenProvider(_configuration, _logger),
new AgenticMcpTokenProvider(userAuthorization!, authHandlerName!, turnContext, _configuration, _logger));

Comment on lines 184 to 191
else if (userAuthorization is not null && authHandlerName is not null)
{
// Production V2-aware path: per-audience OBO tokens.
IMcpTokenProvider tokenProvider = new AgenticMcpTokenProvider(
userAuthorization, authHandlerName, turnContext, _configuration, _logger);
IMcpTokenProvider tokenProvider = new TokenProviderCollection(
new EnvMcpTokenProvider(_configuration, _logger),
new AgenticMcpTokenProvider(userAuthorization, authHandlerName, turnContext, _configuration, _logger));

(servers, toolsByServer) = await _mcpServerConfigurationService.EnumerateToolsFromServersAsync(agentInstanceId, authToken, tokenProvider, turnContext, toolOptions).ConfigureAwait(false);
Comment on lines 558 to 578
cancellationToken.ThrowIfCancellationRequested();
try
{
var scope = Utils.Utility.ResolveTokenScopeForServer(server, _configuration);
if (!tokenByScope.TryGetValue(scope, out var token))
{
token = await tokenProvider.GetTokenAsync(server, cancellationToken).ConfigureAwait(false);
tokenByScope[scope] = token;
_logger.LogDebug(
"Acquired token for scope '{Scope}' (server '{ServerName}')",
scope, server.mcpServerName);
}

var scope = Utils.Utility.ResolveTokenScopeForServer(server, _configuration);
if (!tokenByScope.TryGetValue(scope, out var token))
server.Headers ??= new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
server.Headers[Constants.Headers.Authorization] = $"{Constants.Headers.BearerPrefix} {token}";
}
catch (Exception ex)
{
token = await tokenProvider.GetTokenAsync(server, cancellationToken).ConfigureAwait(false);
tokenByScope[scope] = token;
_logger.LogDebug(
"Acquired token for scope '{Scope}' (server '{ServerName}')",
scope, server.mcpServerName);
failedToAquireServers.Add(server);
_logger.LogError(ex, "Failed to acquire token for server '{ServerName}': {Message}", server.mcpServerName, ex.Message);
}
Comment on lines +555 to 556
List<MCPServerConfig> failedToAquireServers = new List<MCPServerConfig>();
foreach (var server in servers)
/// Tests for <see cref="DevMcpTokenProvider"/> and <see cref="Utility.IsDevScenario"/>.
/// Tests for <see cref="EnvMcpTokenProvider"/> and <see cref="Utility.IsDevScenario"/>.
/// </summary>
public class DevMcpTokenProviderTests
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants