yihuang2026 commited on
Commit
49656bc
·
unverified ·
1 Parent(s): d32f68a

fix client nested context manager

Browse files
Files changed (1) hide show
  1. src/fastmcp/client/client.py +17 -25
src/fastmcp/client/client.py CHANGED
@@ -44,8 +44,8 @@ class Client:
44
  read_timeout_seconds: datetime.timedelta | None = None,
45
  ):
46
  self.transport = infer_transport(transport)
47
- self._session: ClientSession | None = None
48
- self._session_cms: list[AbstractAsyncContextManager[ClientSession]] = []
49
 
50
  self._session_kwargs: SessionKwargs = {
51
  "sampling_callback": None,
@@ -64,11 +64,11 @@ class Client:
64
  @property
65
  def session(self) -> ClientSession:
66
  """Get the current active session. Raises RuntimeError if not connected."""
67
- if self._session is None:
68
  raise RuntimeError(
69
  "Client is not connected. Use 'async with client:' context manager first."
70
  )
71
- return self._session
72
 
73
  def set_roots(self, roots: RootsList | RootsHandler) -> None:
74
  """Set the roots for the client. This does not automatically call `send_roots_list_changed`."""
@@ -82,32 +82,24 @@ class Client:
82
 
83
  def is_connected(self) -> bool:
84
  """Check if the client is currently connected."""
85
- return self._session is not None
86
 
87
  async def __aenter__(self):
88
- if self.is_connected():
89
- # We're already connected, no need to add None to the session_cms list
90
- return self
91
-
92
- try:
 
93
  session_cm = self.transport.connect_session(**self._session_kwargs)
94
- self._session_cms.append(session_cm)
95
- self._session = await self._session_cms[-1].__aenter__()
96
- return self
97
- except Exception as e:
98
- # Ensure cleanup if __aenter__ fails partially
99
- self._session = None
100
- if self._session_cms:
101
- self._session_cms.pop()
102
- raise ConnectionError(
103
- f"Failed to connect using {self.transport}: {e}"
104
- ) from e
105
 
106
  async def __aexit__(self, exc_type, exc_val, exc_tb):
107
- if self._session_cms:
108
- await self._session_cms[-1].__aexit__(exc_type, exc_val, exc_tb)
109
- self._session = None
110
- self._session_cms.pop()
111
 
112
  # --- MCP Client Methods ---
113
  async def ping(self) -> None:
 
44
  read_timeout_seconds: datetime.timedelta | None = None,
45
  ):
46
  self.transport = infer_transport(transport)
47
+ # stack to record nested context manager, None is pushed if reuse existing session
48
+ self._session_cms: list[(AbstractAsyncContextManager[ClientSession], ClientSession)] = []
49
 
50
  self._session_kwargs: SessionKwargs = {
51
  "sampling_callback": None,
 
64
  @property
65
  def session(self) -> ClientSession:
66
  """Get the current active session. Raises RuntimeError if not connected."""
67
+ if not self._session_cms:
68
  raise RuntimeError(
69
  "Client is not connected. Use 'async with client:' context manager first."
70
  )
71
+ self._session_cms[-1][1]
72
 
73
  def set_roots(self, roots: RootsList | RootsHandler) -> None:
74
  """Set the roots for the client. This does not automatically call `send_roots_list_changed`."""
 
82
 
83
  def is_connected(self) -> bool:
84
  """Check if the client is currently connected."""
85
+ return len(self._session_cms) > 0
86
 
87
  async def __aenter__(self):
88
+ if self._session_cms:
89
+ # share the current session, push a None as cms to avoid close it in aexit
90
+ _, session = self._session_cms[-1]
91
+ self._session_cms.append((None, session))
92
+ else:
93
+ # create new session
94
  session_cm = self.transport.connect_session(**self._session_kwargs)
95
+ session = await session_cm.__aenter__()
96
+ self._session_cms.append((session_cm, session))
97
+ return self
 
 
 
 
 
 
 
 
98
 
99
  async def __aexit__(self, exc_type, exc_val, exc_tb):
100
+ cm, _ = self._session_cms.pop()
101
+ if cm is not None:
102
+ await cm.__aexit__(exc_type, exc_val, exc_tb)
 
103
 
104
  # --- MCP Client Methods ---
105
  async def ping(self) -> None: