Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/google/adk/tools/mcp_tool/mcp_session_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,25 @@ async def create_session(
logger.debug('Created new session: %s', session_key)
return session

except asyncio.CancelledError as e:
# CancelledError can occur when the MCP server returns an HTTP error
# (e.g., 401, 403). The MCP SDK uses anyio cancel scopes internally,
# which raise CancelledError. Since CancelledError is a BaseException
# (not Exception) in Python 3.8+, it must be caught explicitly.
logger.warning(
'MCP session creation cancelled (likely due to HTTP error): %s', e
)
try:
await exit_stack.aclose()
except Exception as exit_stack_error:
logger.warning(
'Error during cancelled session cleanup: %s', exit_stack_error
)
raise ConnectionError(
f'MCP session creation cancelled (server may have returned HTTP'
f' error): {e}'
) from e

except Exception as e:
# If session creation fails, clean up the exit stack
if exit_stack:
Expand Down
36 changes: 36 additions & 0 deletions tests/unittests/tools/mcp_tool/test_mcp_session_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,42 @@ async def test_create_session_timeout(
# Verify cleanup was called
mock_exit_stack.aclose.assert_called_once()

@pytest.mark.asyncio
@patch("google.adk.tools.mcp_tool.mcp_session_manager.stdio_client")
@patch("google.adk.tools.mcp_tool.mcp_session_manager.AsyncExitStack")
async def test_create_session_cancelled_error(
self, mock_exit_stack_class, mock_stdio
):
"""Test session creation when CancelledError is raised (e.g., HTTP 403).

When an MCP server returns an HTTP error (e.g., 401, 403), the MCP SDK
uses anyio cancel scopes internally, which raise CancelledError. This
test verifies that CancelledError is caught and converted to a
ConnectionError with proper cleanup.
"""
manager = MCPSessionManager(self.mock_stdio_connection_params)

mock_exit_stack = MockAsyncExitStack()

mock_exit_stack_class.return_value = mock_exit_stack
mock_stdio.return_value = AsyncMock()

# Simulate CancelledError during session creation (e.g., HTTP 403)
mock_exit_stack.enter_async_context.side_effect = asyncio.CancelledError(
"Cancelled by cancel scope"
)

# Expect ConnectionError due to CancelledError
with pytest.raises(
ConnectionError, match="MCP session creation cancelled"
):
await manager.create_session()

# Verify session was not added to pool
assert not manager._sessions
# Verify cleanup was called
mock_exit_stack.aclose.assert_called_once()

@pytest.mark.asyncio
async def test_close_success(self):
"""Test successful cleanup of all sessions."""
Expand Down