From ddbf2000aaab7819a6f2773b5613eee370fdd2c5 Mon Sep 17 00:00:00 2001 From: Grant Harris Date: Fri, 3 Apr 2026 14:57:11 -0700 Subject: [PATCH 1/7] fixes for python claude sample --- .github/workflows/python-claude-sample.yml | 6 +-- python/claude/sample-agent/agent.py | 13 ++--- .../claude/sample-agent/turn_context_utils.py | 50 +++++-------------- 3 files changed, 21 insertions(+), 48 deletions(-) diff --git a/.github/workflows/python-claude-sample.yml b/.github/workflows/python-claude-sample.yml index d371f8ff..9564571c 100644 --- a/.github/workflows/python-claude-sample.yml +++ b/.github/workflows/python-claude-sample.yml @@ -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}') diff --git a/python/claude/sample-agent/agent.py b/python/claude/sample-agent/agent.py index a09e76f6..ba3f3c38 100644 --- a/python/claude/sample-agent/agent.py +++ b/python/claude/sample-agent/agent.py @@ -73,7 +73,6 @@ create_agent_details, create_invoke_agent_details, create_caller_details, - create_tenant_details, create_request, build_baggage_builder, ) @@ -332,19 +331,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 @@ -359,10 +357,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() @@ -469,9 +466,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, diff --git a/python/claude/sample-agent/turn_context_utils.py b/python/claude/sample-agent/turn_context_utils.py index 7fa9c152..84acb31b 100644 --- a/python/claude/sample-agent/turn_context_utils.py +++ b/python/claude/sample-agent/turn_context_utils.py @@ -17,13 +17,12 @@ from microsoft_agents.hosting.core import TurnContext from microsoft_agents_a365.observability.core import ( AgentDetails, - TenantDetails, + InvokeAgentScopeDetails, Request, - ExecutionType, - InvokeAgentDetails, ) from microsoft_agents_a365.observability.core.middleware.baggage_builder import BaggageBuilder from microsoft_agents_a365.observability.core.models.caller_details import CallerDetails +from microsoft_agents_a365.observability.core.models.user_details import UserDetails from microsoft_agents_a365.observability.hosting.scope_helpers.populate_baggage import populate @@ -110,13 +109,11 @@ def create_agent_details(details: TurnContextDetails, description: str = "AI age """ return AgentDetails( agent_id=details.agent_id, - conversation_id=details.conversation_id, agent_name=details.agent_name, agent_description=description, tenant_id=details.tenant_id, - agent_upn=details.agent_upn, + agentic_user_id=details.agent_auid, agent_blueprint_id=details.agent_blueprint_id, - agent_auid=details.agent_auid, ) @@ -131,26 +128,13 @@ def create_caller_details(details: TurnContextDetails) -> CallerDetails: CallerDetails for observability """ return CallerDetails( - caller_id=details.caller_id or "unknown-caller", - caller_upn=details.caller_name or "unknown-user", - caller_user_id=details.caller_aad_object_id or details.caller_id or "unknown-user-id", - caller_name=details.caller_name, + user_details=UserDetails( + user_id=details.caller_aad_object_id or details.caller_id or "unknown-user-id", + user_name=details.caller_name, + ), ) -def create_tenant_details(details: TurnContextDetails) -> TenantDetails: - """ - Create TenantDetails from extracted TurnContextDetails. - - Args: - details: The extracted turn context details - - Returns: - TenantDetails for observability - """ - return TenantDetails(tenant_id=details.tenant_id) - - def create_request(details: TurnContextDetails, message: str) -> Request: """ Create a Request from extracted TurnContextDetails and message. @@ -164,42 +148,34 @@ def create_request(details: TurnContextDetails, message: str) -> Request: """ return Request( content=message, - execution_type=ExecutionType.HUMAN_TO_AGENT, session_id=details.conversation_id, + conversation_id=details.conversation_id, ) -def create_invoke_agent_details(details: TurnContextDetails, description: str = "AI agent powered by Anthropic Claude Agent SDK") -> InvokeAgentDetails: +def create_invoke_agent_details(details: TurnContextDetails) -> InvokeAgentScopeDetails: """ - Create InvokeAgentDetails from extracted TurnContextDetails. + Create InvokeAgentScopeDetails from extracted TurnContextDetails. Args: details: The extracted turn context details - description: Description of the agent Returns: - InvokeAgentDetails for observability + InvokeAgentScopeDetails for observability """ - agent_details = create_agent_details(details, description) - return InvokeAgentDetails( - details=agent_details, - session_id=details.conversation_id, - ) + return InvokeAgentScopeDetails() -def build_baggage_builder(context: TurnContext, correlation_id: Optional[str] = None) -> BaggageBuilder: +def build_baggage_builder(context: TurnContext) -> BaggageBuilder: """ Build a BaggageBuilder populated from TurnContext activity. Args: context: The TurnContext from the Microsoft Agents SDK - correlation_id: Optional correlation id to add to baggage Returns: Populated BaggageBuilder instance """ builder = BaggageBuilder() populate(builder, context) - if correlation_id: - builder.correlation_id(correlation_id) return builder From ce94df18e2fabb72fcc55b6f5b8e492609f16792 Mon Sep 17 00:00:00 2001 From: Grant Harris Date: Tue, 14 Apr 2026 12:06:40 -0700 Subject: [PATCH 2/7] fix agent ID bug --- .../claude/sample-agent/host_agent_server.py | 38 +++++++++++-------- .../claude/sample-agent/turn_context_utils.py | 5 ++- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/python/claude/sample-agent/host_agent_server.py b/python/claude/sample-agent/host_agent_server.py index b4869b82..d256bdb8 100644 --- a/python/claude/sample-agent/host_agent_server.py +++ b/python/claude/sample-agent/host_agent_server.py @@ -155,20 +155,24 @@ async def on_installation_update(context: TurnContext, _: TurnState): getattr(from_prop, "id", "(unknown)") if from_prop else "(unknown)", ) if action == "add": - await context.send_activity("Thank you for hiring me! Looking forward to assisting you in your professional journey!") + try: + await context.send_activity("Thank you for hiring me! Looking forward to assisting you in your professional journey!") + except Exception as e: + logger.warning("Could not send welcome message: %s", e) elif action == "remove": - await context.send_activity("Thank you for your time, I enjoyed working with you.") + try: + await context.send_activity("Thank you for your time, I enjoyed working with you.") + except Exception as e: + logger.warning("Could not send remove reply: %s", e) @self.agent_app.activity("message", **handler_config) async def on_message(context: TurnContext, _: TurnState): """Handle all messages with the hosted agent""" try: - # Ensure the agent is available - if not self.agent_instance: - error_msg = "❌ Sorry, the agent is not available." - logger.error(error_msg) - await context.send_activity(error_msg) + result = await self._validate_agent_and_setup_context(context) + if result is None: return + tenant_id, agent_id = result user_message = context.activity.text or "" logger.info(f"📨 Processing message: '{user_message}'") @@ -309,13 +313,13 @@ async def _handle_notification_with_agent( response = await self.agent_instance.handle_agent_notification_activity( notification_activity, self.agent_app.auth, context, self.auth_handler_name ) - + # For email notifications, wrap response in EmailResponse entity if notification_activity.notification_type == NotificationTypes.EMAIL_NOTIFICATION: response_activity = EmailResponse.create_email_response_activity(response) await context.send_activity(response_activity) return - + # Send the response for other notification types await context.send_activity(response) @@ -331,7 +335,7 @@ async def _validate_agent_and_setup_context(self, context: TurnContext): """ # Extract tenant and agent IDs tenant_id = context.activity.recipient.tenant_id if context.activity.recipient else None - agent_id = context.activity.recipient.agentic_app_id if context.activity.recipient else None + agent_id = context.activity.get_agentic_instance_id() # Ensure agent is available if not self.agent_instance: @@ -358,7 +362,7 @@ async def _setup_observability_token( """ if not OBSERVABILITY_AVAILABLE: return - + try: from microsoft_agents_a365.runtime.environment_utils import ( get_observability_authentication_scope, @@ -367,7 +371,7 @@ async def _setup_observability_token( exchange_kwargs = {} if self.auth_handler_name: exchange_kwargs["auth_handler_id"] = self.auth_handler_name - + exaau_token = await self.agent_app.auth.exchange_token( context, scopes=get_observability_authentication_scope(), @@ -397,12 +401,14 @@ async def initialize_agent(self): def create_auth_configuration(self) -> AgentAuthConfiguration | None: """Create authentication configuration based on available environment variables.""" - client_id = environ.get("CLIENT_ID") - tenant_id = environ.get("TENANT_ID") - client_secret = environ.get("CLIENT_SECRET") + # Read from the CONNECTIONS service-connection settings (canonical source) + # to avoid duplicating CLIENT_ID / TENANT_ID / CLIENT_SECRET. + client_id = environ.get("CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID") + tenant_id = environ.get("CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID") + client_secret = environ.get("CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET") if client_id and tenant_id and client_secret: - logger.info("🔒 Using Client Credentials authentication (CLIENT_ID/TENANT_ID provided)") + logger.info("🔒 Using Client Credentials authentication") try: return AgentAuthConfiguration( client_id=client_id, diff --git a/python/claude/sample-agent/turn_context_utils.py b/python/claude/sample-agent/turn_context_utils.py index 84acb31b..709cc192 100644 --- a/python/claude/sample-agent/turn_context_utils.py +++ b/python/claude/sample-agent/turn_context_utils.py @@ -63,7 +63,10 @@ def extract_turn_context_details(context: TurnContext) -> TurnContextDetails: # Extract agent details from recipient (ChannelAccount) tenant_id = recipient.tenant_id if recipient else None - agent_id = getattr(recipient, "id", None) if recipient else None + # Use get_agentic_instance_id() (recipient.agentic_app_id) for agent_id + agent_id = activity.get_agentic_instance_id() + if not agent_id: + agent_id = getattr(recipient, "id", None) if recipient else None if not agent_id: agent_id = os.getenv("AGENT_ID", "claude-agent") agent_name = getattr(recipient, "name", None) if recipient else None From dfd3737fee2f79a37d32e719ddbadbeced1177bb Mon Sep 17 00:00:00 2001 From: Grant Harris Date: Tue, 14 Apr 2026 12:06:57 -0700 Subject: [PATCH 3/7] update .env.template --- python/claude/sample-agent/.env.template | 106 ++++++++--------------- 1 file changed, 37 insertions(+), 69 deletions(-) diff --git a/python/claude/sample-agent/.env.template b/python/claude/sample-agent/.env.template index a38cd33e..70d1bcbc 100644 --- a/python/claude/sample-agent/.env.template +++ b/python/claude/sample-agent/.env.template @@ -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 @@ -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 From bf5b5fddd3a5ba7c44c33861978bf47d72f5ecce Mon Sep 17 00:00:00 2001 From: Grant Harris Date: Tue, 14 Apr 2026 12:11:49 -0700 Subject: [PATCH 4/7] update observability --- .../claude/sample-agent/host_agent_server.py | 39 ------------------- 1 file changed, 39 deletions(-) diff --git a/python/claude/sample-agent/host_agent_server.py b/python/claude/sample-agent/host_agent_server.py index d256bdb8..c08a4d00 100644 --- a/python/claude/sample-agent/host_agent_server.py +++ b/python/claude/sample-agent/host_agent_server.py @@ -56,7 +56,6 @@ # Observability imports (optional) try: - from microsoft_agents_a365.observability.core.config import configure as configure_observability from microsoft_agents_a365.observability.core.middleware.baggage_builder import BaggageBuilder from token_cache import get_cached_agentic_token, cache_agentic_token OBSERVABILITY_AVAILABLE = True @@ -569,44 +568,6 @@ def create_and_run_host(agent_class: type[AgentInterface], *agent_args, **agent_ if not check_agent_inheritance(agent_class): raise TypeError(f"Agent class {agent_class.__name__} must inherit from AgentInterface") - # Configure observability if available and enabled - if OBSERVABILITY_AVAILABLE: - enable_observability = os.getenv("ENABLE_OBSERVABILITY", "false").lower() in ("true", "1", "yes") - if enable_observability: - service_name = os.getenv("OBSERVABILITY_SERVICE_NAME", "generic-agent-host") - service_namespace = os.getenv("OBSERVABILITY_SERVICE_NAMESPACE", "agent365") - - # Token resolver for Agent365 exporter (optional) - def token_resolver(agent_id: str, tenant_id: str) -> str | None: - """Resolve authentication token for observability exporter""" - try: - logger.debug(f"Token resolver called for agent_id: {agent_id}, tenant_id: {tenant_id}") - # Use cached agentic token if available - cached_token = get_cached_agentic_token(tenant_id, agent_id) - if cached_token: - logger.debug("Using cached agentic token for observability") - return cached_token - logger.debug("No cached token available for observability") - return None - except Exception as e: - logger.warning(f"Error resolving token for observability: {e}") - return None - - try: - configure_observability( - service_name=service_name, - service_namespace=service_namespace, - token_resolver=token_resolver, - cluster_category=os.getenv("PYTHON_ENVIRONMENT", "development"), - ) - logger.info(f"✅ Observability configured: {service_name} ({service_namespace})") - except Exception as e: - logger.warning(f"⚠️ Failed to configure observability: {e}") - else: - logger.info("ℹ️ Observability disabled (ENABLE_OBSERVABILITY=false)") - else: - logger.debug("ℹ️ Observability packages not available") - # Create the host host = GenericAgentHost(agent_class, *agent_args, **agent_kwargs) From 4c284cdb0db7300b338dc185c0536573406cea9a Mon Sep 17 00:00:00 2001 From: Grant Harris Date: Tue, 14 Apr 2026 16:20:36 -0700 Subject: [PATCH 5/7] update to use SDK --- python/claude/sample-agent/agent.py | 102 +-- .../mcp_tool_registration_service.py | 713 +++--------------- python/claude/sample-agent/pyproject.toml | 12 +- 3 files changed, 124 insertions(+), 703 deletions(-) diff --git a/python/claude/sample-agent/agent.py b/python/claude/sample-agent/agent.py index ba3f3c38..a5a70063 100644 --- a/python/claude/sample-agent/agent.py +++ b/python/claude/sample-agent/agent.py @@ -78,10 +78,7 @@ ) # 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 @@ -175,112 +172,53 @@ def _create_client(self): # 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____ format. - - Returns: - List of prefixed tool names for allowed_tools - """ + """Get MCP tool names in Claude's mcp____ format.""" return self.mcp_service.get_allowed_tool_names_for_claude() # diff --git a/python/claude/sample-agent/mcp_tool_registration_service.py b/python/claude/sample-agent/mcp_tool_registration_service.py index fb732b54..88e8772e 100644 --- a/python/claude/sample-agent/mcp_tool_registration_service.py +++ b/python/claude/sample-agent/mcp_tool_registration_service.py @@ -2,182 +2,53 @@ # Licensed under the MIT License. """ -MCP Tool Registration Service for Claude Agent SDK +MCP Tool Registration Service for Claude Agent SDK. -This service provides MCP (Model Context Protocol) tool integration for Claude agents, -similar to the OpenAI extension but adapted for Claude's tool calling mechanism. - -Features: -- Discovers MCP servers from McpToolServerConfigurationService (production) or ToolingManifest.json (dev) -- Connects to MCP servers via Streamable HTTP -- Lists available tools from MCP servers -- Executes MCP tool calls and returns results -- Handles authentication and authorization +Thin wrapper around the Agent365 SDK's McpToolServerConfigurationService that +discovers MCP servers, builds properly authenticated headers, and exposes the +result in the format Claude SDK expects. """ -from typing import Dict, List, Optional, Any -from dataclasses import dataclass, field import logging -import os -import aiohttp -import asyncio +from typing import Dict, List, Optional from microsoft_agents.hosting.core import Authorization, TurnContext -from microsoft_agents_a365.tooling.utils.constants import Constants + +from microsoft_agents_a365.runtime.utility import Utility +from microsoft_agents_a365.tooling.models import ToolOptions from microsoft_agents_a365.tooling.services.mcp_tool_server_configuration_service import ( McpToolServerConfigurationService, ) +from microsoft_agents_a365.tooling.utils.constants import Constants from microsoft_agents_a365.tooling.utils.utility import ( get_mcp_platform_authentication_scope, ) -# Default MCP Platform endpoint (can be overridden via MCP_PLATFORM_ENDPOINT env var) -DEFAULT_MCP_PLATFORM_ENDPOINT = "https://agent365.svc.cloud.microsoft" - -# Production configuration -MCP_REQUEST_TIMEOUT_SECONDS = 30 -MCP_CONNECT_TIMEOUT_SECONDS = 10 -MCP_MAX_RETRIES = 2 -MCP_RETRY_DELAY_SECONDS = 1 - - -def get_mcp_platform_endpoint() -> str: - """Get the MCP platform endpoint from environment or use default.""" - endpoint = os.getenv("MCP_PLATFORM_ENDPOINT", "").strip() - return endpoint if endpoint else DEFAULT_MCP_PLATFORM_ENDPOINT - - -@dataclass -class MCPToolDefinition: - """Definition of an MCP tool""" - name: str - description: str - input_schema: Dict[str, Any] - server_url: str - server_name: str - -@dataclass -class MCPServerConnection: - """Information about a connected MCP server""" - name: str - url: str - headers: Dict[str, str] = field(default_factory=dict) - tools: List[MCPToolDefinition] = field(default_factory=list) - connected: bool = False - - -# Claude SDK MCP server config type -McpHttpServerConfig = Dict[str, Any] # {"type": "http", "url": str, "headers": dict} +# Claude SDK MCP server config type: {"type": "http", "url": str, "headers": dict} +McpHttpServerConfig = Dict[str, object] class McpToolRegistrationService: """ Service for managing MCP tools and servers for Claude agents. - - This service provides equivalent functionality to the OpenAI extension's - McpToolRegistrationService, but adapted for Claude Agent SDK which doesn't - have native MCP support. - - Discovery modes: - - Production: Uses McpToolServerConfigurationService to discover servers from Gateway - - Development: Falls back to ToolingManifest.json if SDK returns no servers + + Delegates all discovery and configuration to the SDK's + McpToolServerConfigurationService and exposes results in the + ``{server_name: {type, url, headers}}`` format that the Claude SDK expects. """ - + _orchestrator_name: str = "Claude" - def __init__(self, logger: Optional[logging.Logger] = None): - """ - Initialize the MCP Tool Registration Service for Claude. - - Args: - logger: Logger instance for logging operations. - """ + def __init__(self, logger: Optional[logging.Logger] = None) -> None: self._logger = logger or logging.getLogger(self.__class__.__name__) - self._connected_servers: List[MCPServerConnection] = [] - self._tools_by_name: Dict[str, MCPToolDefinition] = {} - self._auth_token: Optional[str] = None self._config_service = McpToolServerConfigurationService(logger=self._logger) + self._connected_servers: Dict[str, McpHttpServerConfig] = {} + self._allowed_tool_names: List[str] = [] - def _load_manifest_servers_fallback(self) -> List[Dict[str, Any]]: - """ - Load MCP server configurations directly from ToolingManifest.json. - - This is a fallback for local development when McpToolServerConfigurationService - cannot discover servers (e.g., no Gateway connection). - - Returns: - List of server configurations with name, url, scope, audience. - """ - import json - - servers = [] - manifest_path = os.path.join(os.getcwd(), "ToolingManifest.json") - - if not os.path.exists(manifest_path): - self._logger.debug(f"ToolingManifest.json not found at {manifest_path}") - return servers - - try: - with open(manifest_path, 'r') as f: - manifest = json.load(f) - - self._logger.info(f"📄 [Fallback] Loaded ToolingManifest.json") - - for server in manifest.get("mcpServers", []): - name = server.get("mcpServerName", server.get("mcpServerUniqueName", "unknown")) - url = server.get("url", "") - scope = server.get("scope", "") - audience = server.get("audience", "") - - if url: - servers.append({ - "name": name, - "url": url, - "scope": scope, - "audience": audience, - }) - self._logger.info(f" 📌 [Manifest] Server: {name} -> {url}") - - except Exception as e: - self._logger.error(f"Failed to load ToolingManifest.json: {e}") - - return servers - - def _build_full_url(self, server_path: str) -> str: - """ - Build a full URL from a server path. - - If the path is already a full URL (http/https), return as-is. - Otherwise, prepend the MCP platform endpoint. - - Args: - server_path: The server URL or relative path - - Returns: - Full URL to the MCP server - """ - if not server_path: - return "" - - # Already a full URL - if server_path.startswith("http://") or server_path.startswith("https://"): - return server_path - - # Build full URL from relative path - platform_endpoint = get_mcp_platform_endpoint() - - # Handle different path formats: - # - "/agents/servers/mcp_MailTools" -> prepend endpoint - # - "agents/servers/mcp_MailTools" -> prepend endpoint with / - # - "mcp_MailTools" -> assume it's under /agents/servers/ - path = server_path.lstrip("/") - - if not path.startswith("agents/"): - # Just a server name like "mcp_MailTools" - path = f"agents/servers/{path}" - - return f"{platform_endpoint.rstrip('/')}/{path}" + # ------------------------------------------------------------------ + # Discovery + # ------------------------------------------------------------------ async def discover_and_connect_servers( self, @@ -186,500 +57,112 @@ async def discover_and_connect_servers( auth_handler_name: str, context: TurnContext, auth_token: Optional[str] = None, - ) -> List[MCPToolDefinition]: + ) -> None: """ - Discover MCP servers using McpToolServerConfigurationService and connect to them. - - In production, uses the SDK service to discover servers from Gateway. - In development, falls back to ToolingManifest.json if SDK returns no servers. - + Discover MCP servers via the SDK and prepare them for Claude. + Args: agentic_app_id: The agent's application ID for server discovery. auth: Authorization handler for token exchange. auth_handler_name: Name of the authorization handler. context: Turn context for the current operation. auth_token: Optional pre-configured authentication token. - - Returns: - List of all available tool definitions from connected servers. """ - # Determine authentication mode (mutually exclusive, no fallbacks) - if auth_token: - self._auth_token = auth_token - self._logger.info("Using provided auth token for MCP authentication") - else: - environment = os.getenv("ENVIRONMENT", "Production").strip().lower() - bearer_token = (os.getenv("BEARER_TOKEN") or "").strip() - - if bearer_token: - # Bearer token mode (development only) - if environment != "development": - raise ValueError( - "BEARER_TOKEN is set but ENVIRONMENT is not 'development'. " - "Bearer tokens are only supported in development environments." - ) - self._auth_token = bearer_token - self._logger.info("Using BEARER_TOKEN authentication (development mode)") - elif auth_handler_name: - # Auth handler mode (production) - scopes = get_mcp_platform_authentication_scope() - self._logger.info(f"🔑 Attempting token exchange with scopes: {scopes}") - auth_result = await auth.exchange_token(context, scopes, auth_handler_name) - if not auth_result or not auth_result.token: - raise RuntimeError( - f"Auth handler '{auth_handler_name}' is configured but failed to provide a token. " - "Not falling back to unauthenticated mode." - ) - self._auth_token = auth_result.token - self._logger.info(f"Using auth handler: {auth_handler_name}") - else: - # No authentication configured - self._auth_token = None - self._logger.warning( - "No authentication configured - MCP servers requiring auth will be skipped" + # --- Authenticate ------------------------------------------------ + if auth_token is None or auth_token.strip() == "": + scopes = get_mcp_platform_authentication_scope() + self._logger.info("Exchanging token with scopes: %s", scopes) + auth_result = await auth.exchange_token(context, scopes, auth_handler_name) + if not auth_result or not auth_result.token: + raise RuntimeError( + f"Auth handler '{auth_handler_name}' failed to provide a token." ) + auth_token = auth_result.token + + # --- Discover servers via SDK ------------------------------------ + agentic_app_id = Utility.resolve_agent_identity(context, auth_token) + options = ToolOptions(orchestrator_name=self._orchestrator_name) - auth_token = self._auth_token - - # Get the MCP platform base URL for reference - platform_endpoint = get_mcp_platform_endpoint() - self._logger.info(f"🌐 MCP Platform endpoint: {platform_endpoint}") - - # Try to discover servers using McpToolServerConfigurationService (production path) - mcp_server_configs = [] + self._logger.info("Listing MCP tool servers for agent %s", agentic_app_id) try: - self._logger.info(f"🔍 Discovering MCP servers for agent {agentic_app_id}") - sdk_configs = await self._config_service.list_tool_servers( + server_configs = await self._config_service.list_tool_servers( agentic_app_id=agentic_app_id, - auth_token=auth_token if auth_token else None, + auth_token=auth_token, + options=options, ) - - # Convert SDK config objects to our format - for config in sdk_configs: - # Extract URL - try different attribute names the SDK might use - server_url = getattr(config, "url", None) or \ - getattr(config, "server_url", None) or \ - getattr(config, "endpoint", None) - - server_name = getattr(config, "mcp_server_name", None) or \ - getattr(config, "mcp_server_unique_name", None) or \ - getattr(config, "name", "unknown") - - # If URL is not a full URL, it might just be the server name/path - if not server_url: - # Use server name as path if no URL provided - server_url = getattr(config, "mcp_server_unique_name", None) or server_name - - # Build full URL - full_url = self._build_full_url(server_url) - - if full_url: - mcp_server_configs.append({ - "name": server_name, - "url": full_url, - }) - self._logger.info(f" 📌 [SDK] Server: {server_name} -> {full_url}") - - self._logger.info(f"📋 SDK discovered {len(mcp_server_configs)} MCP server(s)") - except Exception as e: - self._logger.warning(f"⚠️ McpToolServerConfigurationService failed: {e}") - - # Fallback to ToolingManifest.json if SDK returned no servers (development mode) - if not mcp_server_configs: - self._logger.info("📄 Falling back to ToolingManifest.json for server discovery") - mcp_server_configs = self._load_manifest_servers_fallback() - - self._logger.info(f"Found {len(mcp_server_configs)} MCP server configurations total") - - # Connect to each server and fetch tools - all_tools: List[MCPToolDefinition] = [] - - for server_config in mcp_server_configs: - try: - connection = await self._connect_to_server( - name=server_config["name"], - url=server_config["url"], - auth_token=auth_token, - ) - - if connection and connection.connected: - self._connected_servers.append(connection) - all_tools.extend(connection.tools) - - # Index tools by name for quick lookup - for tool in connection.tools: - self._tools_by_name[tool.name] = tool - - self._logger.info( - f"Connected to MCP server '{connection.name}' with " - f"{len(connection.tools)} tools" - ) - - except Exception as e: - self._logger.warning( - f"Failed to connect to MCP server {server_config['name']}: {e}" - ) - continue - - self._logger.info(f"Total {len(all_tools)} MCP tools available") - return all_tools + self._logger.warning("SDK server discovery failed: %s", e) + server_configs = [] + + if not server_configs: + self._logger.info("Falling back to ToolingManifest.json for server discovery") + server_configs = self._config_service._load_servers_from_manifest() + + self._logger.info("Loaded %d MCP server configurations", len(server_configs)) + + # --- Build headers (same logic as the SDK extensions) ------------ + headers: Dict[str, str] = { + Constants.Headers.AUTHORIZATION: ( + f"{Constants.Headers.BEARER_PREFIX} {auth_token}" + ), + Constants.Headers.USER_AGENT: Utility.get_user_agent_header( + self._orchestrator_name + ), + } - async def _connect_to_server( - self, - name: str, - url: str, - auth_token: str, - ) -> Optional[MCPServerConnection]: - """ - Connect to an MCP server and fetch its tools. - - Args: - name: Server display name. - url: Server URL endpoint. - auth_token: Authentication token. - - Returns: - MCPServerConnection with tools, or None if connection failed. - """ - # Check if this is a local server (no auth needed) - is_local = url.startswith("http://localhost") or url.startswith("http://127.0.0.1") - - if is_local: - headers = { - "Content-Type": "application/json", - } - self._logger.info(f"🏠 Connecting to local MCP server: {url}") - else: - if not auth_token: - self._logger.warning(f"⚠️ Skipping remote server {name} - no auth token") - return None - headers = { - Constants.Headers.AUTHORIZATION: f"{Constants.Headers.BEARER_PREFIX} {auth_token}", - "User-Agent": f"Claude-Agent-SDK/1.0 ({self._orchestrator_name})", - "Content-Type": "application/json", - } - self._logger.info(f"☁️ Connecting to remote MCP server: {url}") - - connection = MCPServerConnection( - name=name, - url=url, - headers=headers, - ) - - try: - # Fetch available tools from the server - tools = await self._list_server_tools(url, headers, name) - connection.tools = tools - connection.connected = True - return connection - - except Exception as e: - self._logger.error(f"Failed to connect to MCP server {name} at {url}: {e}") - return None + # --- Register each server in Claude format ---------------------- + self._connected_servers = {} + self._allowed_tool_names = [] - async def _parse_sse_response(self, response) -> Dict[str, Any]: - """ - Parse Server-Sent Events (SSE) response from MCP server. - - Agent 365 MCP servers use Streamable HTTP transport which returns - responses as SSE with content-type: text/event-stream. - - Args: - response: aiohttp response object - - Returns: - Parsed JSON-RPC result from the SSE stream - """ - import json - - content_type = response.headers.get('Content-Type', '') - - # If it's regular JSON, parse directly - if 'application/json' in content_type: - return await response.json() - - # Handle SSE (text/event-stream) - result = None - async for line in response.content: - line_str = line.decode('utf-8').strip() - - # Skip empty lines and comments - if not line_str or line_str.startswith(':'): - continue - - # Parse SSE data lines - if line_str.startswith('data:'): - data_str = line_str[5:].strip() - if data_str: - try: - parsed = json.loads(data_str) - # Look for the JSON-RPC result - if 'result' in parsed or 'error' in parsed: - result = parsed - break - # Some servers send the result directly - if 'jsonrpc' in parsed: - result = parsed - break - except json.JSONDecodeError: - continue - # Handle non-prefixed JSON lines (some SSE implementations) - elif line_str.startswith('{'): - try: - parsed = json.loads(line_str) - if 'result' in parsed or 'jsonrpc' in parsed: - result = parsed - break - except json.JSONDecodeError: - continue - - if result is None: - raise Exception("No valid JSON-RPC response found in SSE stream") - - return result - - async def _list_server_tools( - self, - server_url: str, - headers: Dict[str, str], - server_name: str, - ) -> List[MCPToolDefinition]: - """ - List available tools from an MCP server. - - Args: - server_url: The MCP server URL endpoint. - headers: HTTP headers including authorization. - server_name: Server name for tool attribution. - - Returns: - List of tool definitions. - """ - payload = { - "jsonrpc": "2.0", - "id": 1, - "method": "tools/list", - "params": {} - } - - # Add Accept header for SSE - request_headers = {**headers, "Accept": "text/event-stream, application/json"} - - # Configure timeout for production - timeout = aiohttp.ClientTimeout( - total=MCP_REQUEST_TIMEOUT_SECONDS, - connect=MCP_CONNECT_TIMEOUT_SECONDS - ) - - async with aiohttp.ClientSession(timeout=timeout) as session: - async with session.post(server_url, headers=request_headers, json=payload) as response: - if response.status == 200: - result = await self._parse_sse_response(response) - tools_data = result.get("result", {}).get("tools", []) - - tools = [] - for tool_data in tools_data: - tool = MCPToolDefinition( - name=tool_data.get("name", ""), - description=tool_data.get("description", ""), - input_schema=tool_data.get("inputSchema", {}), - server_url=server_url, - server_name=server_name, - ) - tools.append(tool) - - self._logger.debug(f"Listed {len(tools)} tools from {server_name}") - return tools - else: - error_text = await response.text() - raise Exception(f"Failed to list tools: {response.status} - {error_text}") - - async def call_tool( - self, - tool_name: str, - arguments: Dict[str, Any], - ) -> str: - """ - Execute an MCP tool and return the result. - - Includes retry logic for transient failures and proper timeout handling. - - Args: - tool_name: Name of the tool to execute. - arguments: Tool arguments as a dictionary. - - Returns: - The tool result as a string. - - Raises: - ValueError: If the tool is not found or not connected. - Exception: If the tool call fails after retries. - """ - if tool_name not in self._tools_by_name: - available = list(self._tools_by_name.keys())[:10] # Limit for logging - raise ValueError(f"Tool '{tool_name}' not found. Available tools: {available}...") - - tool = self._tools_by_name[tool_name] - - # Find the connection for this tool - connection = None - for conn in self._connected_servers: - if conn.url == tool.server_url: - connection = conn - break - - if not connection: - raise ValueError(f"No connection found for tool '{tool_name}'") - - payload = { - "jsonrpc": "2.0", - "id": 1, - "method": "tools/call", - "params": { - "name": tool_name, - "arguments": arguments - } - } - - self._logger.info(f"Calling MCP tool '{tool_name}' on server '{connection.name}'") - self._logger.debug(f"Tool arguments: {arguments}") - - # Add Accept header for SSE - request_headers = {**connection.headers, "Accept": "text/event-stream, application/json"} - - # Configure timeout for production - timeout = aiohttp.ClientTimeout( - total=MCP_REQUEST_TIMEOUT_SECONDS, - connect=MCP_CONNECT_TIMEOUT_SECONDS - ) - - last_error = None - for attempt in range(MCP_MAX_RETRIES + 1): - try: - async with aiohttp.ClientSession(timeout=timeout) as session: - async with session.post( - connection.url, - headers=request_headers, - json=payload - ) as response: - if response.status == 200: - result = await self._parse_sse_response(response) - - # Extract content from MCP response - content = result.get("result", {}).get("content", []) - if content and len(content) > 0: - # Handle different content types - first_content = content[0] - if isinstance(first_content, dict): - result_text = first_content.get("text", str(first_content)) - else: - result_text = str(first_content) - - self._logger.info(f"MCP tool '{tool_name}' executed successfully") - return result_text - - return str(result.get("result", "")) - - elif response.status in (502, 503, 504): - # Retryable server errors - error_text = await response.text() - last_error = Exception(f"MCP server error: {response.status} - {error_text}") - self._logger.warning(f"Retryable error on attempt {attempt + 1}: {response.status}") - else: - # Non-retryable error - error_text = await response.text() - raise Exception(f"MCP tool call failed: {response.status} - {error_text}") - - except asyncio.TimeoutError: - last_error = Exception(f"MCP tool call timed out after {MCP_REQUEST_TIMEOUT_SECONDS}s") - self._logger.warning(f"Timeout on attempt {attempt + 1} for tool '{tool_name}'") - except aiohttp.ClientError as e: - last_error = e - self._logger.warning(f"Connection error on attempt {attempt + 1}: {e}") - - # Wait before retry (except on last attempt) - if attempt < MCP_MAX_RETRIES: - await asyncio.sleep(MCP_RETRY_DELAY_SECONDS) - - # All retries exhausted - self._logger.error(f"MCP tool '{tool_name}' failed after {MCP_MAX_RETRIES + 1} attempts") - raise last_error or Exception("MCP tool call failed") - - def get_tools_for_claude(self) -> List[Dict[str, Any]]: - """ - Get tool definitions in Claude's expected format. - - Returns: - List of tool definitions compatible with Claude's tool use format. - """ - claude_tools = [] - - for tool in self._tools_by_name.values(): - claude_tool = { - "name": tool.name, - "description": tool.description, - "input_schema": tool.input_schema, + for config in server_configs: + server_name = config.mcp_server_name or config.mcp_server_unique_name + server_url = config.url + + self._connected_servers[server_name] = { + "type": "http", + "url": server_url, + "headers": dict(headers), } - claude_tools.append(claude_tool) - - return claude_tools + + # Allow all tools from this server via wildcard pattern + self._allowed_tool_names.append(f"mcp__{server_name}__*") + + self._logger.info( + "Registered MCP server '%s' at %s", server_name, server_url + ) + + # ------------------------------------------------------------------ + # Claude SDK accessors + # ------------------------------------------------------------------ def get_mcp_servers_for_claude(self) -> Dict[str, McpHttpServerConfig]: """ Get MCP server configurations in Claude SDK's expected format. - - Claude SDK expects mcp_servers as: - { - "server_name": { - "type": "http", - "url": "https://...", - "headers": {"Authorization": "Bearer ..."} - } - } - + Returns: - Dict mapping server names to McpHttpServerConfig objects. + Dict mapping server names to ``{type, url, headers}`` configs. """ - mcp_servers: Dict[str, McpHttpServerConfig] = {} - - for connection in self._connected_servers: - mcp_servers[connection.name] = { - "type": "http", - "url": connection.url, - "headers": connection.headers, - } - - return mcp_servers + return dict(self._connected_servers) def get_allowed_tool_names_for_claude(self) -> List[str]: """ - Get tool names in Claude's MCP format: mcp____ - + Get tool names in Claude's MCP format: ``mcp____``. + Returns: - List of tool names prefixed for Claude MCP usage. + List of prefixed tool names for ``allowed_tools``. """ - allowed_tools = [] - - for tool in self._tools_by_name.values(): - # Claude MCP tool naming convention: mcp____ - prefixed_name = f"mcp__{tool.server_name}__{tool.name}" - allowed_tools.append(prefixed_name) - - return allowed_tools + return list(self._allowed_tool_names) def get_available_tool_names(self) -> List[str]: - """ - Get list of available MCP tool names. - - Returns: - List of tool names that can be called. - """ - return list(self._tools_by_name.keys()) + """Get list of connected MCP server names.""" + return list(self._connected_servers.keys()) + + # ------------------------------------------------------------------ + # Cleanup + # ------------------------------------------------------------------ - async def cleanup(self): + async def cleanup(self) -> None: """Clean up all connected MCP servers.""" - self._connected_servers = [] - self._tools_by_name = {} - self._auth_token = None + self._connected_servers = {} + self._allowed_tool_names = [] self._logger.info("MCP tool registration service cleaned up") diff --git a/python/claude/sample-agent/pyproject.toml b/python/claude/sample-agent/pyproject.toml index bc8c56b7..a0f51531 100644 --- a/python/claude/sample-agent/pyproject.toml +++ b/python/claude/sample-agent/pyproject.toml @@ -12,12 +12,12 @@ dependencies = [ "microsoft-agents-authentication-msal>=0.7.0", "microsoft-agents-activity>=0.7.0", - # Agent 365 packages (use stable versions from PyPI) - "microsoft_agents_a365_observability_core>=0.1.0", - "microsoft_agents_a365_observability_hosting>=0.1.0", - "microsoft_agents_a365_notifications>=0.1.0", - "microsoft_agents_a365_tooling>=0.1.0", - "microsoft_agents_a365_runtime>=0.1.0", + # Agent 365 packages (using local SDK from Agent365-python repo) + "microsoft-agents-a365-observability-core>=0.1.0", + "microsoft-agents-a365-observability-hosting>=0.1.0", + "microsoft-agents-a365-notifications>=0.1.0", + "microsoft-agents-a365-tooling>=0.1.0", + "microsoft-agents-a365-runtime>=0.1.0", # Core dependencies "python-dotenv", From 1206d69a662e5b4d883f37c9266fa16572bc3fcc Mon Sep 17 00:00:00 2001 From: Grant Harris Date: Tue, 14 Apr 2026 16:22:55 -0700 Subject: [PATCH 6/7] update comment --- python/claude/sample-agent/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/claude/sample-agent/pyproject.toml b/python/claude/sample-agent/pyproject.toml index a0f51531..9db12f8c 100644 --- a/python/claude/sample-agent/pyproject.toml +++ b/python/claude/sample-agent/pyproject.toml @@ -12,7 +12,7 @@ dependencies = [ "microsoft-agents-authentication-msal>=0.7.0", "microsoft-agents-activity>=0.7.0", - # Agent 365 packages (using local SDK from Agent365-python repo) + # Agent 365 packages (using stable versions from PyPI) "microsoft-agents-a365-observability-core>=0.1.0", "microsoft-agents-a365-observability-hosting>=0.1.0", "microsoft-agents-a365-notifications>=0.1.0", From 5ab6341c61cddfb184d3682c7f24bfff1e740b19 Mon Sep 17 00:00:00 2001 From: Grant Harris Date: Thu, 16 Apr 2026 19:41:03 -0700 Subject: [PATCH 7/7] remove unused refs --- python/claude/sample-agent/host_agent_server.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/claude/sample-agent/host_agent_server.py b/python/claude/sample-agent/host_agent_server.py index c08a4d00..38fcfc01 100644 --- a/python/claude/sample-agent/host_agent_server.py +++ b/python/claude/sample-agent/host_agent_server.py @@ -171,7 +171,6 @@ async def on_message(context: TurnContext, _: TurnState): result = await self._validate_agent_and_setup_context(context) if result is None: return - tenant_id, agent_id = result user_message = context.activity.text or "" logger.info(f"📨 Processing message: '{user_message}'")