fix: collapse single-exception ExceptionGroups from task groups#2245
Open
Varun6578 wants to merge 5 commits intomodelcontextprotocol:mainfrom
Open
fix: collapse single-exception ExceptionGroups from task groups#2245Varun6578 wants to merge 5 commits intomodelcontextprotocol:mainfrom
Varun6578 wants to merge 5 commits intomodelcontextprotocol:mainfrom
Conversation
Replace all 16 anyio.create_task_group() calls with create_mcp_task_group() which automatically unwraps BaseExceptionGroups containing a single exception. This allows callers to catch specific error types (e.g. except ConnectionError) instead of having to handle ExceptionGroup wrapping. - Add src/mcp/shared/_task_group.py with collapse_exception_group() utility and _CollapsingTaskGroup wrapper class - Update all client transports (sse, stdio, websocket, streamable_http, memory) - Update all server transports (sse, stdio, websocket, streamable_http) - Update shared session, session_group, lowlevel server, task_support, task_result_handler, and streamable_http_manager - Add builtins config for BaseExceptionGroup/ExceptionGroup in ruff - Add 12 comprehensive tests covering collapse logic and integration Closes modelcontextprotocol#2114 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
On Python < 3.11, BaseExceptionGroup and ExceptionGroup are not builtins. Import them from the exceptiongroup backport package conditionally. Also fix coverage miss on unreachable pytest.fail line and revert unnecessary builtins ruff config. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add pragma no cover to version-conditional imports (Python 3.10 compat) - Add type: ignore for pyright false positives (isinstance in loop, duck-type assignment) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ression tests - Replace 'pragma: no cover' with 'pragma: lax no cover' (matches repo convention for lines that may/may not be covered per environment) - Add test_start_failure_is_unwrapped for start() error path - Add test_issue_2114_except_specific_error_type regression test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Reported-by: maxisbey <modelcontextprotocol#2114>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #2114 — ExceptionGroup wrapping obscures real errors from task groups.
When an anyio task group contains tasks and one fails, the exception is always wrapped in an
ExceptionGroup— even if there is only a single real exception. This makes it impossible for callers to catch specific error types withexcept ConnectionError:.Changes
New module
src/mcp/shared/_task_group.py:collapse_exception_group()— recursively unwraps single-exceptionBaseExceptionGroups_CollapsingTaskGroup— drop-in wrapper around anyio'sTaskGroupthat collapses on__aexit__create_mcp_task_group()— factory function replacinganyio.create_task_group()All 16
anyio.create_task_group()call sites updated across:sse.py,stdio.py,websocket.py,streamable_http.py,_memory.pysse.py,stdio.py,websocket.py,streamable_http.pysession.py,session_group.pylowlevel/server.py,streamable_http_manager.py,task_support.py,task_result_handler.py12 new tests covering collapse logic, integration with task groups, edge cases
Ruff config: added
builtinsforBaseExceptionGroup/ExceptionGroup(Python 3.10 compat)Behavior
ExceptionGroup([ConnectionError])ConnectionErrorraised directlyExceptionGroup([ConnectionError])ConnectionErrorraised directlyExceptionGroup([A, B])ExceptionGroup([A, B])(unchanged)The collapsed exception preserves the
ExceptionGroupas__cause__for debugging.