1+ import os
2+ from contextlib import AsyncExitStack
3+ from dataclasses import dataclass
4+ from typing import Any
5+
6+ from azure .ai .projects .aio import AIProjectClient
7+ from azure .identity import InteractiveBrowserCredential
8+ from azure .identity .aio import DefaultAzureCredential
9+ from semantic_kernel .agents .azure_ai .azure_ai_agent import AzureAIAgent
10+ from semantic_kernel .connectors .mcp import MCPStreamableHttpPlugin
11+ from v3 .magentic_agents .models .agent_models import MCPConfig
12+
13+
14+ class MCPEnabledBase :
15+ """
16+ Base that owns an AsyncExitStack and, if configured, enters the MCP plugin
17+ as an async context. Subclasses build the actual agent in _after_open().
18+ """
19+
20+ def __init__ (self , mcp : MCPConfig | None = None ) -> None :
21+ self ._stack : AsyncExitStack | None = None
22+ self .mcp_cfg : MCPConfig = mcp or MCPConfig .from_env ()
23+ self .mcp_plugin : MCPStreamableHttpPlugin | None = None
24+ self ._agent : Any | None = None # delegate target
25+
26+ async def open (self ) -> "MCPEnabledBase" :
27+ if self ._stack is not None :
28+ return self
29+ self ._stack = AsyncExitStack ()
30+ await self ._enter_mcp_if_configured ()
31+ await self ._after_open ()
32+ return self
33+
34+ async def close (self ) -> None :
35+ if self ._stack is None :
36+ return
37+ try :
38+ self .cred .close ()
39+ await self ._stack .aclose ()
40+ finally :
41+ self ._stack = None
42+ self .mcp_plugin = None
43+ self ._agent = None
44+
45+ # Context manager
46+ async def __aenter__ (self ) -> "MCPEnabledBase" :
47+ return await self .open ()
48+
49+ async def __aexit__ (self , exc_type , exc , tb ) -> None :
50+ await self .close ()
51+
52+ # Delegate attributes to the built agent
53+ def __getattr__ (self , name : str ) -> Any :
54+ if self ._agent is not None :
55+ return getattr (self ._agent , name )
56+ raise AttributeError (f"{ type (self ).__name__ } has no attribute '{ name } '" )
57+
58+ # Hooks
59+ async def _after_open (self ) -> None :
60+ """Subclasses must build self._agent here."""
61+ raise NotImplementedError
62+
63+ # Internals
64+ def _build_mcp_headers (self ) -> dict :
65+ if not self .mcp_cfg .client_id :
66+ return {}
67+ self .cred = InteractiveBrowserCredential (
68+ tenant_id = self .mcp_cfg .tenant_id or None ,
69+ client_id = self .mcp_cfg .client_id ,
70+ )
71+ tok = self .cred .get_token (f"api://{ self .mcp_cfg .client_id } /access_as_user" )
72+ return {
73+ "Authorization" : f"Bearer { tok .token } " ,
74+ "Content-Type" : "application/json" ,
75+ }
76+
77+ async def _enter_mcp_if_configured (self ) -> None :
78+ if not self .mcp_cfg .url :
79+ return
80+ # Note: had this commented out in my testing because I don't have
81+ # access to your resources
82+ headers = self ._build_mcp_headers ()
83+ plugin = MCPStreamableHttpPlugin (
84+ name = self .mcp_cfg .name ,
85+ description = self .mcp_cfg .description ,
86+ url = self .mcp_cfg .url ,
87+ headers = headers ,
88+ )
89+ # Enter MCP async context via the stack to ensure correct LIFO cleanup
90+ if self ._stack is None :
91+ self ._stack = AsyncExitStack ()
92+ self .mcp_plugin = await self ._stack .enter_async_context (plugin )
93+
94+
95+ class AzureAgentBase (MCPEnabledBase ):
96+ """
97+ Extends MCPEnabledBase with Azure async contexts that many agents need:
98+ - DefaultAzureCredential (async)
99+ - AzureAIAgent.create_client(...) (async)
100+ Subclasses then create an AzureAIAgent definition and bind plugins.
101+ """
102+
103+ def __init__ (self , mcp : MCPConfig | None = None ) -> None :
104+ super ().__init__ (mcp = mcp )
105+ self .creds : DefaultAzureCredential | None = None
106+ self .client : AIProjectClient | None = None
107+
108+ async def open (self ) -> "AzureAgentBase" :
109+ if self ._stack is not None :
110+ return self
111+ self ._stack = AsyncExitStack ()
112+ # Azure async contexts
113+ self .creds = DefaultAzureCredential ()
114+ await self ._stack .enter_async_context (self .creds )
115+ self .client = AzureAIAgent .create_client (credential = self .creds )
116+ await self ._stack .enter_async_context (self .client )
117+
118+ # MCP async context if requested
119+ await self ._enter_mcp_if_configured ()
120+
121+ # Build the agent
122+ await self ._after_open ()
123+ return self
124+
125+ async def close (self ) -> None :
126+ await self .creds .close ()
127+ await super ().close ()
0 commit comments