Skip to content

fix: collapse single-exception ExceptionGroups from task groups - #2245

Open
Varun6578 wants to merge 5 commits into
modelcontextprotocol:mainfrom
Varun6578:fix/collapse-exception-group
Open

fix: collapse single-exception ExceptionGroups from task groups#2245
Varun6578 wants to merge 5 commits into
modelcontextprotocol:mainfrom
Varun6578:fix/collapse-exception-group

Conversation

@Varun6578

Copy link
Copy Markdown
Contributor

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 with except ConnectionError:.

Changes

  • New modulesrc/mcp/shared/_task_group.py:

    • collapse_exception_group() — recursively unwraps single-exception BaseExceptionGroups
    • _CollapsingTaskGroup — drop-in wrapper around anyio's TaskGroup that collapses on __aexit__
    • create_mcp_task_group() — factory function replacing anyio.create_task_group()
  • All 16 anyio.create_task_group() call sites updated across:

    • Client transports: sse.py, stdio.py, websocket.py, streamable_http.py, _memory.py
    • Server transports: sse.py, stdio.py, websocket.py, streamable_http.py
    • Shared: session.py, session_group.py
    • Server internals: lowlevel/server.py, streamable_http_manager.py, task_support.py, task_result_handler.py
  • 12 new tests covering collapse logic, integration with task groups, edge cases

  • Ruff config: added builtins for BaseExceptionGroup/ExceptionGroup (Python 3.10 compat)

Behavior

ScenarioBeforeAfter
Single task failsExceptionGroup([ConnectionError])ConnectionError raised directly
Single task fails + siblings cancelledExceptionGroup([ConnectionError])ConnectionError raised directly
Multiple tasks failExceptionGroup([A, B])ExceptionGroup([A, B]) (unchanged)
No failuresClean exitClean exit (unchanged)

The collapsed exception preserves the ExceptionGroup as __cause__ for debugging.

Varun Sharmaand others added 5 commits March 7, 2026 15:46
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
Closesmodelcontextprotocol#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>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ExceptionGroup wrapping obscures real errors from task groups

1 participant

@Varun6578