| |

MCP 2.0 Migration: The Breaking Changes Every SDET Will Hit

MCP 2.0 migration guide featured image for SDETs

The Model Context Protocol Python SDK shipped its 2.0.0 stable release on July 28, 2026, and pip install mcp now gives you the 2.x line by default. If your QA team built MCP test harnesses, smoke suites, or agent fixtures on v1, that upgrade just broke them. This MCP 2.0 migration guide walks you through every breaking change that matters to an SDET, with before-and-after code you can run today.

Table of Contents

Contents

Why MCP 2.0 Migration Matters for QA Teams

MCP has become the default way QA engineers connect AI agents to the tools they test. I see the same pattern across the teams I talk to: someone wrote an MCP server to expose a product API to an agent, someone else wrote a test client to drive that server, and neither of them is in the “migration” mood when a major version drops.

The adoption numbers make the case for urgency. The python-sdk repo sits at roughly 24,000 GitHub stars and 3,790 forks. PyPI’s raw download counter logged about 343 million downloads of the mcp package in the last 30 days, and the npm @modelcontextprotocol/sdk cleared roughly 198 million downloads over the same window. That raw counter includes CI and mirror traffic, so take it as a direction, not a census. The point stands: this is not a niche tool.

One gotcha: the Python and JavaScript SDKs are now on different version tracks. Python is at 2.0.0; the npm package is still on 1.30.0. Migrate them separately.

Here is the part that should grab you. The v1.x line is now in maintenance mode and will only receive security fixes from here on. That is a polite way of saying “your v1 test fixtures are frozen while the ecosystem moves on.” I have written about the testing side of this before in my MCP server test plan for tool-calling agents and my compatibility checklist for MCP server testing. This article is the migration piece that completes that series.

What actually shipped? Three things define the release. One SDK now speaks both protocol eras: the 2026-07-28 revision with stateless requests, server/discover, and subscriptions/listen, while still serving every 2025-era client. FastMCP is renamed to MCPServer, and a first-class Client replaces the old transport-plus-session-plus-initialize layering. Protocol types moved into their own mcp-types package, and OpenTelemetry tracing ships on by default. Those three changes cascade into dozens of smaller breaking changes.

The MCP 2.0 Migration Checklist

Before I show code, here is the full surface of what breaks when you bump from mcp 1.x to 2.x. Read this like a pre-flight checklist for your own test code.

Changes every MCP test harness hits

  • FastMCP renamed to MCPServer (import path changes).
  • mcp.types moved to the mcp-types package, imported as mcp_types.
  • Field names flipped from camelCase to snake_case: isError to is_error, nextCursor to next_cursor, inputSchema to input_schema.
  • McpError renamed to MCPError.
  • ClientSession replaced by a first-class Client object.
  • MCP_* environment variables and .env files are no longer read automatically.
  • Request timeouts take float seconds instead of timedelta, and timeouts now raise -32001 REQUEST_TIMEOUT instead of HTTP 408.

Changes that bite specific test setups

  • Streamable HTTP request bodies are capped at 4 MiB, returning HTTP 413 beyond that.
  • FileResource(is_binary=...) replaced by an encoding parameter.
  • MCPServer.get_context() is removed; inject ctx: Context as a handler parameter instead.
  • Context.client_id is removed.
  • httpx swapped for httpx2, and sse-starlette bumped to 3.x.
  • The testing helper create_connected_server_and_client_session is removed.
  • Content, ResourceReference, and Cursor type aliases renamed to ContentBlock, ResourceTemplateReference, and plain str.
  • Resource URIs changed from AnyUrl to plain str.
  • OAuth helpers: RFC7523OAuthClientProvider removed, scopes= became scope=, and timeout= dropped from OAuthClientProvider.

That is a lot of surface area, which is exactly why the official migration guide exists. The next sections cover the changes that actually show up in QA code, not the ones only library authors touch.

FastMCP Is Now MCPServer

This is the change every tutorial on the internet is now wrong about. If you learned MCP from a 2025 blog post, it started with FastMCP. That name is gone.

The rename and the new import path

# Before (v1)
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("Demo")
# After (v2)
from mcp.server.mcpserver import MCPServer, Context

mcp = MCPServer("Demo")

The decorator API itself is unchanged. Your @mcp.tool(), @mcp.resource(), and @mcp.prompt() decorators keep working. The rename is the breaking part, because a find-and-replace on the import is rarely enough once you hit the constructor changes below.

Constructor changes that will trip you

In v1, the second positional argument to FastMCP was instructions. In v2 it is a keyword argument.

# Before (v1)
mcp = FastMCP("Demo", "You answer questions about the weather.")
# After (v2)
mcp = MCPServer("Demo", instructions="You answer questions about the weather.")

Transport-specific settings also moved out of the constructor and into run(). If your test spins up a server on a specific port or with a JSON response mode, this is the one that breaks your smoke harness.

# Before (v1): transport params in the constructor
mcp = FastMCP("Server", host="0.0.0.0", port=9000, sse_path="/events")
mcp.run(transport="sse")
# After (v2): transport params passed to run()
mcp = MCPServer("Server")
mcp.run(transport="sse", host="0.0.0.0", port=9000, sse_path="/events")

The default server name also changed from "FastMCP" to "mcp-server", which matters if any of your assertions check the serverInfo.name field. Grep your test code for that string before you upgrade.

mcp.types Moved to mcp_types (and camelCase Died)

The second big breaking change is structural. Every protocol type now lives in its own package, mcp-types, imported as mcp_types, and published in lock-step with mcp.

The import split

# Before (v1)
from mcp.types import Tool, Resource
from mcp.shared.version import LATEST_PROTOCOL_VERSION
# After (v2)
from mcp_types import Tool, Resource
from mcp_types.version import LATEST_PROTOCOL_VERSION

There is a compatibility alias, so from mcp.types import ... still works for now. But the canonical home is mcp_types, and the per-version wire packages (mcp_types._v*) are private. If you are writing a shared QA library, import from mcp_types so your code does not rot.

camelCase to snake_case

This is the one that produces silent breakage, because Python attribute typos fail at runtime instead of at import. v1 exposed isError, nextCursor, and inputSchema. v2 exposes is_error, next_cursor, and input_schema.

# Before (v1)
result = await session.call_tool("my_tool", {"x": 1})
if result.isError:
    ...
tools = await session.list_tools()
cursor = tools.nextCursor
schema = tools.tools[0].inputSchema
# After (v2)
result = await session.call_tool("my_tool", {"x": 1})
if result.is_error:
    ...
tools = await session.list_tools()
cursor = tools.next_cursor
schema = tools.tools[0].input_schema

My advice: after migrating, run pytest with filterwarnings = ["error"] so any lingering deprecated attribute fails hard. The SDK ships an MCPDeprecationWarning category you can promote to error to catch stragglers in CI.

Type aliases renamed

A few aliases you probably used in type hints are gone or renamed. Content is now ContentBlock, ResourceReference is now ResourceTemplateReference, and pagination Cursor is just str. Resource URIs are plain strings now, not AnyUrl.

# Before (v1): AnyUrl rejected relative paths
from pydantic import AnyUrl
resource = Resource(name="test", uri=AnyUrl("users/me"))  # fails validation

# After (v2): plain strings accepted
resource = Resource(name="test", uri="users/me")  # works

ClientSession Is Gone: Meet the First-Class Client

For SDETs, this is the most important change, because your test client is where most of your MCP code lives. In v1 you stacked three concepts: a transport, a ClientSession, and an explicit initialize() call. In v2, one Client object does all of it, and it can connect to a URL, a stdio subprocess, a custom transport, or straight to a server object in memory.

The in-memory client for fast tests

The testing helper you probably used in v1 is gone. create_connected_server_and_client_session was removed. The replacement is cleaner: pass your server straight to Client and skip the transport entirely.

# Before (v1)
from mcp.shared.memory import create_connected_server_and_client_session

async with create_connected_server_and_client_session(server) as session:
    result = await session.call_tool("my_tool", {"x": 1})
# After (v2)
from mcp.client import Client

async with Client(server) as client:
    result = await client.call_tool("my_tool", {"x": 1})

That in-memory path is a big win for QA: your unit tests no longer pay the cost of a real transport, which means faster, less flaky test suites. It is also the first thing to migrate, because it exercises the most code with the least infrastructure.

Calling a live server

# After (v2): connect to a URL or a stdio subprocess
from mcp.client import Client

async with Client("http://localhost:8000/mcp") as client:
    tools = await client.list_tools()
    result = await client.call_tool("lookup_user", {"id": 42})

Version negotiation happens automatically. Client(target) figures out which protocol era the server speaks and talks to it accordingly, which is the piece that lets one test suite cover both old and new servers.

Capability access and pagination

If your test code read server capabilities through get_server_capabilities(), that is replaced by era-neutral accessors.

# Before (v1)
capabilities = session.get_server_capabilities()

# After (v2)
capabilities = client.server_capabilities
server_info = client.server_info
instructions = client.instructions
version = client.protocol_version

And list methods no longer take a bare cursor= argument. They take a params= object.

# After (v2)
from mcp_types import PaginatedRequestParams

tools = []
cursor = None
while True:
    page = await client.list_tools(params=PaginatedRequestParams(cursor=cursor))
    tools.extend(page.tools)
    if (cursor := page.next_cursor) is None:
        break

Errors, Timeouts, and Env Vars: The Sneaky Breakers

The headline renames are easy to find. This next set is the kind of change that passes a quick smoke test and then fails three weeks later in production.

McpError became MCPError

# Before (v1)
from mcp.shared.exceptions import McpError

try:
    result = await session.call_tool("my_tool")
except McpError as e:
    print(f"Error: {e.error.message}")
# After (v2)
from mcp.shared.exceptions import MCPError

try:
    result = await client.call_tool("my_tool")
except MCPError as e:
    print(f"Error: {e.message}")

Note the shape change: v1 wrapped the code and message in an .error object, while v2 exposes .code, .message, and .data directly. Any error-handling assertion in your suite needs both the import and the attribute access updated.

Timeouts: float seconds, and a new error code

v1 took timedelta for timeouts and surfaced a timeout as an HTTP 408. v2 takes plain float seconds and surfaces a JSON-RPC error code -32001 (REQUEST_TIMEOUT).

# Before (v1)
from datetime import timedelta
session = ClientSession(read_stream, write_stream, read_timeout_seconds=timedelta(seconds=30))

# After (v2)
session = ClientSession(read_stream, write_stream, read_timeout_seconds=30)
# Before (v1): check for 408
import httpx
from mcp.shared.exceptions import McpError
try:
    result = await session.call_tool("slow_tool", {})
except McpError as e:
    if e.error.code == httpx.codes.REQUEST_TIMEOUT:
        ...  # retry

# After (v2): check for -32001
from mcp.shared.exceptions import MCPError
from mcp_types import REQUEST_TIMEOUT
try:
    result = await client.call_tool("slow_tool", {})
except MCPError as e:
    if e.code == REQUEST_TIMEOUT:
        ...  # retry

MCP_* env vars are gone

v1 silently read MCP_* environment variables and .env files. v2 does not. If your CI pipeline configured the SDK through environment variables, those settings now do nothing, which is the quietest possible failure mode. The v2 approach is pydantic-settings for your own config, and explicit constructor arguments for the SDK.

# After (v2): pass settings explicitly, not via MCP_* env vars
import os
from mcp.server.mcpserver import MCPServer

mcp = MCPServer("Demo", debug=os.environ.get("MCP_DEBUG") == "true")

4 MiB request body limit

Streamable HTTP servers now reject request bodies over 4 MiB with HTTP 413. If any of your tests upload large payloads through an MCP tool (a big fixture file, a base64 asset, a dataset), they will start failing on upgrade. You can raise the limit with max_request_body_size in run(), but the better fix is to keep large artifacts off the MCP wire and pass a reference instead.

mcp.run(transport="streamable-http", max_request_body_size=8 * 1024 * 1024)

A 7-Step Migration Roadmap That Won’t Eat a Week

I have done a few of these migrations now, and the teams that finish fast all follow roughly the same sequence. Here is the playbook I give out.

  1. Pin v1 first. Before touching anything, add an upper bound so a careless pip install -U mcp does not break the build mid-migration: mcp>=1.28,<2. The v1.x line still gets security fixes, so this is a safe holding pattern, not a hack.
  2. Inventory every MCP import. Grep your repo for from mcp, FastMCP, ClientSession, McpError, and isError. You cannot migrate what you have not found.
  3. Migrate the in-memory client first. Swap create_connected_server_and_client_session for Client(server). This gives you the fastest feedback loop and the most code coverage per change.
  4. Rename servers. FastMCP to MCPServer, move transport params into run(), and fix instructions= as a keyword argument.
  5. Retype the data layer. mcp_types imports, snake_case fields, MCPError, and the ContentBlock / str alias changes.
  6. Fix timeouts and env config. timedelta to float seconds, 408 checks to -32001, and replace any MCP_* env var reliance with explicit settings.
  7. Re-run the full suite with warnings as errors. Promote MCPDeprecationWarning to error in pytest and let CI catch the stragglers you missed.

The trap most teams hit is the silent breakage: MCP_* env vars vanish without a warning, snake_case fields fail only when a code path runs, and the 4 MiB limit shows up as a confusing 413. Budget time for the quiet failures, not the loud ones.

The sequence matters less than the order of operations: in-memory client first, real transport second, data layer third, config last. Do not start with the OAuth or low-level Server code; leave the rare paths until the common ones are green.

A Working MCP 2.0 Smoke Test in Python

Here is a complete, runnable smoke test that exercises the v2 API end to end. It spins up an in-memory server, calls a tool, and asserts on the snake_case result shape. Save it as test_mcp_v2.py and run it with pytest.

import pytest
from mcp.server.mcpserver import MCPServer, Context
from mcp.client import Client
from mcp.shared.exceptions import MCPError

mcp = MCPServer("qa-demo")


@mcp.tool()
async def add(a: int, b: int, ctx: Context) -> int:
    await ctx.report_progress(1, 1, message="adding")
    return a + b


@pytest.mark.asyncio
async def test_add_tool_returns_snake_case_result():
    async with Client(mcp) as client:
        result = await client.call_tool("add", {"a": 2, "b": 3})
        assert result.is_error is False
        assert result.content[0].text == "5"


@pytest.mark.asyncio
async def test_tool_error_raises_mcperror():
    async with Client(mcp) as client:
        with pytest.raises(MCPError):
            await client.call_tool("missing_tool", {})

Two things to notice. First, ctx is injected as a parameter now, because mcp.get_context() is gone. Second, the result exposes is_error and content, the snake_case fields from the data-layer change. If this test passes, your core v2 wiring is correct and you can build outward from it.

Version Compatibility: Migrate the Server, Keep Testing Old Clients

One of the best design decisions in v2 is that a single MCPServer serves both protocol eras: the 2026-07-28 revision and every 2025-era client, over Streamable HTTP and stdio, with no extra configuration. Client(target) negotiates the version on connect.

That has a direct testing payoff. You do not have to migrate your entire test fleet in one big bang. You can migrate the server first, keep a legacy v1 client pinned with mcp>=1.28,<2, and use it as a compatibility harness against the new server. Then you migrate the client side on your own schedule. The official guidance is explicit that v1.x lives on the v1.x branch and keeps receiving critical bug fixes and security patches while you are mid-migration.

There is one caveat to plan for: at the 2026-07-28 revision, the server can no longer call the client. Tools return the question instead, using a Resolve(fn) parameter that the client fills in invisibly to the model. For testers that means sampling, elicitation, and roots flows behave differently across eras, and your compatibility tests should cover both directions. I walk through the memory and state side of this in my LangGraph agent memory testing guide.

The OpenTelemetry piece is a gift for QA. Tracing ships on by default in v2, so you get request-level visibility into your MCP traffic without wiring anything up. If you have been testing agents and wondering where a tool call went, the default traces answer that question for free.

India Context: MCP Skills in SDET Job Descriptions

This migration is not a hobbyist concern in the Indian QA market. I am seeing MCP and agent-testing skills appear in SDET and AI-QA job descriptions across Bengaluru, Hyderabad, and Pune, usually alongside LangChain, LangGraph, and the eval frameworks. A year ago the listings asked for Selenium and API testing. Now the same roles list “MCP server testing” or “AI agent test harness” as a differentiator.

The salary signal is real but uneven. A mid-level SDET in a product company in Bengaluru can command roughly ₹25-40 LPA, and candidates who can show an actual working MCP test harness, not just a tutorial screenshot, sit at the top of that band. The candidates who only know the 2025 FastMCP syntax are about to look outdated on exactly the skill that is newly in demand. Migration experience is a resume line now, because every team that adopted MCP in 2025 is about to do this exact upgrade, and most of them have no one who wants to own it.

If you are on the manual-to-automation track, this is a cheap way to stand out: migrate one open-source MCP server to v2, write the smoke test above, and put the PR link in your portfolio.

Key Takeaways

  • The MCP 2.0 migration is live: SDK 2.0.0 shipped July 28, 2026, pip install mcp now installs 2.x, and v1.x is in maintenance mode.
  • The rename is FastMCP to MCPServer, with transport params moving into run() and instructions= becoming a keyword argument.
  • mcp.types moved to mcp_types, fields flipped to snake_case, and McpError became MCPError.
  • ClientSession is replaced by a first-class Client, and the in-memory Client(server) path is the fastest way to migrate test code.
  • Pin v1 with mcp>=1.28,<2, migrate the in-memory client first, and promote MCPDeprecationWarning to error in CI.

FAQ

Do I have to migrate my MCP test fixtures right now?

No, but the clock is ticking. v1.x gets security fixes only, so every new server you integrate will increasingly target v2. Pinning mcp>=1.28,<2 buys time; ignoring the migration defers the same work to a busier week.

Will my old v1 test client still work against a v2 server?

Yes. A v2 MCPServer serves every 2025-era client from the same server, over Streamable HTTP and stdio, with no extra configuration. You can migrate the server and keep the legacy client as a compatibility harness while you catch up.

What is the fastest single change to make my suite pass on v2?

Swap the testing helper. Replace create_connected_server_and_client_session(server) with Client(server) and update the result attributes from camelCase to snake_case. That one change covers the majority of real-world test code.

Why do my large-payload MCP tests fail with HTTP 413 now?

Streamable HTTP servers cap request bodies at 4 MiB by default in v2. Either raise the limit with max_request_body_size=... in run(), or better, pass a reference to large artifacts instead of sending them over the MCP wire.

Where do I find the authoritative list of every breaking change?

The v2.0.0 release notes summarize the highlights, and the official migration guide lists every breaking change with before-and-after code. The npm SDK is a separate track, still on 1.30.0.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.