583 lines
19 KiB
Markdown
583 lines
19 KiB
Markdown
# 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
|