diff --git a/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs b/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs index eed9f0c6..394fb781 100644 --- a/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs +++ b/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs @@ -43,14 +43,39 @@ 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 - private readonly string AgenticIdAuthHandler = "agentic"; - // Setup reusable auto sign-in handler for OBO (On-Behalf-Of) authentication - private readonly string MyAuthHandler = "me"; + // 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(); + /// + /// 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); + } + + /// + /// 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, @@ -64,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 use OBO authentication via the "me" handler - OnActivity(ActivityTypes.Message, OnMessageAsync, isAgenticOnly: false, autoSignInHandlers: new[] { MyAuthHandler }); + // 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) @@ -103,12 +134,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, use OBO auth handler (supports bearer token or configured auth) + string? ObservabilityAuthHandlerName; + string? ToolAuthHandlerName; if (turnContext.IsAgenticRequest()) - ObservabilityAuthHandlerName = ToolAuthHandlerName = AgenticIdAuthHandler; + { + ObservabilityAuthHandlerName = ToolAuthHandlerName = AgenticAuthHandlerName; + } else - ObservabilityAuthHandlerName = ToolAuthHandlerName = MyAuthHandler; + { + // Non-agentic: use OBO auth handler if configured + ObservabilityAuthHandlerName = ToolAuthHandlerName = OboAuthHandlerName; + } await A365OtelWrapper.InvokeObservedAgentOperation( @@ -117,7 +155,7 @@ await A365OtelWrapper.InvokeObservedAgentOperation( turnState, _agentTokenCache, UserAuthorization, - ObservabilityAuthHandlerName, + ObservabilityAuthHandlerName ?? string.Empty, _logger, async () => { @@ -166,7 +204,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 +239,71 @@ 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)); + if (!string.IsNullOrEmpty(agentId)) + { + 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 + { + _logger?.LogWarning("Could not resolve agent identity from auth handler token."); + } + } + 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)"); + 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 + { + _logger?.LogWarning("Could not resolve agent identity from bearer token."); + } + } + 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; + // 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; + } } } 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..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": { @@ -13,17 +16,18 @@ "https://graph.microsoft.com/.default" ] } - }, - "me": { - "Type": "MsalUserAuthorization", - "Settings": { - "ClientId": "{{ClientId}}", - "TenantId": "{{TenantId}}", - "Scopes": [ - "https://graph.microsoft.com/.default" - ] - } } + // To use OBO auth instead, uncomment the following lines. + // "me": { + // "Type": "MsalUserAuthorization", + // "Settings": { + // "ClientId": "{{ClientId}}", + // "TenantId": "{{TenantId}}", + // "Scopes": [ + // "https://graph.microsoft.com/.default" + // ] + // } + // } } } },