# Migration Guide: v1 to v2 This guide covers the breaking changes introduced in v2 of the MCP Python SDK and how to update your code. ## Overview 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. ## Breaking Changes ### `streamablehttp_client` removed The deprecated `streamablehttp_client` function has been removed. Use `streamable_http_client` instead. **Before (v1):** ```python 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):** ```python 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` callback removed from `streamable_http_client` The `get_session_id` callback (third element of the returned tuple) has been removed from `streamable_http_client`. The function now returns a 2-tuple `(read_stream, write_stream)` instead of a 3-tuple. If you need to capture the session ID (e.g., for session resumption testing), you can use httpx event hooks to capture it from the response headers: **Before (v1):** ```python from mcp.client.streamable_http import streamable_http_client async with streamable_http_client(url) as (read_stream, write_stream, get_session_id): async with ClientSession(read_stream, write_stream) as session: await session.initialize() session_id = get_session_id() # Get session ID via callback ``` **After (v2):** ```python import httpx from mcp.client.streamable_http import streamable_http_client # Option 1: Simply ignore if you don't need the session ID async with streamable_http_client(url) as (read_stream, write_stream): async with ClientSession(read_stream, write_stream) as session: await session.initialize() # Option 2: Capture session ID via httpx event hooks if needed captured_session_ids: list[str] = [] async def capture_session_id(response: httpx.Response) -> None: session_id = response.headers.get("mcp-session-id") if session_id: captured_session_ids.append(session_id) http_client = httpx.AsyncClient( event_hooks={"response": [capture_session_id]}, follow_redirects=True, ) async with http_client: async with streamable_http_client(url, http_client=http_client) as (read_stream, write_stream): async with ClientSession(read_stream, write_stream) as session: await session.initialize() session_id = captured_session_ids[0] if captured_session_ids else None ``` ### `StreamableHTTPTransport` parameters removed The `headers`, `timeout`, `sse_read_timeout`, and `auth` parameters have been removed from `StreamableHTTPTransport`. Configure these on the `httpx.AsyncClient` instead (see example above). ### Removed type aliases and classes 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):** ```python from mcp.types import Content, ResourceReference, Cursor ``` **After (v2):** ```python from mcp.types import ContentBlock, ResourceTemplateReference # Use `str` instead of `Cursor` for pagination cursors ``` ### `args` parameter removed from `ClientSessionGroup.call_tool()` The deprecated `args` parameter has been removed from `ClientSessionGroup.call_tool()`. Use `arguments` instead. **Before (v1):** ```python result = await session_group.call_tool("my_tool", args={"key": "value"}) ``` **After (v2):** ```python result = await session_group.call_tool("my_tool", arguments={"key": "value"}) ``` ### `cursor` parameter removed from `ClientSession` list methods 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):** ```python result = await session.list_resources(cursor="next_page_token") result = await session.list_tools(cursor="next_page_token") ``` **After (v2):** ```python 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")) ``` ### `McpError` renamed to `MCPError` The `McpError` exception class has been renamed to `MCPError` for consistent naming with the MCP acronym style used throughout the SDK. **Before (v1):** ```python 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):** ```python from mcp.shared.exceptions import MCPError try: result = await session.call_tool("my_tool") except MCPError as e: print(f"Error: {e.message}") ``` `MCPError` is also exported from the top-level `mcp` package: ```python from mcp import MCPError ``` ### `FastMCP` renamed to `MCPServer` The `FastMCP` class has been renamed to `MCPServer` to better reflect its role as the main server class in the SDK. This is a simple rename with no functional changes to the class itself. **Before (v1):** ```python from mcp.server.fastmcp import FastMCP mcp = FastMCP("Demo") ``` **After (v2):** ```python from mcp.server.mcpserver import MCPServer mcp = MCPServer("Demo") ``` ### `mount_path` parameter removed from MCPServer The `mount_path` parameter has been removed from `MCPServer.__init__()`, `MCPServer.run()`, `MCPServer.run_sse_async()`, and `MCPServer.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 moved from MCPServer constructor to run()/app methods Transport-specific parameters have been moved from the `MCPServer` 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 binding - `sse_path`, `message_path` - SSE transport paths - `streamable_http_path` - StreamableHTTP endpoint path - `json_response`, `stateless_http` - StreamableHTTP behavior - `event_store`, `retry_interval` - StreamableHTTP event handling - `transport_security` - DNS rebinding protection **Before (v1):** ```python 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):** ```python from mcp.server.mcpserver import MCPServer # Transport params passed to run() mcp = MCPServer("Demo") mcp.run(transport="streamable-http", json_response=True, stateless_http=True) # Or for SSE mcp = MCPServer("Server") mcp.run(transport="sse", host="0.0.0.0", port=9000, sse_path="/events") ``` **For mounted apps:** When mounting in a Starlette app, pass transport params to the app methods: ```python # Before (v1) from mcp.server.fastmcp import FastMCP mcp = FastMCP("App", json_response=True) app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())]) # After (v2) from mcp.server.mcpserver import MCPServer mcp = MCPServer("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. ### Replace `RootModel` by union types with `TypeAdapter` validation The following union types are no longer `RootModel` subclasses: - `ClientRequest` - `ServerRequest` - `ClientNotification` - `ServerNotification` - `ClientResult` - `ServerResult` - `JSONRPCMessage` 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):** ```python 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.root ``` **After (v2):** ```python 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 type ``` **Available 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`. ### `RequestParams.Meta` replaced with `RequestParamsMeta` TypedDict 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_token` field changed from `ProgressToken | None = None` to `NotRequired[ProgressToken]` ` **In request context handlers:** ```python # 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) ``` ### Resource URI type changed from `AnyUrl` to `str` The `uri` field on resource-related types now uses `str` instead of Pydantic's `AnyUrl`. This aligns with the [MCP specification schema](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/draft/schema.ts) 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):** ```python from pydantic import AnyUrl from mcp.types import Resource # Required wrapping in AnyUrl resource = Resource(name="test", uri=AnyUrl("users/me")) # Would fail validation ``` **After (v2):** ```python 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") # Works ``` If your code passes `AnyUrl` objects to URI fields, convert them to strings: ```python # If you have an AnyUrl from elsewhere uri = str(my_any_url) # Convert to string ``` Affected types: - `Resource.uri` - `ReadResourceRequestParams.uri` - `ResourceContents.uri` (and subclasses `TextResourceContents`, `BlobResourceContents`) - `SubscribeRequestParams.uri` - `UnsubscribeRequestParams.uri` - `ResourceUpdatedNotificationParams.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: ```python # 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)) ``` ### Low-Level Server Decorator-Based API Removed The decorator-based API for registering handlers on the low-level `Server` class has been removed. Use the constructor-based handler registration instead, which provides better type safety and clearer dependencies. **Before (v1):** ```python from mcp.server.lowlevel import Server server = Server("my-server") @server.list_tools() async def list_tools(): return [types.Tool(name="tool", description="...")] @server.call_tool() async def call_tool(name: str, arguments: dict): return {"result": "..."} ``` **After (v2):** ```python from mcp.server.lowlevel import Server from mcp.server.session import ServerSession from mcp.shared.context import RequestContext import mcp.types as types from typing import Any async def list_tools( ctx: RequestContext[ServerSession, Any, Any], params: types.PaginatedRequestParams | None, ) -> types.ListToolsResult: return types.ListToolsResult(tools=[ types.Tool(name="tool", description="...", input_schema={"type": "object"}) ]) async def call_tool( ctx: RequestContext[ServerSession, Any, Any], params: types.CallToolRequestParams, ) -> types.CallToolResult: return types.CallToolResult( content=[types.TextContent(type="text", text="result")] ) server = Server( "my-server", on_list_tools=list_tools, on_call_tool=call_tool, ) ``` **Key differences:** 1. Handlers receive `(context, params)` instead of extracted arguments 2. Handlers return proper result types (`ListToolsResult`, `CallToolResult`, etc.) 3. Context provides access to session, lifespan data, and request metadata 4. Handlers are passed to the constructor using `on_*` parameters **Migration steps:** 1. Update handler signatures to accept `(ctx, params)` 2. Update return types to use proper result classes 3. Pass handlers to the Server constructor using `on_*` parameters 4. Remove decorator calls **Available constructor parameters:** | Constructor Parameter | Params Type | Return Type | |-----------------------|-------------|-------------| | `on_list_prompts` | `PaginatedRequestParams \| None` | `ListPromptsResult` | | `on_get_prompt` | `GetPromptRequestParams` | `GetPromptResult` | | `on_list_resources` | `PaginatedRequestParams \| None` | `ListResourcesResult` | | `on_list_resource_templates` | `PaginatedRequestParams \| None` | `ListResourceTemplatesResult` | | `on_read_resource` | `ReadResourceRequestParams` | `ReadResourceResult` | | `on_subscribe_resource` | `SubscribeRequestParams` | `EmptyResult` | | `on_unsubscribe_resource` | `UnsubscribeRequestParams` | `EmptyResult` | | `on_list_tools` | `PaginatedRequestParams \| None` | `ListToolsResult` | | `on_call_tool` | `CallToolRequestParams` | `CallToolResult` | | `on_set_logging_level` | `SetLevelRequestParams` | `EmptyResult` | | `on_completion` | `CompleteRequestParams` | `CompleteResult` | | `on_progress_notification` | `ProgressNotificationParams` | `None` | **Benefits:** - Context available in all handlers (session, lifespan data, request metadata) - Type-safe params and return types - Clearer dependencies at construction time - Better testability (handlers can be mocked/replaced) ## Bug Fixes ### Extra fields no longer allowed on top-level MCP types 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. ```python # 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 ) ``` ## New Features ### `streamable_http_app()` available on lowlevel Server The `streamable_http_app()` method is now available directly on the lowlevel `Server` class, not just `MCPServer`. This allows using the streamable HTTP transport without the MCPServer wrapper. ```python from mcp.server.lowlevel.server import Server from mcp.server.session import ServerSession from mcp.shared.context import RequestContext import mcp.types as types from typing import Any async def list_tools( ctx: RequestContext[ServerSession, Any, Any], params: types.PaginatedRequestParams | None, ) -> types.ListToolsResult: return types.ListToolsResult(tools=[ types.Tool(name="my_tool", description="...", inputSchema={"type": "object"}) ]) server = Server("my-server", on_list_tools=list_tools) # 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()`. ## Need Help? If you encounter issues during migration: 1. Check the [API Reference](api.md) for updated method signatures 2. Review the [examples](https://github.com/modelcontextprotocol/python-sdk/tree/main/examples) for updated usage patterns 3. Open an issue on [GitHub](https://github.com/modelcontextprotocol/python-sdk/issues) if you find a bug or need further assistance