Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 111 additions & 24 deletions dotnet/agent-framework/sample-agent/Agent/MyAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,39 @@ Otherwise you should use the tools available to you to help answer the user's qu
private readonly IExporterTokenCache<AgenticTokenStruct>? _agentTokenCache = null;
private readonly ILogger<MyAgent>? _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<string, List<AITool>> _agentToolCache = new();

/// <summary>
/// Check if a bearer token is available in the environment for development/testing.
/// </summary>
public static bool TryGetBearerTokenForDevelopment(out string? bearerToken)
{
bearerToken = Environment.GetEnvironmentVariable("BEARER_TOKEN");
return !string.IsNullOrEmpty(bearerToken);
}

/// <summary>
/// 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".
/// </summary>
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,
Expand All @@ -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<string>("AgentApplication:AgenticAuthHandlerName");
OboAuthHandlerName = _configuration.GetValue<string>("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<string>();
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<string>();
OnActivity(ActivityTypes.Message, OnMessageAsync, isAgenticOnly: false, autoSignInHandlers: oboHandlers);
}

protected async Task WelcomeMessageAsync(ITurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken)
Expand Down Expand Up @@ -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(
Expand All @@ -117,7 +155,7 @@ await A365OtelWrapper.InvokeObservedAgentOperation(
turnState,
_agentTokenCache,
UserAuthorization,
ObservabilityAuthHandlerName,
ObservabilityAuthHandlerName ?? string.Empty,
_logger,
async () =>
{
Expand Down Expand Up @@ -166,7 +204,7 @@ await A365OtelWrapper.InvokeObservedAgentOperation(
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
private async Task<AIAgent?> GetClientAgent(ITurnContext context, ITurnState turnState, IMcpToolRegistrationService? toolService, string authHandlerName)
private async Task<AIAgent?> GetClientAgent(ITurnContext context, ITurnState turnState, IMcpToolRegistrationService? toolService, string? authHandlerName)
{
AssertionHelpers.ThrowIfNull(_configuration!, nameof(_configuration));
AssertionHelpers.ThrowIfNull(context, nameof(context));
Expand Down Expand Up @@ -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;
}
}
}

Expand Down
18 changes: 0 additions & 18 deletions dotnet/agent-framework/sample-agent/ToolingManifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
}
24 changes: 14 additions & 10 deletions dotnet/agent-framework/sample-agent/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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"
// ]
// }
// }
}
}
},
Expand Down