Run the complete session context within a single task.
(self)
| 246 | await self.close() |
| 247 | |
| 248 | async def _run(self): |
| 249 | """Run the complete session context within a single task.""" |
| 250 | try: |
| 251 | async with AsyncExitStack() as exit_stack: |
| 252 | if is_feature_enabled(FeatureName._MCP_GRACEFUL_ERROR_HANDLING): # pylint: disable=protected-access |
| 253 | # Post-fix: do NOT wrap in asyncio.wait_for. The MCP client uses |
| 254 | # AnyIO TaskGroup/CancelScope internally, which must be entered |
| 255 | # and exited in the same task. asyncio.wait_for runs its target |
| 256 | # in a nested task and can cancel from a different task on |
| 257 | # timeout, producing "Attempted to exit cancel scope in a |
| 258 | # different task" errors. The connection-establishment timeout |
| 259 | # is still enforced by MCPSessionManager.create_session via its |
| 260 | # outer asyncio.wait_for around |
| 261 | # exit_stack.enter_async_context(SessionContext(...)). |
| 262 | transports = await exit_stack.enter_async_context(self._client) |
| 263 | else: |
| 264 | # Pre-fix behavior: wrap with asyncio.wait_for so the inner |
| 265 | # context entry has its own timeout. Callers that depend on |
| 266 | # this inner timeout firing rely on this path; without it, |
| 267 | # mocks that delay `__aenter__` cause tests to time out at the |
| 268 | # test framework limit instead of the configured per-step timeout. |
| 269 | transports = await asyncio.wait_for( |
| 270 | exit_stack.enter_async_context(self._client), |
| 271 | timeout=self._timeout, |
| 272 | ) |
| 273 | # The streamable http client returns a GetSessionCallback in addition |
| 274 | # to the read/write MemoryObjectStreams needed to build the |
| 275 | # ClientSession. We limit to the first two values to be compatible |
| 276 | # with all clients. |
| 277 | if self._is_stdio: |
| 278 | session = await exit_stack.enter_async_context( |
| 279 | ClientSession( |
| 280 | *transports[:2], |
| 281 | read_timeout_seconds=timedelta(seconds=self._timeout) |
| 282 | if self._timeout is not None |
| 283 | else None, |
| 284 | sampling_callback=self._sampling_callback, |
| 285 | sampling_capabilities=self._sampling_capabilities, |
| 286 | ) |
| 287 | ) |
| 288 | else: |
| 289 | # For SSE and Streamable HTTP clients, use the sse_read_timeout |
| 290 | # instead of the connection timeout as the read_timeout for the session. |
| 291 | session = await exit_stack.enter_async_context( |
| 292 | ClientSession( |
| 293 | *transports[:2], |
| 294 | read_timeout_seconds=timedelta(seconds=self._sse_read_timeout) |
| 295 | if self._sse_read_timeout is not None |
| 296 | else None, |
| 297 | sampling_callback=self._sampling_callback, |
| 298 | sampling_capabilities=self._sampling_capabilities, |
| 299 | ) |
| 300 | ) |
| 301 | # pylint: disable-next=protected-access |
| 302 | if is_feature_enabled(FeatureName._MCP_GRACEFUL_ERROR_HANDLING): |
| 303 | # Use anyio.fail_after to keep session.initialize within the AnyIO |
| 304 | # cancel scope instead of asyncio.wait_for which runs in a nested |
| 305 | # task. |
no test coverage detected