This guide covers the breaking changes introduced in v2 of the MCP Python SDK and how to update your code.
Version 2 of the MCP Python SDK introduces several breaking changes to improve the API, align with the MCP specification, and provide better type safety.
The deprecated streamablehttp_client function has been removed. Use streamable_http_client instead.
Before (v1):
from mcp.client.streamable_http import streamablehttp_client
async with streamablehttp_client(
url="http://localhost:8000/mcp",
headers={"Authorization": "Bearer token"},
timeout=30,
sse_read_timeout=300,
auth=my_auth,
) as (read_stream, write_stream, get_session_id):
...After (v2):
import httpx
from mcp.client.streamable_http import streamable_http_client
# Configure headers, timeout, and auth on the httpx.AsyncClient
http_client = httpx.AsyncClient(
headers={"Authorization": "Bearer token"},
timeout=httpx.Timeout(30, read=300),
auth=my_auth,
)
async with http_client:
async with streamable_http_client(
url="http://localhost:8000/mcp",
http_client=http_client,
) as (read_stream, write_stream, get_session_id):
...The headers, timeout, sse_read_timeout, and auth parameters have been removed from StreamableHTTPTransport. Configure these on the httpx.AsyncClient instead (see example above).
The following deprecated type aliases and classes have been removed from mcp.types:
| Removed | Replacement |
|---|---|
Content |
ContentBlock |
ResourceReference |
ResourceTemplateReference |
Cursor |
Use str directly |
MethodT |
Internal TypeVar, not intended for public use |
RequestParamsT |
Internal TypeVar, not intended for public use |
NotificationParamsT |
Internal TypeVar, not intended for public use |
Before (v1):
from mcp.types import Content, ResourceReference, CursorAfter (v2):
from mcp.types import ContentBlock, ResourceTemplateReference
# Use `str` instead of `Cursor` for pagination cursorsThe deprecated args parameter has been removed from ClientSessionGroup.call_tool(). Use arguments instead.
Before (v1):
result = await session_group.call_tool("my_tool", args={"key": "value"})After (v2):
result = await session_group.call_tool("my_tool", arguments={"key": "value"})The deprecated cursor parameter has been removed from the following ClientSession methods:
list_resources()list_resource_templates()list_prompts()list_tools()
Use params=PaginatedRequestParams(cursor=...) instead.
Before (v1):
result = await session.list_resources(cursor="next_page_token")
result = await session.list_tools(cursor="next_page_token")After (v2):
from mcp.types import PaginatedRequestParams
result = await session.list_resources(params=PaginatedRequestParams(cursor="next_page_token"))
result = await session.list_tools(params=PaginatedRequestParams(cursor="next_page_token"))The mount_path parameter has been removed from FastMCP.__init__(), FastMCP.run(), FastMCP.run_sse_async(), and FastMCP.sse_app(). It was also removed from the Settings class.
This parameter was redundant because the SSE transport already handles sub-path mounting via ASGI's standard root_path mechanism. When using Starlette's Mount("/path", app=mcp.sse_app()), Starlette automatically sets root_path in the ASGI scope, and the SseServerTransport uses this to construct the correct message endpoint path.
Transport-specific parameters have been moved from the FastMCP constructor to the run(), sse_app(), and streamable_http_app() methods. This provides better separation of concerns - the constructor now only handles server identity and authentication, while transport configuration is passed when starting the server.
Parameters moved:
host,port- HTTP server bindingsse_path,message_path- SSE transport pathsstreamable_http_path- StreamableHTTP endpoint pathjson_response,stateless_http- StreamableHTTP behaviorevent_store,retry_interval- StreamableHTTP event handlingtransport_security- DNS rebinding protection
Before (v1):
from mcp.server.fastmcp import FastMCP
# Transport params in constructor
mcp = FastMCP("Demo", json_response=True, stateless_http=True)
mcp.run(transport="streamable-http")
# Or for SSE
mcp = FastMCP("Server", host="0.0.0.0", port=9000, sse_path="/events")
mcp.run(transport="sse")After (v2):
from mcp.server.fastmcp import FastMCP
# Transport params passed to run()
mcp = FastMCP("Demo")
mcp.run(transport="streamable-http", json_response=True, stateless_http=True)
# Or for SSE
mcp = FastMCP("Server")
mcp.run(transport="sse", host="0.0.0.0", port=9000, sse_path="/events")For mounted apps:
When mounting FastMCP in a Starlette app, pass transport params to the app methods:
# Before (v1)
mcp = FastMCP("App", json_response=True)
app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())])
# After (v2)
mcp = FastMCP("App")
app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app(json_response=True))])Note: DNS rebinding protection is automatically enabled when host is 127.0.0.1, localhost, or ::1. This now happens in sse_app() and streamable_http_app() instead of the constructor.
The following union types are no longer RootModel subclasses:
ClientRequestServerRequestClientNotificationServerNotificationClientResultServerResultJSONRPCMessage
This means you can no longer access .root on these types or use model_validate() directly on them. Instead, use the provided TypeAdapter instances for validation.
Before (v1):
from mcp.types import ClientRequest, ServerNotification
# Using RootModel.model_validate()
request = ClientRequest.model_validate(data)
actual_request = request.root # Accessing the wrapped value
notification = ServerNotification.model_validate(data)
actual_notification = notification.rootAfter (v2):
from mcp.types import client_request_adapter, server_notification_adapter
# Using TypeAdapter.validate_python()
request = client_request_adapter.validate_python(data)
# No .root access needed - request is the actual type
notification = server_notification_adapter.validate_python(data)
# No .root access needed - notification is the actual typeAvailable adapters:
| Union Type | Adapter |
|---|---|
ClientRequest |
client_request_adapter |
ServerRequest |
server_request_adapter |
ClientNotification |
client_notification_adapter |
ServerNotification |
server_notification_adapter |
ClientResult |
client_result_adapter |
ServerResult |
server_result_adapter |
JSONRPCMessage |
jsonrpc_message_adapter |
All adapters are exported from mcp.types.
The nested RequestParams.Meta Pydantic model class has been replaced with a top-level RequestParamsMeta TypedDict. This affects the ctx.meta field in request handlers and any code that imports or references this type.
Key changes:
RequestParams.Meta(Pydantic model) →RequestParamsMeta(TypedDict)- Attribute access (
meta.progress_token) → Dictionary access (meta.get("progress_token")) progress_tokenfield changed fromProgressToken | None = NonetoNotRequired[ProgressToken]`
In request context handlers:
# Before (v1)
@server.call_tool()
async def handle_tool(name: str, arguments: dict) -> list[TextContent]:
ctx = server.request_context
if ctx.meta and ctx.meta.progress_token:
await ctx.session.send_progress_notification(ctx.meta.progress_token, 0.5, 100)
# After (v2)
@server.call_tool()
async def handle_tool(name: str, arguments: dict) -> list[TextContent]:
ctx = server.request_context
if ctx.meta and "progress_token" in ctx.meta:
await ctx.session.send_progress_notification(ctx.meta["progress_token"], 0.5, 100)The uri field on resource-related types now uses str instead of Pydantic's AnyUrl. This aligns with the MCP specification schema which defines URIs as plain strings (uri: string) without strict URL validation. This change allows relative paths like users/me that were previously rejected.
Before (v1):
from pydantic import AnyUrl
from mcp.types import Resource
# Required wrapping in AnyUrl
resource = Resource(name="test", uri=AnyUrl("users/me")) # Would fail validationAfter (v2):
from mcp.types import Resource
# Plain strings accepted
resource = Resource(name="test", uri="users/me") # Works
resource = Resource(name="test", uri="custom://scheme") # Works
resource = Resource(name="test", uri="https://example.com") # WorksIf your code passes AnyUrl objects to URI fields, convert them to strings:
# If you have an AnyUrl from elsewhere
uri = str(my_any_url) # Convert to stringAffected types:
Resource.uriReadResourceRequestParams.uriResourceContents.uri(and subclassesTextResourceContents,BlobResourceContents)SubscribeRequestParams.uriUnsubscribeRequestParams.uriResourceUpdatedNotificationParams.uri
The Client and ClientSession methods read_resource(), subscribe_resource(), and unsubscribe_resource() now only accept str for the uri parameter. If you were passing AnyUrl objects, convert them to strings:
# Before (v1)
from pydantic import AnyUrl
await client.read_resource(AnyUrl("test://resource"))
# After (v2)
await client.read_resource("test://resource")
# Or if you have an AnyUrl from elsewhere:
await client.read_resource(str(my_any_url))MCP protocol types no longer accept arbitrary extra fields at the top level. This matches the MCP specification which only allows extra fields within _meta objects, not on the types themselves.
# This will now raise a validation error
from mcp.types import CallToolRequestParams
params = CallToolRequestParams(
name="my_tool",
arguments={},
unknown_field="value", # ValidationError: extra fields not permitted
)
# Extra fields are still allowed in _meta
params = CallToolRequestParams(
name="my_tool",
arguments={},
_meta={"progressToken": "tok", "customField": "value"}, # OK
)The streamable_http_app() method is now available directly on the lowlevel Server class, not just FastMCP. This allows using the streamable HTTP transport without the FastMCP wrapper.
from mcp.server.lowlevel.server import Server
server = Server("my-server")
# Register handlers...
@server.list_tools()
async def list_tools():
return [...]
# Create a Starlette app for streamable HTTP
app = server.streamable_http_app(
streamable_http_path="/mcp",
json_response=False,
stateless_http=False,
)The lowlevel Server also now exposes a session_manager property to access the StreamableHTTPSessionManager after calling streamable_http_app().
If you encounter issues during migration:
- Check the API Reference for updated method signatures
- Review the examples for updated usage patterns
- Open an issue on GitHub if you find a bug or need further assistance