Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions docs/get-started/first-steps.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,6 @@ Three plain functions, three decorators. Each decorator is the entire registrati

Everything else (the name, the description, the argument schema) the SDK reads from the function itself: its name, its docstring, its type hints. You never declared any of it separately.

!!! tip
The two halves of the SDK have two import paths: `from mcp import Client` and
`from mcp.server import MCPServer`. There is no `from mcp import MCPServer`.

### Try it

Run it with the MCP Inspector:
Expand Down
13 changes: 11 additions & 2 deletions docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@
| `AnyFunction` | Use `Callable[..., Any]` directly |
| `ClientRequestType`, `ClientNotificationType`, `ClientResultType`, `ServerRequestType`, `ServerNotificationType`, `ServerResultType` | The union is now the bare name: `ClientRequest`, `ClientNotification`, `ClientResult`, `ServerRequest`, `ServerNotification`, `ServerResult` |
| `TaskExecutionMode`, `TASK_FORBIDDEN`, `TASK_OPTIONAL`, `TASK_REQUIRED`, `TASK_STATUS_*` | Use string literals; `TaskStatus` remains as the literal-union type |
| `SamplingRole` (a top-level `mcp` alias for `Role`, never a `mcp.types` name) | `mcp.types.Role` |

**Before (v1):**

Expand Down Expand Up @@ -657,6 +658,8 @@
mcp = MCPServer("Demo")
```

`MCPServer` is also exported from the top-level `mcp` package, next to `Client` (`from mcp import MCPServer, Client`); `mcp.server` and `mcp.server.mcpserver` keep working as import paths.

`Context` is the type annotation for the `ctx` parameter injected into tools, resources, and prompts (see [`get_context()` removed](#mcpserverget_context-removed) below). The `ctx.fastmcp` property is now `ctx.mcp_server`.

All submodules under `mcp.server.fastmcp.*` are now under `mcp.server.mcpserver.*` with the same structure. Common imports:
Expand Down Expand Up @@ -2166,6 +2169,12 @@

`sse_client` is unchanged apart from the `httpx2` retyping: it still takes `url`, `headers`, `timeout`, `sse_read_timeout`, `httpx_client_factory` (which must now return an `httpx2.AsyncClient`), `auth` (now `httpx2.Auth | None`), and `on_session_created`. Only the streamable HTTP transport dropped its transport-level parameters; `StreamableHTTPTransport(url)` now takes just the URL.

### `StreamableHTTPError`, `ResumptionError`, and `StreamableHTTPTransport.get_session_id()` removed

`mcp.client.streamable_http` no longer defines `StreamableHTTPError` or its `ResumptionError` subclass: the transport never raised either from a code path a caller could catch, so `from mcp.client.streamable_http import StreamableHTTPError, ResumptionError` now raises `ImportError`. Transport failures surface as per-request JSON-RPC errors (see [Streamable HTTP: non-2xx responses now surface as per-request JSON-RPC errors](#streamable-http-non-2xx-responses-now-surface-as-per-request-json-rpc-errors)) or as the underlying `httpx2` exception; catch those instead.

The unused `StreamableHTTPTransport.get_session_id()` method is gone as well; read the `session_id` attribute directly (or capture the header, as in [`get_session_id` callback removed](#get_session_id-callback-removed-from-streamable_http_client) above).

### `StreamableHTTPTransport.protocol_version` attribute removed

The transport no longer holds per-connection protocol state; era-dependent headers (e.g. `MCP-Protocol-Version`) are now supplied per-message by the session. If you were reading `transport.protocol_version` to learn the negotiated version, read `session.protocol_version` (or `client.protocol_version` on the high-level `Client`) instead.
Expand Down Expand Up @@ -2768,10 +2777,10 @@
The deprecation and the back-channel are separate axes. Sampling, roots, and push elicitation are server-initiated *requests*, so on a connection negotiated at 2026-07-28 — including the default in-process `Client(server)` — `create_message()`, `list_roots()`, and `elicit()` / `elicit_form()` raise `NoBackChannelError` rather than working with a warning; the resolver markers `Sample`, `ListRoots`, and `Elicit` are the era-portable form (see [Server-initiated sampling, elicitation, and roots raise `NoBackChannelError`](#server-initiated-sampling-elicitation-and-roots-raise-nobackchannelerror)).

The user-facing methods for these features now carry `typing_extensions.deprecated`, so type checkers, IDEs, and the runtime surface a deprecation warning where they are called:

- Sampling: `ServerSession.create_message()`, `ClientPeer.sample()`
- Roots: `ServerSession.list_roots()`, `ClientPeer.list_roots()`, `ClientSession.send_roots_list_changed()`, `Client.send_roots_list_changed()`
- Logging: `ServerSession.send_log_message()`, `Connection.log()`, `ClientSession.set_logging_level()`, `Client.set_logging_level()`, `mcp.server.context.Context.log()` (the lowlevel `Context`), and the `MCPServer` `Context` helpers `log()`, `debug()`, `info()`, `warning()`, `error()`
- Logging: `ServerSession.send_log_message()`, `Connection.log()`, `ClientSession.set_logging_level()`, `Client.set_logging_level()`, and the `MCPServer` `Context` helpers `log()`, `debug()`, `info()`, `warning()`, `error()`

Check failure on line 2783 in docs/migration.md

View check run for this annotation

Claude / Claude Code Review

Context privatization breaking change missing from docs/migration.md

The breaking change moving the lowlevel `Context` class from the public `mcp.server.context` module to the private `mcp.server._context` module is not documented in `docs/migration.md`, even though the PR description states it is. AGENTS.md requires all breaking changes to be documented there — please add a short migration entry (grouped with the existing lowlevel-server/context sections) noting the class is now private and that `from mcp.server.context import Context` raises `ImportError`.
Comment on lines 2780 to +2783

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 The breaking change moving the lowlevel Context class from the public mcp.server.context module to the private mcp.server._context module is not documented in docs/migration.md, even though the PR description states it is. AGENTS.md requires all breaking changes to be documented there — please add a short migration entry (grouped with the existing lowlevel-server/context sections) noting the class is now private and that from mcp.server.context import Context raises ImportError.

Extended reasoning...

What the bug is. This PR moves the lowlevel server Context class out of the public mcp.server.context module into a new private mcp.server._context module. That is a breaking change: any code doing from mcp.server.context import Context now raises ImportError. The PR description explicitly claims this is covered: "Yes, all documented in docs/migration.md: ... mcp.server.context.Context is now private" — but no such entry exists in the guide.

Verification. Grepping the post-PR docs/migration.md for _context, Context is now private, and related phrases finds nothing. The PR's migration.md edits are only: the SamplingRole table row, the MCPServer top-level export note, the new StreamableHTTPError/ResumptionError/get_session_id() section, the NoBackChannelError export note, and — at lines 2780–2783 — the removal of the mcp.server.context.Context.log() mention from the SEP-2577 deprecated-logging list. That removed mention also confirms the class was documented public surface before this PR: the guide referenced it as live API, and the PR deleted the reference rather than documenting the move.

Why this matters. AGENTS.md states: "Breaking changes (including those softened by a backwards-compatibility shim) must be documented in docs/migration.md." This is an explicit repo-mandated requirement, and the guide already carries intra-v2 pre-release notes (since 2.0.0b2, since 2.0.0rc1), so "Context never existed in v1" does not excuse the omission. Notably, the three other removals in this same PR (SamplingRole; StreamableHTTPError/ResumptionError/get_session_id()) each received a dedicated migration section — the omission looks like an oversight, not a policy decision, and the author's own PR description shows the entry was intended.

Concrete walk-through. (1) A v2-RC user has middleware or tests importing from mcp.server.context import Context (a public, documented path before this PR — the migration guide itself referenced mcp.server.context.Context.log()). (2) They upgrade past this PR. (3) The import raises ImportError. (4) They open docs/migration.md — the canonical record of every breaking change per AGENTS.md — and search for Context or mcp.server.context: no entry explains the class moved to mcp.server._context or that it is now private. They are left to bisect the SDK source.

Mitigating factors considered. One verifier noted the class's own new docstring says "Private for now: ServerRunner does not construct it", so real-world importers are likely rare, and the fix is docs-only. That argues for lower impact, but the repo's own instructions make documenting breaking changes in migration.md a hard requirement, and the PR affirmatively (and incorrectly) claims compliance — this is not a stale-description case, since the entry was clearly intended and simply never written.

How to fix. Add a short section to docs/migration.md (grouped near the existing lowlevel-Server/context sections, e.g. after the RequestContext split section) stating: the lowlevel Context class moved from mcp.server.context to the private mcp.server._context module and is no longer public API; from mcp.server.context import Context now raises ImportError; ServerRequestContext and the middleware types (CallNext, HandlerResult, ServerMiddleware) in mcp.server.context are unchanged; handlers receive a ServerRequestContext, which is the supported surface.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

migration.md is for breaking changes from v1 to v2. this class wasn't in v1 so there's no reason for it to be in migration.md


Registering a handler for a deprecated capability is deprecated too. The `Server.__init__` parameters `on_set_logging_level` (Logging) and `on_roots_list_changed` (Roots) are now split out into a `typing_extensions.deprecated` overload, so passing either is flagged by type checkers and emits `mcp.MCPDeprecationWarning` at construction time. `on_progress` follows the same pattern (see below). The non-deprecated overload omits these parameters, so the common case stays warning-free.

Expand Down Expand Up @@ -2804,7 +2813,7 @@

The 2026-07-28 protocol has no server-initiated requests, so a handler that reaches back to the client mid-request — `ctx.elicit()`, `ctx.elicit_url()`, `ctx.session.create_message()`, `ctx.session.list_roots()`, or any other `ServerSession` request helper — raises `NoBackChannelError` on such a connection instead of sending. An in-process `Client(server)` negotiates 2026-07-28 by default (see [`Client` defaults to `mode='auto'`](#client-defaults-to-modeauto)), so the first smoke test of an unchanged v1 sampling or elicitation tool fails, and setting `sampling_callback=` / `elicitation_callback=` on the client changes nothing because no request ever reaches the client.

`NoBackChannelError` lives in `mcp.shared.exceptions` and subclasses `MCPError` (code `-32600`, message `Cannot send '<method>': this transport context has no back-channel for server-initiated requests.`). Raised inside an `@mcp.tool()` it reaches the client as a top-level JSON-RPC error, not `CallToolResult(is_error=True)` — see [`MCPError` raised from an `@mcp.tool()` handler now surfaces as a JSON-RPC error](#mcperror-raised-from-an-mcptool-handler-now-surfaces-as-a-json-rpc-error) — and the [Troubleshooting](troubleshooting.md) page walks through the client-side traceback. The same exception is raised on a legacy session against a `stateless_http=True` server, and on the request-scoped channel of a stateful legacy session against a `json_response=True` server (a JSON body carries exactly one response, so a mid-request `ctx.elicit()` cannot ride it; the session's standalone `GET` stream still carries unrelated messages) — both places v1 dropped the message and stalled ([`Server.run()` no longer takes a `stateless` flag](#serverrun-no-longer-takes-a-stateless-flag)). Notifications never raise it: `send_log_message()`, `send_tool_list_changed()`, and the other notification helpers are dropped with a debug log where no channel exists, and `UrlElicitationRequiredError` from a tool is unaffected (it is an error response, not a request).
`NoBackChannelError` is exported from the top-level `mcp` package (`from mcp import NoBackChannelError`; it lives in `mcp.shared.exceptions`) and subclasses `MCPError` (code `-32600`, message `Cannot send '<method>': this transport context has no back-channel for server-initiated requests.`). Raised inside an `@mcp.tool()` it reaches the client as a top-level JSON-RPC error, not `CallToolResult(is_error=True)` — see [`MCPError` raised from an `@mcp.tool()` handler now surfaces as a JSON-RPC error](#mcperror-raised-from-an-mcptool-handler-now-surfaces-as-a-json-rpc-error) — and the [Troubleshooting](troubleshooting.md) page walks through the client-side traceback. The same exception is raised on a legacy session against a `stateless_http=True` server, and on the request-scoped channel of a stateful legacy session against a `json_response=True` server (a JSON body carries exactly one response, so a mid-request `ctx.elicit()` cannot ride it; the session's standalone `GET` stream still carries unrelated messages) — both places v1 dropped the message and stalled ([`Server.run()` no longer takes a `stateless` flag](#serverrun-no-longer-takes-a-stateless-flag)). Notifications never raise it: `send_log_message()`, `send_tool_list_changed()`, and the other notification helpers are dropped with a debug log where no channel exists, and `UrlElicitationRequiredError` from a tool is unaffected (it is an error response, not a request).

Two ways to migrate:

Expand Down
2 changes: 2 additions & 0 deletions docs/whats-new.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ from mcp.server import MCPServer # v1: from mcp.server.fastmcp import FastMCP
mcp = MCPServer("Demo") # v1: FastMCP("Demo")
```

Both halves of the SDK are also on the top-level package, so `from mcp import MCPServer, Client` works.

It is also, for a decorator-built server, most of the port. `@mcp.tool()`, `@mcp.resource()`, and `@mcp.prompt()` accept what they accepted in v1 (`@mcp.resource()` adds one optional `security=` keyword), and the input schema still comes from your type hints. Around the edges: everything under `mcp.server.fastmcp.*` now lives under `mcp.server.mcpserver.*`, `ctx.fastmcp` is `ctx.mcp_server`, `get_context()` is gone (declare a `ctx: Context` parameter instead), and the exception base `FastMCPError` is `MCPServerError`. The **[Migration Guide](migration.md#fastmcp-renamed-to-mcpserver)** has the import table.

### `Resolve`: the new way to ask the user for input
Expand Down
2 changes: 1 addition & 1 deletion docs_src/subscriptions/tutorial005.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import anyio

from mcp import Client
from mcp.client.subscriptions import SubscriptionLost
from mcp.client import SubscriptionLost

from .tutorial003 import read_board

Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ select = [
"F", # pyflakes
"I", # isort
"PERF", # Perflint
"RUF022", # unsorted-dunder-all: keep `__all__` sorted
"UP", # pyupgrade
"TID251", # https://docs.astral.sh/ruff/rules/banned-api/
]
Expand All @@ -219,6 +220,8 @@ max-complexity = 24 # Default is 10
"__init__.py" = ["F401"]
# The mcp.types package is an alias that mirrors mcp_types namespaces by design.
"src/mcp/types/*.py" = ["F403"]
# mcp_types' __all__ is grouped by section comments (capabilities, content types, requests, ...) rather than sorted.
"src/mcp-types/mcp_types/__init__.py" = ["RUF022"]
# Generated by scripts/gen_surface_types.py: raw datamodel-codegen output (TID251 lifts the repo-wide RootModel ban for these generated validators).
"src/mcp-types/mcp_types/_v*/__init__.py" = ["D212", "E501", "I001", "TID251", "UP007", "UP037"]
"tests/server/mcpserver/test_func_metadata.py" = ["E501"]
Expand Down
2 changes: 1 addition & 1 deletion src/mcp-types/mcp_types/methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
"CLIENT_NOTIFICATIONS",
"CLIENT_REQUESTS",
"CLIENT_RESULTS",
"CacheableMethod",
"INPUT_REQUIRED_METHODS",
"MONOLITH_NOTIFICATIONS",
"MONOLITH_REQUESTS",
Expand All @@ -37,6 +36,7 @@
"SERVER_RESULTS",
"SPEC_CLIENT_METHODS",
"SPEC_CLIENT_NOTIFICATION_METHODS",
"CacheableMethod",
"is_input_required",
"parse_client_notification",
"parse_client_request",
Expand Down
8 changes: 4 additions & 4 deletions src/mcp-types/mcp_types/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@
from typing import Final

__all__ = [
"KNOWN_PROTOCOL_VERSIONS",
"HANDSHAKE_PROTOCOL_VERSIONS",
"MODERN_PROTOCOL_VERSIONS",
"SUPPORTED_PROTOCOL_VERSIONS",
"LATEST_PROTOCOL_VERSION",
"KNOWN_PROTOCOL_VERSIONS",
"LATEST_HANDSHAKE_VERSION",
"LATEST_MODERN_VERSION",
"LATEST_PROTOCOL_VERSION",
"MODERN_PROTOCOL_VERSIONS",
"OLDEST_SUPPORTED_VERSION",
"SUPPORTED_PROTOCOL_VERSIONS",
"is_version_at_least",
]

Expand Down
13 changes: 7 additions & 6 deletions src/mcp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@
ToolUseContent,
UnsubscribeRequest,
)
from mcp_types import Role as SamplingRole

# Bind the `mcp.types` submodule on the package, as v1's `from .types import
# ...` did, so `import mcp` followed by `mcp.types.Tool` keeps working.
Expand All @@ -66,9 +65,10 @@
from .client.session import ClientSession
from .client.session_group import ClientSessionGroup
from .client.stdio import StdioServerParameters, stdio_client
from .server.mcpserver import MCPServer
from .server.session import ServerSession
from .server.stdio import stdio_server
from .shared.exceptions import MCPDeprecationWarning, MCPError, UrlElicitationRequiredError
from .shared.exceptions import MCPDeprecationWarning, MCPError, NoBackChannelError, UrlElicitationRequiredError
from .shared.uri_template import InvalidUriTemplate, UriTemplate

__all__ = [
Expand All @@ -93,6 +93,7 @@
"InitializeResult",
"InitializedNotification",
"InputRequiredRoundsExceededError",
"InvalidUriTemplate",
"JSONRPCError",
"JSONRPCRequest",
"JSONRPCResponse",
Expand All @@ -105,22 +106,23 @@
"LoggingMessageNotification",
"MCPDeprecationWarning",
"MCPError",
"MCPServer",
"NoBackChannelError",
"Notification",
"PingRequest",
"ProgressNotification",
"PromptsCapability",
"ReadResourceRequest",
"ReadResourceResult",
"Resource",
"ResourcesCapability",
"ResourceUpdatedNotification",
"ResourcesCapability",
"RootsCapability",
"SamplingCapability",
"SamplingContent",
"SamplingContextCapability",
"SamplingMessage",
"SamplingMessageContentBlock",
"SamplingRole",
"SamplingToolsCapability",
"ServerCapabilities",
"ServerNotification",
Expand All @@ -134,12 +136,11 @@
"Tool",
"ToolChoice",
"ToolResultContent",
"ToolsCapability",
"ToolUseContent",
"ToolsCapability",
"UnsubscribeRequest",
"UriTemplate",
"UrlElicitationRequiredError",
"InvalidUriTemplate",
"stdio_client",
"stdio_server",
]
3 changes: 1 addition & 2 deletions src/mcp/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,4 @@

from .cli import app

if __name__ == "__main__": # pragma: no cover
app()
__all__ = ["app"]
6 changes: 5 additions & 1 deletion src/mcp/client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
advertise,
)
from mcp.client.session import ClientSession, IncomingMessage
from mcp.client.subscriptions import ListenNotSupportedError, Subscription, SubscriptionLost

__all__ = [
"CacheConfig",
Expand All @@ -32,12 +33,15 @@
"ClientExtension",
"ClientRequestContext",
"ClientSession",
"IncomingMessage",
"InMemoryResponseCacheStore",
"IncomingMessage",
"InputRequiredRoundsExceededError",
"ListenNotSupportedError",
"NotificationBinding",
"ResponseCacheStore",
"ResultClaim",
"Subscription",
"SubscriptionLost",
"Transport",
"UnexpectedClaimedResult",
"advertise",
Expand Down
2 changes: 1 addition & 1 deletion src/mcp/client/_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from mcp.shared._stream_protocols import ReadStream, WriteStream
from mcp.shared.message import SessionMessage

__all__ = ["ReadStream", "WriteStream", "Transport", "TransportStreams"]
__all__ = ["ReadStream", "Transport", "TransportStreams", "WriteStream"]

TransportStreams = tuple[ReadStream[SessionMessage | Exception], WriteStream[SessionMessage]]

Expand Down
31 changes: 6 additions & 25 deletions src/mcp/client/streamable_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,6 @@
MAX_RECONNECTION_ATTEMPTS = 2 # Max retry attempts before giving up


class StreamableHTTPError(Exception):
"""Base exception for StreamableHTTP transport errors."""


class ResumptionError(StreamableHTTPError):
"""Raised when resumption request is invalid."""


@dataclass
class RequestContext:
"""Context for a request operation."""
Expand Down Expand Up @@ -240,13 +232,10 @@ async def handle_get_stream(self, client: httpx2.AsyncClient, read_stream_writer
logger.info(f"GET stream disconnected, reconnecting in {delay_ms}ms...")
await anyio.sleep(delay_ms / 1000.0)

async def _handle_resumption_request(self, ctx: RequestContext) -> None:
async def _handle_resumption_request(self, ctx: RequestContext, resumption_token: str) -> None:
"""Handle a resumption request using GET with SSE."""
headers = self._prepare_headers()
if ctx.metadata and ctx.metadata.resumption_token:
headers[LAST_EVENT_ID] = ctx.metadata.resumption_token
else:
raise ResumptionError("Resumption request requires a resumption token") # pragma: no cover
headers[LAST_EVENT_ID] = resumption_token

# Extract original request ID to map responses
original_request_id = None
Expand Down Expand Up @@ -556,8 +545,8 @@ async def _handle_message(session_message: SessionMessage) -> None:
else None
)

# Check if this is a resumption request
is_resumption = bool(metadata and metadata.resumption_token)
# A resumption token routes this send to a GET+Last-Event-ID replay
resumption_token = metadata.resumption_token if metadata else None

logger.debug(f"Sending client message: {message}")

Expand All @@ -583,8 +572,8 @@ async def _handle_message(session_message: SessionMessage) -> None:
)

async def handle_request_async():
if is_resumption:
await self._handle_resumption_request(ctx)
if resumption_token:
await self._handle_resumption_request(ctx, resumption_token)
else:
await self._handle_post_request(ctx)

Expand Down Expand Up @@ -635,15 +624,7 @@ async def terminate_session(self, client: httpx2.AsyncClient) -> None:
except Exception as exc: # pragma: no cover
logger.warning(f"Session termination failed: {exc}")

# TODO(Marcelo): Check the TODO below, and cover this with tests if necessary.
def get_session_id(self) -> str | None:
"""Get the current session ID."""
return self.session_id # pragma: no cover


# TODO(Marcelo): I've dropped the `get_session_id` callback because it breaks the Transport protocol. Is that needed?
# It's a completely wrong abstraction, so removal is a good idea. But if we need the client to find the session ID,
# we should think about a better way to do it. I believe we can achieve it with other means.
@asynccontextmanager
async def streamable_http_client(
url: str,
Expand Down
2 changes: 1 addition & 1 deletion src/mcp/server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
from .mcpserver import MCPServer
from .models import InitializationOptions

__all__ = ["CacheHint", "Server", "ServerRequestContext", "MCPServer", "NotificationOptions", "InitializationOptions"]
__all__ = ["CacheHint", "InitializationOptions", "MCPServer", "NotificationOptions", "Server", "ServerRequestContext"]
Loading