From 8a413671ccf194af4ef6e0f986fba692020ee79c Mon Sep 17 00:00:00 2001 From: Rahul Devikar Date: Wed, 21 Jan 2026 16:07:59 -0800 Subject: [PATCH 1/4] Fix bearer token approach for Dotnet AF Sample --- .../sample-agent/Agent/MyAgent.cs | 76 ++++++++++++++----- .../sample-agent/ToolingManifest.json | 18 ----- .../sample-agent/appsettings.json | 10 --- 3 files changed, 56 insertions(+), 48 deletions(-) diff --git a/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs b/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs index eed9f0c6..4384df6a 100644 --- a/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs +++ b/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs @@ -43,14 +43,20 @@ Otherwise you should use the tools available to you to help answer the user's qu private readonly IExporterTokenCache? _agentTokenCache = null; private readonly ILogger? _logger = null; private readonly IMcpToolRegistrationService? _toolService = null; - // Setup reusable auto sign-in handlers - // Setup reusable auto sign-in handler for agentic requests + // Setup reusable auto sign-in handler for user authorization private readonly string AgenticIdAuthHandler = "agentic"; - // Setup reusable auto sign-in handler for OBO (On-Behalf-Of) authentication - private readonly string MyAuthHandler = "me"; // Temp private static readonly ConcurrentDictionary> _agentToolCache = new(); + /// + /// Check if a bearer token is available in the environment for development/testing. + /// + public static bool TryGetBearerTokenForDevelopment(out string? bearerToken) + { + bearerToken = Environment.GetEnvironmentVariable("BEARER_TOKEN"); + return !string.IsNullOrEmpty(bearerToken); + } + public MyAgent(AgentApplicationOptions options, IChatClient chatClient, IConfiguration configuration, @@ -72,8 +78,8 @@ public MyAgent(AgentApplicationOptions options, // Listen for ANY message to be received. MUST BE AFTER ANY OTHER MESSAGE HANDLERS // Agentic requests require the "agentic" handler for user authorization OnActivity(ActivityTypes.Message, OnMessageAsync, isAgenticOnly: true, autoSignInHandlers: new[] { AgenticIdAuthHandler }); - // Non-agentic requests use OBO authentication via the "me" handler - OnActivity(ActivityTypes.Message, OnMessageAsync, isAgenticOnly: false, autoSignInHandlers: new[] { MyAuthHandler }); + // Non-agentic requests (Playground, WebChat) - no auto sign-in handlers, uses bearer token from config if available + OnActivity(ActivityTypes.Message, OnMessageAsync, isAgenticOnly: false, autoSignInHandlers: Array.Empty()); } protected async Task WelcomeMessageAsync(ITurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken) @@ -103,12 +109,19 @@ await AgentMetrics.InvokeObservedAgentOperation( protected async Task OnMessageAsync(ITurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken) { // Select the appropriate auth handler based on request type - string ObservabilityAuthHandlerName = ""; - string ToolAuthHandlerName = ""; + // For agentic requests, use the agentic auth handler + // For non-agentic requests, we'll use bearer token if available (no auth handler needed) + string? ObservabilityAuthHandlerName; + string? ToolAuthHandlerName; if (turnContext.IsAgenticRequest()) + { ObservabilityAuthHandlerName = ToolAuthHandlerName = AgenticIdAuthHandler; + } else - ObservabilityAuthHandlerName = ToolAuthHandlerName = MyAuthHandler; + { + // Non-agentic: no auth handler, will use bearer token in GetClientAgent + ObservabilityAuthHandlerName = ToolAuthHandlerName = null; + } await A365OtelWrapper.InvokeObservedAgentOperation( @@ -117,7 +130,7 @@ await A365OtelWrapper.InvokeObservedAgentOperation( turnState, _agentTokenCache, UserAuthorization, - ObservabilityAuthHandlerName, + ObservabilityAuthHandlerName ?? string.Empty, _logger, async () => { @@ -166,7 +179,7 @@ await A365OtelWrapper.InvokeObservedAgentOperation( /// /// /// - private async Task GetClientAgent(ITurnContext context, ITurnState turnState, IMcpToolRegistrationService? toolService, string authHandlerName) + private async Task GetClientAgent(ITurnContext context, ITurnState turnState, IMcpToolRegistrationService? toolService, string? authHandlerName) { AssertionHelpers.ThrowIfNull(_configuration!, nameof(_configuration)); AssertionHelpers.ThrowIfNull(context, nameof(context)); @@ -201,22 +214,45 @@ await A365OtelWrapper.InvokeObservedAgentOperation( // Notify the user we are loading tools await context.StreamingResponse.QueueInformativeUpdateAsync("Loading tools..."); - string agentId = Utility.ResolveAgentIdentity(context, await UserAuthorization.GetTurnTokenAsync(context, authHandlerName)); - var a365Tools = await toolService.GetMcpToolsAsync(agentId, UserAuthorization, authHandlerName, context).ConfigureAwait(false); + // Check if we have a valid auth handler or bearer token for MCP + if (!string.IsNullOrEmpty(authHandlerName)) + { + // Use auth handler (agentic flow) + string agentId = Utility.ResolveAgentIdentity(context, await UserAuthorization.GetTurnTokenAsync(context, authHandlerName)); + var a365Tools = await toolService.GetMcpToolsAsync(agentId, UserAuthorization, authHandlerName, context).ConfigureAwait(false); - // Add the A365 tools to the tool options - if (a365Tools != null && a365Tools.Count > 0) + if (a365Tools != null && a365Tools.Count > 0) + { + toolList.AddRange(a365Tools); + _agentToolCache.TryAdd(toolCacheKey, [.. a365Tools]); + } + } + else if (TryGetBearerTokenForDevelopment(out var bearerToken)) + { + // Use bearer token from environment (non-agentic/development flow) + _logger?.LogInformation("Using bearer token from environment for MCP tools."); + _logger?.LogInformation("Bearer token length: {Length}", bearerToken?.Length ?? 0); + string agentId = Utility.ResolveAgentIdentity(context, bearerToken!); + _logger?.LogInformation("Resolved agentId: '{AgentId}'", agentId ?? "(null)"); + // Pass bearer token as the last parameter (accessToken override) + var a365Tools = await toolService.GetMcpToolsAsync(agentId, UserAuthorization, AgenticIdAuthHandler, context, bearerToken).ConfigureAwait(false); + + if (a365Tools != null && a365Tools.Count > 0) + { + toolList.AddRange(a365Tools); + _agentToolCache.TryAdd(toolCacheKey, [.. a365Tools]); + } + } + else { - toolList.AddRange(a365Tools); - _agentToolCache.TryAdd(toolCacheKey, [.. a365Tools]); + _logger?.LogWarning("No auth handler or bearer token available. MCP tools will not be loaded."); } } } catch (Exception ex) { - // Log error and rethrow - MCP tool registration is required - _logger?.LogError(ex, "Failed to register MCP tool servers. Ensure MCP servers are configured correctly or use mock MCP servers for local testing."); - throw; + // Log error but continue without MCP tools - graceful fallback to bare LLM mode + _logger?.LogError(ex, "Failed to register MCP tool servers. Continuing without MCP tools."); } } diff --git a/dotnet/agent-framework/sample-agent/ToolingManifest.json b/dotnet/agent-framework/sample-agent/ToolingManifest.json index 68f99e35..7c94ba62 100644 --- a/dotnet/agent-framework/sample-agent/ToolingManifest.json +++ b/dotnet/agent-framework/sample-agent/ToolingManifest.json @@ -2,24 +2,6 @@ "mcpServers": [ { "mcpServerName": "mcp_MailTools" - }, - { - "mcpServerName": "mcp_CalendarTools" - }, - { - "mcpServerName": "OneDriveMCPServer" - }, - { - "mcpServerName": "mcp_NLWeb" - }, - { - "mcpServerName": "mcp_KnowledgeTools" - }, - { - "mcpServerName": "mcp_MeServer" - }, - { - "mcpServerName": "mcp_WordServer" } ] } \ No newline at end of file diff --git a/dotnet/agent-framework/sample-agent/appsettings.json b/dotnet/agent-framework/sample-agent/appsettings.json index 7afd0ef1..9e346c48 100644 --- a/dotnet/agent-framework/sample-agent/appsettings.json +++ b/dotnet/agent-framework/sample-agent/appsettings.json @@ -13,16 +13,6 @@ "https://graph.microsoft.com/.default" ] } - }, - "me": { - "Type": "MsalUserAuthorization", - "Settings": { - "ClientId": "{{ClientId}}", - "TenantId": "{{TenantId}}", - "Scopes": [ - "https://graph.microsoft.com/.default" - ] - } } } } From f99677aff9de18351f83f15381699b4088e4cc71 Mon Sep 17 00:00:00 2001 From: Rahul Devikar Date: Wed, 21 Jan 2026 17:01:54 -0800 Subject: [PATCH 2/4] add skip on error --- .../sample-agent/Agent/MyAgent.cs | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs b/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs index 4384df6a..9beef40e 100644 --- a/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs +++ b/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs @@ -57,6 +57,24 @@ public static bool TryGetBearerTokenForDevelopment(out string? bearerToken) return !string.IsNullOrEmpty(bearerToken); } + /// + /// Checks if graceful fallback to bare LLM mode is enabled when MCP tools fail to load. + /// This is only allowed in Development environment AND when SKIP_TOOLING_ON_ERRORS is explicitly set to "true". + /// + private static bool ShouldSkipToolingOnErrors() + { + var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? + Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? + "Production"; + + var skipToolingOnErrors = Environment.GetEnvironmentVariable("SKIP_TOOLING_ON_ERRORS"); + + // Only allow skipping tooling errors in Development mode AND when explicitly enabled + return environment.Equals("Development", StringComparison.OrdinalIgnoreCase) && + !string.IsNullOrEmpty(skipToolingOnErrors) && + skipToolingOnErrors.Equals("true", StringComparison.OrdinalIgnoreCase); + } + public MyAgent(AgentApplicationOptions options, IChatClient chatClient, IConfiguration configuration, @@ -251,8 +269,18 @@ await A365OtelWrapper.InvokeObservedAgentOperation( } catch (Exception ex) { - // Log error but continue without MCP tools - graceful fallback to bare LLM mode - _logger?.LogError(ex, "Failed to register MCP tool servers. Continuing without MCP tools."); + // Only allow graceful fallback in Development mode when SKIP_TOOLING_ON_ERRORS is explicitly enabled + if (ShouldSkipToolingOnErrors()) + { + // Graceful fallback: Log the error but continue without MCP tools + _logger?.LogWarning(ex, "Failed to register MCP tool servers. Continuing without MCP tools (SKIP_TOOLING_ON_ERRORS=true)."); + } + else + { + // In production or when SKIP_TOOLING_ON_ERRORS is not enabled, fail fast + _logger?.LogError(ex, "Failed to register MCP tool servers."); + throw; + } } } From 1a87928e7abe7632b1d33d01278fca97346875a1 Mon Sep 17 00:00:00 2001 From: Rahul Devikar Date: Thu, 22 Jan 2026 16:20:54 -0800 Subject: [PATCH 3/4] add obo handler --- .../sample-agent/Agent/MyAgent.cs | 31 ++++++++++++------- .../sample-agent/appsettings.json | 14 +++++++++ 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs b/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs index 9beef40e..20604ac6 100644 --- a/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs +++ b/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs @@ -43,8 +43,9 @@ Otherwise you should use the tools available to you to help answer the user's qu private readonly IExporterTokenCache? _agentTokenCache = null; private readonly ILogger? _logger = null; private readonly IMcpToolRegistrationService? _toolService = null; - // Setup reusable auto sign-in handler for user authorization - private readonly string AgenticIdAuthHandler = "agentic"; + // Setup reusable auto sign-in handlers for user authorization (configurable via appsettings.json) + private readonly string? AgenticAuthHandlerName; + private readonly string? OboAuthHandlerName; // Temp private static readonly ConcurrentDictionary> _agentToolCache = new(); @@ -88,16 +89,22 @@ public MyAgent(AgentApplicationOptions options, _logger = logger; _toolService = toolService; + // Read auth handler names from configuration (can be empty/null to disable) + AgenticAuthHandlerName = _configuration.GetValue("AgentApplication:AgenticAuthHandlerName"); + OboAuthHandlerName = _configuration.GetValue("AgentApplication:OboAuthHandlerName"); + // Greet when members are added to the conversation OnConversationUpdate(ConversationUpdateEvents.MembersAdded, WelcomeMessageAsync); // Handle A365 Notification Messages. // Listen for ANY message to be received. MUST BE AFTER ANY OTHER MESSAGE HANDLERS - // Agentic requests require the "agentic" handler for user authorization - OnActivity(ActivityTypes.Message, OnMessageAsync, isAgenticOnly: true, autoSignInHandlers: new[] { AgenticIdAuthHandler }); - // Non-agentic requests (Playground, WebChat) - no auto sign-in handlers, uses bearer token from config if available - OnActivity(ActivityTypes.Message, OnMessageAsync, isAgenticOnly: false, autoSignInHandlers: Array.Empty()); + // Agentic requests use the agentic auth handler (if configured) + var agenticHandlers = !string.IsNullOrEmpty(AgenticAuthHandlerName) ? new[] { AgenticAuthHandlerName } : Array.Empty(); + OnActivity(ActivityTypes.Message, OnMessageAsync, isAgenticOnly: true, autoSignInHandlers: agenticHandlers); + // Non-agentic requests (Playground, WebChat) use OBO auth handler (if configured) + var oboHandlers = !string.IsNullOrEmpty(OboAuthHandlerName) ? new[] { OboAuthHandlerName } : Array.Empty(); + OnActivity(ActivityTypes.Message, OnMessageAsync, isAgenticOnly: false, autoSignInHandlers: oboHandlers); } protected async Task WelcomeMessageAsync(ITurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken) @@ -128,17 +135,17 @@ protected async Task OnMessageAsync(ITurnContext turnContext, ITurnState turnSta { // Select the appropriate auth handler based on request type // For agentic requests, use the agentic auth handler - // For non-agentic requests, we'll use bearer token if available (no auth handler needed) + // For non-agentic requests, use OBO auth handler (supports bearer token or configured auth) string? ObservabilityAuthHandlerName; string? ToolAuthHandlerName; if (turnContext.IsAgenticRequest()) { - ObservabilityAuthHandlerName = ToolAuthHandlerName = AgenticIdAuthHandler; + ObservabilityAuthHandlerName = ToolAuthHandlerName = AgenticAuthHandlerName; } else { - // Non-agentic: no auth handler, will use bearer token in GetClientAgent - ObservabilityAuthHandlerName = ToolAuthHandlerName = null; + // Non-agentic: use OBO auth handler if configured + ObservabilityAuthHandlerName = ToolAuthHandlerName = OboAuthHandlerName; } @@ -253,7 +260,9 @@ await A365OtelWrapper.InvokeObservedAgentOperation( string agentId = Utility.ResolveAgentIdentity(context, bearerToken!); _logger?.LogInformation("Resolved agentId: '{AgentId}'", agentId ?? "(null)"); // Pass bearer token as the last parameter (accessToken override) - var a365Tools = await toolService.GetMcpToolsAsync(agentId, UserAuthorization, AgenticIdAuthHandler, context, bearerToken).ConfigureAwait(false); + // Use OboAuthHandlerName for non-agentic requests, fall back to AgenticAuthHandlerName if not set + var handlerForBearerToken = OboAuthHandlerName ?? AgenticAuthHandlerName ?? string.Empty; + var a365Tools = await toolService.GetMcpToolsAsync(agentId, UserAuthorization, handlerForBearerToken, context, bearerToken).ConfigureAwait(false); if (a365Tools != null && a365Tools.Count > 0) { diff --git a/dotnet/agent-framework/sample-agent/appsettings.json b/dotnet/agent-framework/sample-agent/appsettings.json index 9e346c48..ea668f2d 100644 --- a/dotnet/agent-framework/sample-agent/appsettings.json +++ b/dotnet/agent-framework/sample-agent/appsettings.json @@ -3,6 +3,9 @@ "StartTypingTimer": false, "RemoveRecipientMention": false, "NormalizeMentions": false, + "AgenticAuthHandlerName": "agentic", + // To use OBO auth instead, uncomment the following line and comment out the above line. + // "OboAuthHandlerName": "me", "UserAuthorization": { "AutoSignin": false, "Handlers": { @@ -14,6 +17,17 @@ ] } } + // To use OBO auth instead, uncomment the following lines. + // "me": { + // "Type": "MsalUserAuthorization", + // "Settings": { + // "ClientId": "{{ClientId}}", + // "TenantId": "{{TenantId}}", + // "Scopes": [ + // "https://graph.microsoft.com/.default" + // ] + // } + // } } } }, From 8c84da30ef3ad455afefe790179c95829e08c34e Mon Sep 17 00:00:00 2001 From: Rahul Devikar Date: Thu, 22 Jan 2026 16:59:11 -0800 Subject: [PATCH 4/4] add obo handler --- .../sample-agent/Agent/MyAgent.cs | 42 ++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs b/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs index 20604ac6..394fb781 100644 --- a/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs +++ b/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs @@ -243,13 +243,20 @@ await A365OtelWrapper.InvokeObservedAgentOperation( if (!string.IsNullOrEmpty(authHandlerName)) { // Use auth handler (agentic flow) - string agentId = Utility.ResolveAgentIdentity(context, await UserAuthorization.GetTurnTokenAsync(context, authHandlerName)); - var a365Tools = await toolService.GetMcpToolsAsync(agentId, UserAuthorization, authHandlerName, context).ConfigureAwait(false); + string? agentId = Utility.ResolveAgentIdentity(context, await UserAuthorization.GetTurnTokenAsync(context, authHandlerName)); + if (!string.IsNullOrEmpty(agentId)) + { + var a365Tools = await toolService.GetMcpToolsAsync(agentId, UserAuthorization, authHandlerName, context).ConfigureAwait(false); - if (a365Tools != null && a365Tools.Count > 0) + if (a365Tools != null && a365Tools.Count > 0) + { + toolList.AddRange(a365Tools); + _agentToolCache.TryAdd(toolCacheKey, [.. a365Tools]); + } + } + else { - toolList.AddRange(a365Tools); - _agentToolCache.TryAdd(toolCacheKey, [.. a365Tools]); + _logger?.LogWarning("Could not resolve agent identity from auth handler token."); } } else if (TryGetBearerTokenForDevelopment(out var bearerToken)) @@ -257,17 +264,24 @@ await A365OtelWrapper.InvokeObservedAgentOperation( // Use bearer token from environment (non-agentic/development flow) _logger?.LogInformation("Using bearer token from environment for MCP tools."); _logger?.LogInformation("Bearer token length: {Length}", bearerToken?.Length ?? 0); - string agentId = Utility.ResolveAgentIdentity(context, bearerToken!); + string? agentId = Utility.ResolveAgentIdentity(context, bearerToken!); _logger?.LogInformation("Resolved agentId: '{AgentId}'", agentId ?? "(null)"); - // Pass bearer token as the last parameter (accessToken override) - // Use OboAuthHandlerName for non-agentic requests, fall back to AgenticAuthHandlerName if not set - var handlerForBearerToken = OboAuthHandlerName ?? AgenticAuthHandlerName ?? string.Empty; - var a365Tools = await toolService.GetMcpToolsAsync(agentId, UserAuthorization, handlerForBearerToken, context, bearerToken).ConfigureAwait(false); - - if (a365Tools != null && a365Tools.Count > 0) + if (!string.IsNullOrEmpty(agentId)) + { + // Pass bearer token as the last parameter (accessToken override) + // Use OboAuthHandlerName for non-agentic requests, fall back to AgenticAuthHandlerName if not set + var handlerForBearerToken = OboAuthHandlerName ?? AgenticAuthHandlerName ?? string.Empty; + var a365Tools = await toolService.GetMcpToolsAsync(agentId, UserAuthorization, handlerForBearerToken, context, bearerToken).ConfigureAwait(false); + + if (a365Tools != null && a365Tools.Count > 0) + { + toolList.AddRange(a365Tools); + _agentToolCache.TryAdd(toolCacheKey, [.. a365Tools]); + } + } + else { - toolList.AddRange(a365Tools); - _agentToolCache.TryAdd(toolCacheKey, [.. a365Tools]); + _logger?.LogWarning("Could not resolve agent identity from bearer token."); } } else