From a33889a4feb28833fd673bb7cfbbe944c77b3525 Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Fri, 15 May 2026 14:17:17 +0800 Subject: [PATCH] fix: preserve MCP connection errors during cleanup --- python/packages/core/agent_framework/_mcp.py | 5 +++ .../tests/core/test_mcp_exception_group.py | 38 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 python/packages/core/tests/core/test_mcp_exception_group.py diff --git a/python/packages/core/agent_framework/_mcp.py b/python/packages/core/agent_framework/_mcp.py index 35ccb1d58a..3aefebb54e 100644 --- a/python/packages/core/agent_framework/_mcp.py +++ b/python/packages/core/agent_framework/_mcp.py @@ -642,6 +642,11 @@ async def _safe_close_exit_stack(self) -> None: ) else: raise + except Exception as e: + if type(e).__name__ == "ExceptionGroup": + logger.warning("Could not cleanly close MCP exit stack: %s", e) + else: + raise except asyncio.CancelledError: logger.warning("Could not cleanly close MCP exit stack because the lifecycle owner task was cancelled.") diff --git a/python/packages/core/tests/core/test_mcp_exception_group.py b/python/packages/core/tests/core/test_mcp_exception_group.py new file mode 100644 index 0000000000..f5de0d3844 --- /dev/null +++ b/python/packages/core/tests/core/test_mcp_exception_group.py @@ -0,0 +1,38 @@ +# Copyright (c) Microsoft. All rights reserved. + +import sys +from unittest.mock import AsyncMock, Mock, patch + +import pytest + +from agent_framework import MCPStreamableHTTPTool +from agent_framework.exceptions import ToolException + + +async def test_connect_initialization_failure_keeps_original_error_when_cleanup_raises_exception_group(): + exception_group_type = getattr(sys.modules["builtins"], "ExceptionGroup", None) + if exception_group_type is None: + pytest.skip("ExceptionGroup is only available on Python 3.11+") + + tool = MCPStreamableHTTPTool(name="test", url="http://example.com") + + mock_transport = (Mock(), Mock()) + mock_context_manager = Mock() + mock_context_manager.__aenter__ = AsyncMock(return_value=mock_transport) + mock_context_manager.__aexit__ = AsyncMock( + side_effect=exception_group_type("unhandled errors in a TaskGroup", [ConnectionError("cleanup failed")]) + ) + tool.get_mcp_client = Mock(return_value=mock_context_manager) + + mock_session = Mock() + mock_session.initialize = AsyncMock(side_effect=ConnectionError("Server not ready")) + + with patch("mcp.client.session.ClientSession") as mock_session_class: + mock_session_class.return_value.__aenter__ = AsyncMock(return_value=mock_session) + mock_session_class.return_value.__aexit__ = AsyncMock(return_value=None) + + with pytest.raises(ToolException) as exc_info: + await tool.connect() + + assert "MCP server failed to initialize" in str(exc_info.value) + assert "Server not ready" in str(exc_info.value)