1212from contextlib import AsyncExitStack
1313from typing import Any , Dict , List , Optional , Tuple
1414
15+ import httpx
1516from mcp .client .session import ClientSession
1617from mcp .client .streamable_http import streamablehttp_client
1718
@@ -26,6 +27,9 @@ class MCPConnectionManager:
2627 def __init__ (self ):
2728 self ._tools_cache : Dict [str , List [Dict ]] = {}
2829 self ._tools_cache_lock = asyncio .Lock ()
30+ # Shared HTTP client for control plane requests with high connection limits
31+ self ._shared_client : Optional [httpx .AsyncClient ] = None
32+ self ._client_lock = asyncio .Lock ()
2933
3034 async def initialize_session (self , session : MCPSession ) -> None :
3135 """
@@ -114,6 +118,12 @@ async def _prewarm_tools_cache(self, session: MCPSession) -> None:
114118 """
115119 cache_key = session .base_url
116120
121+ # Fast path: if cache already exists, return immediately (no lock)
122+ if cache_key in self ._tools_cache :
123+ logger .debug (f"Tools cache already exists for { cache_key } " )
124+ return
125+
126+ # Slow path: need to create cache (use lock only for creation)
117127 async with self ._tools_cache_lock :
118128 # Only fetch tools if not already cached for this base_url
119129 if cache_key not in self ._tools_cache :
@@ -244,21 +254,33 @@ async def get_initial_state(self, session: MCPSession) -> Any:
244254 # Use shorter timeout for playback mode, longer timeout for high-concurrency initialization
245255 # (50+ concurrent sessions need more time for initial state setup)
246256 timeout = 3.0 if hasattr (session , "_is_playback_mode" ) and session ._is_playback_mode else 15.0
247- async with httpx .AsyncClient (timeout = timeout ) as client :
248- initial_state_response = await client .get (
249- f"{ base_url } /control/initial_state" ,
250- headers = headers ,
251- timeout = timeout ,
257+
258+ # TIMING: Get shared client
259+ client_start = __import__ ("time" ).time ()
260+ client = await self ._get_shared_client (timeout )
261+ client_time = __import__ ("time" ).time () - client_start
262+ logger .info (
263+ f"DEBUG_CLIENT: Getting shared client took { client_time :.3f} s for { session .session_id } "
264+ )
265+
266+ # TIMING: HTTP request with shared client
267+ request_start = __import__ ("time" ).time ()
268+ initial_state_response = await client .get (
269+ f"{ base_url } /control/initial_state" ,
270+ headers = headers ,
271+ timeout = timeout ,
272+ )
273+ request_time = __import__ ("time" ).time () - request_start
274+ logger .info (f"DEBUG_REQUEST: HTTP request took { request_time :.3f} s for { session .session_id } " )
275+ if initial_state_response .status_code == 200 :
276+ initial_observation = initial_state_response .json ()
277+ logger .info (
278+ f"Session { session .session_id } : ✅ Successfully fetched session-aware initial state from control plane endpoint"
279+ )
280+ else :
281+ logger .warning (
282+ f"Control plane initial state endpoint returned { initial_state_response .status_code } "
252283 )
253- if initial_state_response .status_code == 200 :
254- initial_observation = initial_state_response .json ()
255- logger .info (
256- f"Session { session .session_id } : ✅ Successfully fetched session-aware initial state from control plane endpoint"
257- )
258- else :
259- logger .warning (
260- f"Control plane initial state endpoint returned { initial_state_response .status_code } "
261- )
262284 except httpx .TimeoutException :
263285 logger .warning (f"Control plane initial state endpoint timed out after { timeout } s" )
264286 except Exception as e :
@@ -579,3 +601,47 @@ async def close_session(self, session: MCPSession) -> None:
579601 finally :
580602 session ._exit_stack = None
581603 session ._mcp_session = None
604+
605+ async def _get_shared_client (self , timeout : float ) -> httpx .AsyncClient :
606+ """
607+ Get or create a shared HTTP client with high connection limits for concurrent requests.
608+
609+ Args:
610+ timeout: Timeout for requests
611+
612+ Returns:
613+ Shared httpx.AsyncClient instance
614+ """
615+ # Fast path: if client exists and is not closed, return it immediately
616+ if self ._shared_client is not None and not self ._shared_client .is_closed :
617+ return self ._shared_client
618+
619+ # Slow path: need to create client (use lock only for creation)
620+ async with self ._client_lock :
621+ # Double-check pattern: another task might have created it while we waited
622+ if self ._shared_client is None or self ._shared_client .is_closed :
623+ # Create HTTP client with high connection limits for concurrent initial state requests
624+ limits = httpx .Limits (
625+ max_keepalive_connections = None , # Unlimited keep-alive connections
626+ max_connections = None , # Unlimited total connection pool size
627+ keepalive_expiry = 30.0 , # Keep connections alive for 30s
628+ )
629+
630+ self ._shared_client = httpx .AsyncClient (
631+ timeout = timeout ,
632+ limits = limits ,
633+ # Enable connection pooling and keep-alive
634+ http2 = False , # Disable HTTP/2 for better connection pooling with many concurrent requests
635+ )
636+ logger .info (
637+ "Created shared HTTP client with unlimited connection limits for MCP control plane requests"
638+ )
639+
640+ return self ._shared_client
641+
642+ async def close_shared_client (self ):
643+ """Close the shared HTTP client when shutting down."""
644+ async with self ._client_lock :
645+ if self ._shared_client and not self ._shared_client .is_closed :
646+ await self ._shared_client .aclose ()
647+ self ._shared_client = None
0 commit comments