Skip to content
104 changes: 71 additions & 33 deletions dotnet/semantic-kernel/sample-agent/Agents/Agent365Agent.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Agent365SemanticKernelSampleAgent.Plugins;
using Microsoft.Agents.A365.Tooling.Extensions.SemanticKernel.Services;
using Microsoft.Agents.Builder;
using Microsoft.Agents.Builder.App.UserAuth;
using Microsoft.Extensions.Configuration;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using Agent365SemanticKernelSampleAgent.Plugins;
using Microsoft.Agents.A365.Tooling.Extensions.SemanticKernel.Services;
using Microsoft.Agents.Builder;
using Microsoft.Agents.Builder.App.UserAuth;
using Microsoft.Extensions.Configuration;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading.Tasks;

namespace Agent365SemanticKernelSampleAgent.Agents;
Expand Down Expand Up @@ -54,10 +54,28 @@ public static async Task<Agent365Agent> CreateA365AgentWrapper(Kernel kernel, IS
return _agent;
}

public static bool TryGetBearerTokenForDevelopment(out string? bearerToken)
{
bearerToken = Environment.GetEnvironmentVariable("BEARER_TOKEN");
return !string.IsNullOrEmpty(bearerToken);
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);
}

/// <summary>
Expand All @@ -79,17 +97,37 @@ public async Task InitializeAgent365Agent(Kernel kernel, IServiceProvider servic
// Provide the tool service with necessary parameters to connect to A365
this._kernel.ImportPluginFromType<TermsAndConditionsAcceptedPlugin>();

await turnContext.StreamingResponse.QueueInformativeUpdateAsync("Loading tools...");

if (TryGetBearerTokenForDevelopment(out var bearerToken))
{
// Development mode: Use bearer token from environment variable for simplified local testing
await toolService.AddToolServersToAgentAsync(kernel, userAuthorization, authHandlerName, turnContext, bearerToken);
}
else
{
// Production mode: Use standard authentication flow (Client Credentials, Managed Identity, or Federated Credentials)
await toolService.AddToolServersToAgentAsync(kernel, userAuthorization, authHandlerName, turnContext);
await turnContext.StreamingResponse.QueueInformativeUpdateAsync("Loading tools...");

try
{
if (TryGetBearerTokenForDevelopment(out var bearerToken))
{
// Development mode: Use bearer token from environment variable for simplified local testing
await toolService.AddToolServersToAgentAsync(kernel, userAuthorization, authHandlerName, turnContext, bearerToken);
}
else
{
// Production mode: Use standard authentication flow (Client Credentials, Managed Identity, or Federated Credentials)
await toolService.AddToolServersToAgentAsync(kernel, userAuthorization, authHandlerName, turnContext);
}
}
catch (Exception ex)
{
// 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
// This allows the agent to still respond to basic queries using only the LLM
System.Diagnostics.Debug.WriteLine($"Warning: Failed to load MCP tools: {ex.Message}");
Console.WriteLine($"Warning: MCP tools unavailable - running in bare LLM mode. Error: {ex.Message}");
await turnContext.StreamingResponse.QueueInformativeUpdateAsync("Note: Some tools are not available. Running in basic mode.");
}
else
{
// In production or when SKIP_TOOLING_ON_ERRORS is not enabled, fail fast
throw;
}
}
}
else
Expand Down
6 changes: 6 additions & 0 deletions dotnet/semantic-kernel/sample-agent/Agents/MyAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,12 @@ await A365OtelWrapper.InvokeObservedAgentOperation(
{
await TeamsMessageActivityAsync(agent365Agent, turnContext, turnState, cancellationToken);
}
else if (turnContext.Activity.ChannelId.Channel == Channels.Emulator ||
turnContext.Activity.ChannelId.Channel == Channels.Test)
{
var response = await agent365Agent.InvokeAgentAsync(turnContext.Activity.Text, new ChatHistory(), turnContext);
await OutputResponseAsync(turnContext, turnState, response, cancellationToken);
}
else
{
await turnContext.SendActivityAsync(MessageFactory.Text($"Sorry, I do not know how to respond to messages from channel '{turnContext.Activity.ChannelId}'."), cancellationToken);
Expand Down
15 changes: 0 additions & 15 deletions dotnet/semantic-kernel/sample-agent/ToolingManifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,6 @@
"mcpServers": [
{
"mcpServerName": "mcp_MailTools"
},
{
"mcpServerName": "mcp_CalendarTools"
},
{
"mcpServerName": "OneDriveMCPServer"
},
{
"mcpServerName": "mcp_NLWeb"
},
{
"mcpServerName": "mcp_KnowledgeTools"
},
{
"mcpServerName": "mcp_MeServer"
}
]
}
56 changes: 48 additions & 8 deletions python/openai/sample-agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,18 @@ class OpenAIAgentWithMCP(AgentInterface):
# =========================================================================
# <Initialization>

@staticmethod
def should_skip_tooling_on_errors() -> bool:
"""
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".
"""
environment = os.getenv("ENVIRONMENT", os.getenv("ASPNETCORE_ENVIRONMENT", "Production"))
skip_tooling_on_errors = os.getenv("SKIP_TOOLING_ON_ERRORS", "").lower()

# Only allow skipping tooling errors in Development mode AND when explicitly enabled
return environment.lower() == "development" and skip_tooling_on_errors == "true"

def __init__(self, openai_api_key: str | None = None):
self.openai_api_key = openai_api_key or os.getenv("OPENAI_API_KEY")
if not self.openai_api_key and (
Expand All @@ -84,11 +96,15 @@ def __init__(self, openai_api_key: str | None = None):
api_key=api_key,
api_version="2025-01-01-preview",
)
# Use Azure deployment name for Azure OpenAI
model_name = os.getenv("AZURE_OPENAI_DEPLOYMENT", "gpt-4o-mini")
else:
self.openai_client = AsyncOpenAI(api_key=self.openai_api_key)
# Use model name for OpenAI
model_name = os.getenv("OPENAI_MODEL", "gpt-4o-mini")

self.model = OpenAIChatCompletionsModel(
model=os.getenv("OPENAI_MODEL", "gpt-4o-mini"), openai_client=self.openai_client
model=model_name, openai_client=self.openai_client
)

# Configure model settings (optional parameters)
Expand Down Expand Up @@ -220,28 +236,52 @@ def _initialize_services(self):
# return tool_service, auth_options

async def setup_mcp_servers(self, auth: Authorization, auth_handler_name: str, context: TurnContext):
"""Set up MCP server connections"""
"""Set up MCP server connections based on authentication configuration.

Authentication priority:
1. Bearer token from config (BEARER_TOKEN) - for local development/testing
2. Auth handler (auth_handler_name) - for production agentic auth
3. No auth - gracefully skip MCP and run in bare LLM mode

If MCP connection fails for any reason, the agent will gracefully fall back
to bare LLM mode without MCP tools.
"""
try:

use_agentic_auth = os.getenv("USE_AGENTIC_AUTH", "false").lower() == "true"
if use_agentic_auth:
# Priority 1: Bearer token provided in config (for local dev/testing)
if self.auth_options.bearer_token:
logger.info("🔑 Using bearer token from config for MCP servers")
self.agent = await self.tool_service.add_tool_servers_to_agent(
agent=self.agent,
auth=auth,
auth_handler_name=auth_handler_name,
context=context,
auth_token=self.auth_options.bearer_token,
)
else:
# Priority 2: Auth handler configured (production agentic auth)
elif auth_handler_name:
logger.info(f"🔒 Using auth handler '{auth_handler_name}' for MCP servers")
self.agent = await self.tool_service.add_tool_servers_to_agent(
agent=self.agent,
auth=auth,
auth_handler_name=auth_handler_name,
context=context,
auth_token=self.auth_options.bearer_token,
)
# Priority 3: No auth configured - skip MCP and run bare LLM
else:
logger.warning("⚠️ No authentication configured - running in bare LLM mode without MCP tools")
logger.info("💡 To enable MCP: provide BEARER_TOKEN or configure AUTH_HANDLER_NAME")
# Agent already initialized without MCP tools

except Exception as e:
logger.error(f"Error setting up MCP servers: {e}")
# Only allow graceful fallback in Development mode when SKIP_TOOLING_ON_ERRORS is explicitly enabled
if self.should_skip_tooling_on_errors():
logger.error(f"❌ Error setting up MCP servers: {e}")
logger.warning("⚠️ Falling back to bare LLM mode without MCP servers (SKIP_TOOLING_ON_ERRORS=true)")
# Agent continues with base LLM capabilities only
else:
# In production or when SKIP_TOOLING_ON_ERRORS is not enabled, fail fast
logger.error(f"❌ Error setting up MCP servers: {e}")
raise

async def initialize(self):
"""Initialize the agent and MCP server connections"""
Expand Down
41 changes: 25 additions & 16 deletions python/openai/sample-agent/host_agent_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,13 @@ def __init__(self, agent_class: type[AgentInterface], *agent_args, **agent_kwarg
if not check_agent_inheritance(agent_class):
raise TypeError(f"Agent class {agent_class.__name__} must inherit from AgentInterface")

self.auth_handler_name = "AGENTIC"
# Auth handler name can be configured via environment
# Defaults to empty (no auth handler) - set AUTH_HANDLER_NAME=AGENTIC for production agentic auth
self.auth_handler_name = os.getenv("AUTH_HANDLER_NAME", "") or None
if self.auth_handler_name:
logger.info(f"🔐 Using auth handler: {self.auth_handler_name}")
else:
logger.info("🔓 No auth handler configured (AUTH_HANDLER_NAME not set)")

self.agent_class = agent_class
self.agent_args = agent_args
Expand Down Expand Up @@ -110,8 +116,9 @@ async def help_handler(context: TurnContext, _: TurnState):
self.agent_app.conversation_update("membersAdded")(help_handler)
self.agent_app.message("/help")(help_handler)

handler = [self.auth_handler_name]
@self.agent_app.activity("message", auth_handlers=handler)
# Configure auth handlers - required for token exchange when auth_handler_name is set
handler_config = {"auth_handlers": [self.auth_handler_name]} if self.auth_handler_name else {}
@self.agent_app.activity("message", **handler_config)
async def on_message(context: TurnContext, _: TurnState):
"""Handle all messages with the hosted agent"""
try:
Expand All @@ -125,18 +132,20 @@ async def on_message(context: TurnContext, _: TurnState):
await context.send_activity(error_msg)
return

exaau_token = await self.agent_app.auth.exchange_token(
context,
scopes=get_observability_authentication_scope(),
auth_handler_id=self.auth_handler_name,
)

# Cache the agentic token for Agent 365 Observability exporter use
cache_agentic_token(
tenant_id,
agent_id,
exaau_token.token,
)
# Exchange token for observability if auth handler is configured
if self.auth_handler_name:
exaau_token = await self.agent_app.auth.exchange_token(
context,
scopes=get_observability_authentication_scope(),
auth_handler_id=self.auth_handler_name,
)

# Cache the agentic token for Agent 365 Observability exporter use
cache_agentic_token(
tenant_id,
agent_id,
exaau_token.token,
)

user_message = context.activity.text or ""
logger.info(f"📨 Processing message: '{user_message}'")
Expand Down Expand Up @@ -208,7 +217,7 @@ def create_auth_configuration(self) -> AgentAuthConfiguration | None:

if environ.get("BEARER_TOKEN"):
logger.info(
"🔑 BEARER_TOKEN present but incomplete app registration; continuing in anonymous dev mode"
"🔑 BEARER_TOKEN present - will use for MCP server authentication"
)
else:
logger.warning("⚠️ No authentication env vars found; running anonymous")
Expand Down