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:
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:
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 down 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 -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.