3030from agent_framework ._mcp import MCPStreamableHTTPTool
3131from agent_framework ._tools import FunctionTool
3232from agenticlayer .shared .config import McpTool , SubAgent
33- from agenticlayer .shared .otel import TraceContextHttpClient
3433from httpx_retries import Retry
3534from starlette .applications import Starlette
3635
@@ -55,10 +54,14 @@ class MsafAgentExecutor(AgentExecutor):
5554 def __init__ (
5655 self ,
5756 agent : SupportsAgentRun ,
58- extra_tools : list [FunctionTool | MCPStreamableHTTPTool ] | None = None ,
57+ sub_agent_tools : list [FunctionTool ] | None = None ,
58+ mcp_tool_configs : list [McpTool ] | None = None ,
59+ agent_factory : MsafAgentFactory | None = None ,
5960 ) -> None :
6061 self ._agent = agent
61- self ._extra_tools : list [FunctionTool | MCPStreamableHTTPTool ] = extra_tools or []
62+ self ._sub_agent_tools : list [FunctionTool ] = sub_agent_tools or []
63+ self ._mcp_tool_configs : list [McpTool ] = mcp_tool_configs or []
64+ self ._agent_factory = agent_factory
6265
6366 async def execute (self , context : RequestContext , event_queue : EventQueue ) -> None :
6467 """Execute the agent and publish results to the event queue."""
@@ -93,16 +96,15 @@ async def execute(self, context: RequestContext, event_queue: EventQueue) -> Non
9396 )
9497
9598 try :
96- # Capture current OTel trace context so that MCP HTTP requests
97- # (which run in a background post_writer task without span context)
98- # carry the correct traceparent/tracestate headers.
99- for tool in self ._extra_tools :
100- if isinstance (tool , MCPStreamableHTTPTool ):
101- client = getattr (tool , "_httpx_client" , None )
102- if isinstance (client , TraceContextHttpClient ):
103- client .capture_trace_context ()
104-
105- response = await self ._agent .run (user_input , tools = self ._extra_tools if self ._extra_tools else None )
99+ async with contextlib .AsyncExitStack () as stack :
100+ mcp_tools : list [MCPStreamableHTTPTool ] = []
101+ if self ._mcp_tool_configs and self ._agent_factory :
102+ for mcp_tool in self ._agent_factory .create_mcp_tools (self ._mcp_tool_configs ):
103+ await stack .enter_async_context (mcp_tool )
104+ mcp_tools .append (mcp_tool )
105+
106+ all_tools : list [FunctionTool | MCPStreamableHTTPTool ] = [* self ._sub_agent_tools , * mcp_tools ]
107+ response = await self ._agent .run (user_input , tools = all_tools if all_tools else None )
106108 response_text = response .text if hasattr (response , "text" ) else str (response )
107109
108110 await event_queue .enqueue_event (
@@ -162,7 +164,8 @@ async def create_a2a_app(
162164 description : str | None ,
163165 rpc_url : str ,
164166 sub_agent_tools : list [FunctionTool ],
165- mcp_tools : list [MCPStreamableHTTPTool ],
167+ mcp_tool_configs : list [McpTool ] | None = None ,
168+ agent_factory : MsafAgentFactory | None = None ,
166169) -> A2AStarletteApplication :
167170 """Create an A2A Starlette application from a Microsoft Agent Framework agent.
168171
@@ -172,14 +175,19 @@ async def create_a2a_app(
172175 description: Optional description of the agent
173176 rpc_url: The URL where the agent will be available for A2A communication
174177 sub_agent_tools: Pre-loaded FunctionTools wrapping remote A2A sub-agents
175- mcp_tools: Connected MCPStreamableHTTPTool instances
178+ mcp_tool_configs: MCP tool configurations; per-request connections are created at execution time
179+ agent_factory: Factory used to create MCP tools per request
176180
177181 Returns:
178182 An A2AStarletteApplication instance
179183 """
180184 task_store = InMemoryTaskStore ()
181- extra_tools : list [FunctionTool | MCPStreamableHTTPTool ] = [* sub_agent_tools , * mcp_tools ]
182- agent_executor = MsafAgentExecutor (agent = agent , extra_tools = extra_tools if extra_tools else None )
185+ agent_executor = MsafAgentExecutor (
186+ agent = agent ,
187+ sub_agent_tools = sub_agent_tools if sub_agent_tools else None ,
188+ mcp_tool_configs = mcp_tool_configs ,
189+ agent_factory = agent_factory ,
190+ )
183191 request_handler = DefaultRequestHandler (agent_executor = agent_executor , task_store = task_store )
184192
185193 agent_card = AgentCard (
@@ -247,44 +255,33 @@ async def _build_app(
247255 sub_agents : list [SubAgent ],
248256 tools : list [McpTool ],
249257 factory : MsafAgentFactory ,
250- ) -> tuple [ A2AStarletteApplication , list [ MCPStreamableHTTPTool ]] :
251- """Load sub-agents, connect MCP tools, and return the A2A app plus tools to manage .
258+ ) -> A2AStarletteApplication :
259+ """Load sub-agents and return the A2A app.
252260
253- The returned MCP tools have already been entered as async context managers;
254- the caller (lifespan) is responsible for exiting them on shutdown .
261+ MCP tools are created per-request inside the executor; no connections are
262+ established here .
255263 """
256264 sub_agent_tools = await factory .load_sub_agents (sub_agents )
257- mcp_tools = factory .create_mcp_tools (tools )
258-
259- connected_mcp : list [MCPStreamableHTTPTool ] = []
260- try :
261- for mcp_tool in mcp_tools :
262- await mcp_tool .__aenter__ ()
263- connected_mcp .append (mcp_tool )
264- except Exception :
265- for already_connected in reversed (connected_mcp ):
266- await already_connected .__aexit__ (None , None , None )
267- raise
268-
269- app = await create_a2a_app (
265+
266+ return await create_a2a_app (
270267 agent = agent ,
271268 name = name ,
272269 description = description ,
273270 rpc_url = rpc_url ,
274271 sub_agent_tools = sub_agent_tools ,
275- mcp_tools = connected_mcp ,
272+ mcp_tool_configs = tools if tools else None ,
273+ agent_factory = factory ,
276274 )
277- return app , connected_mcp
278275
279276
280277def to_starlette (
281- a2a_app_creator : Callable [[], Awaitable [tuple [ A2AStarletteApplication , list [ MCPStreamableHTTPTool ]] ]],
278+ a2a_app_creator : Callable [[], Awaitable [A2AStarletteApplication ]],
282279) -> Starlette :
283280 """Convert an A2A application creator to a Starlette application.
284281
285282 Args:
286- a2a_app_creator: A callable that creates an A2AStarletteApplication and
287- connected MCP tools asynchronously during startup.
283+ a2a_app_creator: A callable that creates an A2AStarletteApplication
284+ asynchronously during startup.
288285
289286 Returns:
290287 A Starlette application that can be run with uvicorn
@@ -295,14 +292,10 @@ def to_starlette(
295292
296293 @contextlib .asynccontextmanager
297294 async def lifespan (app : Starlette ) -> AsyncIterator [None ]:
298- a2a_app , connected_mcp = await a2a_app_creator ()
295+ a2a_app = await a2a_app_creator ()
299296 # Add A2A routes to the main app
300297 a2a_app .add_routes_to_app (app )
301- try :
302- yield
303- finally :
304- for mcp_tool in connected_mcp :
305- await mcp_tool .__aexit__ (None , None , None )
298+ yield
306299
307300 # Create a Starlette app that will be configured during startup
308301 starlette_app = Starlette (lifespan = lifespan )
0 commit comments