# The MCP Python SDK 2.0 broke the wrapper libraries. Here's how to spot it and pin back

> Source: <https://dev.to/milkyway008/the-mcp-python-sdk-20-broke-the-wrapper-libraries-heres-how-to-spot-it-and-pin-back-346n>
> Published: 2026-09-13 17:22:32+00:00

If your MCP tools started failing a few weeks ago and nothing in your own code changed, you're not imagining it.

The official Python `mcp` package shipped 2.0.0 on 2026-07-28. It's a real major version, with public APIs removed and renamed. Latest on PyPI right now is 2.2.0, and a plain `pip install mcp` resolves to 2.x. Any library that depends on `mcp` without an upper bound quietly floated forward and broke at install time.

I spent a while reproducing this in throwaway venvs with 1.29.1 and 2.2.0 side by side. Here's what actually changed, what the errors look like, and how to get moving again.

Two SDKs share the name. Python `mcp` is the one on PyPI. The Rust one is `rmcp` on crates.io, which is what Goose and some other Rust agents use, and it's on 3.x now. If you're debugging a Rust agent, the Python 2.x story is not your story. I nearly chased the wrong version myself...... glad I checked the crate name first.

In 1.x:

``` python
from mcp.client.streamable_http import streamablehttp_client

async with streamablehttp_client(url, timeout=30, sse_read_timeout=300) as (read, write, get_session_id):
    ...
```

In 2.x that import raises ImportError. The replacement is:

``` python
from mcp.client.streamable_http import streamable_http_client

async with streamable_http_client(url) as (read, write):
    ...
```

Two changes there. The name has underscores now, and the context manager yields a 2-tuple. The 3rd element, `get_session_id`, is gone.

That 2nd part is the one that hurts, because it doesn't fail at import. The wrapper imports fine, then blows up at runtime, and you get the error a lot of people are probably googling right now:

```
ValueError: not enough values to unpack (expected 3, got 2)
```

I reproduced that against a live streamable-HTTP server on 2.2.0. In the installed package it's `mcp/client/streamable_http.py`, around line 753: `yield read_stream, write_stream`.

`StreamableHTTPTransport.__init__` in 2.x is just `(self, url)`. The `timeout`, `sse_read_timeout`, `headers` and `auth` keywords aren't accepted there anymore; set them on the httpx2 `AsyncClient` instead. Pass them and you get `TypeError: unexpected keyword argument 'timeout'`.

Honesty note in the other direction: `sse_client` still takes `timeout` and `sse_read_timeout`. So the "all the transport kwargs got dropped" version of this story is wrong. It's the streamable-HTTP path that changed.

This is the quiet one. The Pydantic protocol models renamed their attributes:

`tool.inputSchema` goes to `tool.input_schema`
`result.isError` goes to `result.is_error`
`structuredContent` goes to `structured_content`
`nextCursor` goes to `next_cursor`
The JSON on the wire is still camelCase (the aliases keep that), so servers and clients still agree on the protocol. It's your Python that breaks, with an AttributeError. Bonus trap: `model_dump()` without `by_alias=True` now hands you snake_case dicts without complaint, which can poison anything downstream that expects the protocol shape.

Two examples I verified by downloading the packages rather than trusting a changelog. autogen-ext 0.7.5 declares `mcp>=1.11.0` with no upper bound, imports `streamablehttp_client`, unpacks the 3-tuple, and reads `inputSchema` / `isError`, so its `[mcp]` extra is broken against current `mcp`, and as of mid-September that's still the case on main. llama-index-tools-mcp 0.5.0 is the same failure with a twist: it declared `mcp>=2.0.0` but still unpacked the 3-tuple, so it was broken against its own dependency floor. Fixed in 0.5.1 in late August; 0.6.0 is current.

If you don't need 2.x features, pin back. The official 2.0.0 release notes tell library authors to keep a `<2` upper bound, and the 1.x line is still getting security fixes:

```
pip install "mcp>=1.28,<2"     # resolves to 1.30.0 today
uv add "mcp<2"
```

If the wrapper already fixed it (llama-index-tools-mcp >= 0.5.1, for example), upgrade the wrapper instead of downgrading `mcp`. Read the release notes before you assume which side is fixed.

Before guessing, check what you're actually running:

``` python
python -c "import importlib.metadata as m, inspect; print(m.version('mcp')); import mcp.client.streamable_http as sh; print(hasattr(sh,'streamablehttp_client')); print(inspect.signature(sh.streamable_http_client))"
```

And check the wrapper's `pyproject.toml` for an unbounded `mcp` dependency. That missing upper bound is the whole bug, and it'll happen again at the next major.

I saw a couple of claims circulating while digging that can cost you hours if you chase them.

An empty list from a tool producing zero content blocks is not a 2.x regression. I measured the same numbers on both 1.29.1 and 2.2.0: `[]` gives 0 blocks, `''` gives 1, `json.dumps([])` gives 1, `['a','b']` gives 2. It's long-standing FastMCP behavior in `_convert_to_content`, which flattens a list item by item, and an empty list flattens to nothing. `structured_content` is still correct. If your tool returns a list, return something wrapper-friendly instead of a bare `[]` so the model gets one text block to read. That one matters if an agent keeps re-running your search instead of concluding "no match".

Separately, 2.x removed automatic return-value wrapping in the low-level `Server`, and `mcp.types` moved out into a `mcp-types` package. Both are in the migration guide.

A dependency major plus an unbounded requirement in the wrapper equals silent breakage at install time, with a stack trace pointing at your code instead of the dependency. Pin it, then skim the migration guide at py.sdk.modelcontextprotocol.io/migration. It's a long read, but the naming section alone saves the afternoon.

I run a pile of MCP servers locally for agent work, and pinning is the boring habit that keeps it quiet. If you want to see how I aggregate and hot-swap those servers, that's in my Smart-MCP-Proxy repo, but the pin is the only part you need today.

And I could be wrong about your specific stack, so test the pin in a throwaway venv first. That's what I did before writing any of this down.
