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
6 changes: 3 additions & 3 deletions .github/workflows/python-claude-sample.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ jobs:
import agent_interface
import local_authentication_options
import token_cache
import observability_config
import turn_context_utils
import mcp_tool_registration_service
import observability_config
import turn_context_utils
import mcp_tool_registration_service
print('✅ All imports validated successfully')
except ImportError as e:
print(f'❌ Import validation failed: {e}')
Expand Down
106 changes: 37 additions & 69 deletions python/claude/sample-agent/.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -12,58 +12,32 @@ CLAUDE_MODEL=claude-sonnet-4-20250514


# =============================================================================
# MCP (Model Context Protocol) CONFIGURATION (Optional)
# AUTHENTICATION
# =============================================================================

# Environment label for MCP tooling (informational only)
# NOTE: The current runtime does NOT read ENVIRONMENT to control MCP discovery.
# MCP servers are discovered based on the MCP SDK behavior, not this setting.
ENVIRONMENT=Development

# MCP Server Host
MCP_SERVER_HOST=

# MCP Platform Endpoint
MCP_PLATFORM_ENDPOINT=

# =============================================================================
# MICROSOFT 365 AGENTS SDK CONFIGURATION
# =============================================================================

# Agent ID (required for agentic authentication)
AGENT_ID=your-agent-id

# Environment ID (optional, defaults to prod)
# Options: dev, test, preprod, prod
ENVIRONMENT_ID=prod

# =============================================================================
# AUTHENTICATION OPTIONS
# =============================================================================
# Auth handler name (required for devtunnel/deployed modes)
# Set to AGENTIC to enable token exchange for Graph, MCP, and observability.
# Leave empty for playground mode.
AUTH_HANDLER_NAME=AGENTIC

# Use agentic authentication (optional, defaults to false)
# Set to "true" to use agentic authentication with M365 Agents SDK
# Use agentic authentication for MCP (optional, defaults to true)
# Set to "false" to use a static bearer token for MCP instead
USE_AGENTIC_AUTH=true

AUTH_HANDLER_NAME=AGENTIC

# Bearer token (required if not using client credentials)
# Use for development/testing without full app registration
# Bearer token (required for playground/devtunnel modes)
# Used for local development authentication with Agents Playground and dev tunnel
BEARER_TOKEN=

# Agentic authentication scope (required if USE_AGENTIC_AUTH=true)
# Example: https://api.powerplatform.com/.default
AGENTIC_AUTH_SCOPE=https://api.powerplatform.com/.default

# =============================================================================
# AGENT365 AGENTIC AUTHENTICATION CONFIGURATION
# SERVICE CONNECTION
# =============================================================================
# Client credentials used by both the SDK (via load_configuration_from_env) and
# the sample code (for Bot Framework JWT validation in create_auth_configuration).

# Service connection settings for Agent365
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID=
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET=
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID=
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__SCOPES=
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__SCOPES=5a807f24-c9de-44ee-a3a7-329e88a00ffc/.default

# Agent application user authorization settings
AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__TYPE=AgenticUserAuthorization
Expand All @@ -72,59 +46,53 @@ AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__SCOPES=https:/
AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__ALTERNATEBLUEPRINTCONNECTIONNAME=https://graph.microsoft.com/.default

# Connections map configuration
CONNECTIONSMAP_0_SERVICEURL=*
CONNECTIONSMAP_0_CONNECTION=SERVICE_CONNECTION
CONNECTIONSMAP__0__SERVICEURL=*
CONNECTIONSMAP__0__CONNECTION=SERVICE_CONNECTION

# =============================================================================
# CLIENT CREDENTIALS AUTHENTICATION (Optional)
# MCP (Model Context Protocol) CONFIGURATION
# =============================================================================
# For production deployments, use client credentials instead of bearer token

# Azure AD Client ID
CLIENT_ID=

# Azure AD Tenant ID
TENANT_ID=

# Azure AD Client Secret
CLIENT_SECRET=
# Environment label (optional, defaults to Production)
# Set to "Development" to allow BEARER_TOKEN for MCP auth
ENVIRONMENT=Development

WEBSITE_INSTANCE_ID=
# MCP Platform Endpoint (optional, defaults to https://agent365.svc.cloud.microsoft)
MCP_PLATFORM_ENDPOINT=

# =============================================================================
# SERVER CONFIGURATION
# AGENT IDENTITY
# =============================================================================

# Port to run the server on (optional, defaults to 3978)
PORT=3978
# Agent ID (optional, defaults to "claude-agent")
# Fallback for MCP server discovery and observability when the activity's
# recipient fields are not populated.
AGENT_ID=

# Environment ID (optional, defaults to prod)
ENVIRONMENT_ID=prod

# =============================================================================
# LOGGING CONFIGURATION
# SERVER
# =============================================================================

# Logging level (optional, defaults to INFO)
# Options: DEBUG, INFO, WARNING, ERROR, CRITICAL
LOG_LEVEL=INFO
# Port to run the server on (optional, defaults to 3978)
PORT=3978

# =============================================================================
# OBSERVABILITY CONFIGURATION (Optional)
# OBSERVABILITY
# =============================================================================

# Enable observability tracing (set to true to track agent operations)
ENABLE_OBSERVABILITY=true
# Enable Agent 365 Observability Exporter (optional, defaults to false)
# Set to "true" to export telemetry to Agent 365 backend for production monitoring
ENABLE_A365_OBSERVABILITY_EXPORTER=false

# Service name for observability
OBSERVABILITY_SERVICE_NAME=claude-agent

# Service namespace for observability
# Service namespace for observability (optional, defaults to agent365-samples)
OBSERVABILITY_SERVICE_NAMESPACE=agent365-samples

# Enable Agent 365 Observability Exporter (optional, defaults to false)
# Set to "true" to export telemetry to Agent 365 backend for production monitoring
ENABLE_A365_OBSERVABILITY_EXPORTER=false

OTEL_LOG_LEVEL=debug

# Python environment (influences target cluster/category)
# Options: development, production
PYTHON_ENVIRONMENT=development
115 changes: 25 additions & 90 deletions python/claude/sample-agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,12 @@
create_agent_details,
create_invoke_agent_details,
create_caller_details,
create_tenant_details,
create_request,
build_baggage_builder,
)

# MCP Tooling Services
from mcp_tool_registration_service import McpToolRegistrationService, MCPToolDefinition

# MCP Tooling available for Claude SDK
MCP_AVAILABLE = True
from mcp_tool_registration_service import McpToolRegistrationService

# Notifications
from microsoft_agents_a365.notifications.agent_notification import NotificationTypes
Expand Down Expand Up @@ -176,112 +172,53 @@ def _create_client(self):
# <McpTooling>

def _initialize_mcp_services(self):
"""
Initialize MCP services for tool discovery.

Uses McpToolRegistrationService to:
- Discover MCP servers via McpToolServerConfigurationService (production)
- Fallback to ToolingManifest.json (development)
- Connect to MCP servers and fetch available tools
- Provide tool execution capabilities
"""
"""Initialize MCP services for tool discovery."""
self.mcp_service = McpToolRegistrationService(logger=self.logger)
self.mcp_tools: list[MCPToolDefinition] = []
logger.info("✅ MCP tool registration service initialized")
logger.info("MCP tool registration service initialized")

async def setup_mcp_servers(
self, auth: Authorization, auth_handler_name: str, context: TurnContext
):
"""
Discover MCP servers, connect to them, and fetch available tools.

This method uses the McpToolRegistrationService to:
1. Authenticate with the MCP platform
2. Discover available MCP servers via SDK or ToolingManifest.json fallback
3. Connect to each server
4. Fetch and index all available tools

Discover MCP servers via the SDK and register them for Claude.

Args:
auth: Authorization for token exchange
auth_handler_name: Name of the auth handler
context: Turn context from M365 SDK
"""
try:
# Get agentic_app_id from context or environment
agentic_app_id = None
if context.activity and context.activity.recipient:
agentic_app_id = context.activity.recipient.agentic_app_id
if not agentic_app_id:
agentic_app_id = os.getenv("AGENT_ID", "claude-agent")

# Get auth token - prefer token exchange for proper MCP authentication
# When USE_AGENTIC_AUTH=true, the service will exchange token with proper scopes
# Otherwise, we fall back to the static bearer token (for local dev)
# Get auth token for local dev, or let the SDK exchange one
use_agentic_auth = os.getenv("USE_AGENTIC_AUTH", "true").lower() == "true"
auth_token = None

if not use_agentic_auth:
# Use static bearer token for local development
auth_token = self.auth_options.bearer_token
logger.info("ℹ️ Using static bearer token for MCP (USE_AGENTIC_AUTH=false)")
else:
# Let the MCP service exchange the token with proper scopes
logger.info("ℹ️ MCP will use token exchange for authentication")

# Discover and connect to MCP servers
self.mcp_tools = await self.mcp_service.discover_and_connect_servers(
agentic_app_id=agentic_app_id,
logger.info("Using static bearer token for MCP (USE_AGENTIC_AUTH=false)")

await self.mcp_service.discover_and_connect_servers(
agentic_app_id="", # resolved by SDK via Utility.resolve_agent_identity
auth=auth,
auth_handler_name=auth_handler_name,
context=context,
auth_token=auth_token, # None = service will exchange token
auth_token=auth_token,
)

if self.mcp_tools:
logger.info(f"✅ {len(self.mcp_tools)} MCP tool(s) available:")
for tool in self.mcp_tools:
logger.info(f" 🔧 {tool.name}: {tool.description[:50]}...")

servers = self.mcp_service.get_mcp_servers_for_claude()
if servers:
logger.info("%d MCP server(s) registered: %s", len(servers), list(servers.keys()))
else:
logger.info("ℹ️ No MCP tools discovered")
logger.info("No MCP servers discovered")

except Exception as e:
logger.error(f"Error setting up MCP servers: {e}")
self.mcp_tools = []

def get_mcp_tool_names(self) -> list[str]:
"""
Get list of available MCP tool names.

Returns:
List of tool names that can be called
"""
return self.mcp_service.get_available_tool_names()

def get_mcp_tools_for_claude(self) -> list[dict]:
"""
Get MCP tool definitions in Claude's expected format.

Returns:
List of tool definitions compatible with Claude's tool use
"""
return self.mcp_service.get_tools_for_claude()

def get_mcp_servers_for_claude(self) -> dict:
"""
Get MCP servers in Claude SDK's McpHttpServerConfig format.

Returns:
Dict mapping server names to server configs
"""
"""Get MCP servers in Claude SDK's McpHttpServerConfig format."""
return self.mcp_service.get_mcp_servers_for_claude()

def get_allowed_mcp_tool_names(self) -> list[str]:
"""
Get MCP tool names in Claude's mcp__<server>__<tool> format.

Returns:
List of prefixed tool names for allowed_tools
"""
"""Get MCP tool names in Claude's mcp__<server>__<tool> format."""
return self.mcp_service.get_allowed_tool_names_for_claude()

# </McpTooling>
Expand Down Expand Up @@ -332,19 +269,18 @@ async def process_user_message(
logger.warning("⚠️ Observability not configured, spans may not be exported")

# Use BaggageBuilder to set contextual information that flows through all spans
with build_baggage_builder(context, ctx_details.correlation_id).build():
with build_baggage_builder(context).build():
# Create observability details using shared utilities (CrewAI pattern)
agent_details = create_agent_details(ctx_details)
caller_details = create_caller_details(ctx_details)
tenant_details = create_tenant_details(ctx_details)
request = create_request(ctx_details, message)
invoke_details = create_invoke_agent_details(ctx_details)

# Use context manager pattern per documentation
with InvokeAgentScope.start(
invoke_agent_details=invoke_details,
tenant_details=tenant_details,
request=request,
scope_details=invoke_details,
agent_details=agent_details,
caller_details=caller_details,
) as invoke_scope:
# Record input message
Expand All @@ -359,10 +295,9 @@ async def process_user_message(
)

with InferenceScope.start(
request=request,
details=inference_details,
agent_details=agent_details,
tenant_details=tenant_details,
request=request,
) as inference_scope:
# Get MCP servers in Claude SDK format
mcp_servers = self.get_mcp_servers_for_claude()
Expand Down Expand Up @@ -469,9 +404,9 @@ async def process_user_message(

# Start ExecuteToolScope and track it
tool_scope = ExecuteToolScope.start(
request=request,
details=tool_call_details,
agent_details=agent_details,
tenant_details=tenant_details,
)
active_tool_scopes[tool_call_id] = {
"scope": tool_scope,
Expand Down
Loading
Loading