diff --git a/README.md b/README.md index b14a8e1209..33f7c9d921 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ The [Model Context Protocol](https://modelcontextprotocol.io) lets you build ser - **Build MCP servers** that expose tools, resources, and prompts to any MCP host - **Build MCP clients** that connect to any MCP server -- Speak every standard transport: stdio, Streamable HTTP, and SSE +- Speak every standard transport: stdio and Streamable HTTP, plus the deprecated legacy HTTP+SSE ## Requirements diff --git a/docs/client/session-groups.md b/docs/client/session-groups.md index 70ac02e859..6ad6a7e4cb 100644 --- a/docs/client/session-groups.md +++ b/docs/client/session-groups.md @@ -24,7 +24,7 @@ Create a `ClientSessionGroup` and call **`connect_to_server`** once per server: --8<-- "docs_src/session_groups/tutorial003.py" ``` -* `connect_to_server` takes transport parameters, not a server object: `StdioServerParameters` (from `mcp`) to launch a subprocess, or `StreamableHttpParameters` / `SseServerParameters` (from `mcp.client.session_group`) for a server already listening on a URL. +* `connect_to_server` takes transport parameters, not a server object: `StdioServerParameters` (from `mcp`) to launch a subprocess, or `StreamableHttpParameters` (from `mcp.client.session_group`) for a server already listening on a URL. `SseServerParameters` is still accepted for a server on the deprecated HTTP+SSE transport, and warns. * `group.tools` is a `dict[str, Tool]` of every connected server's tools. `group.resources` and `group.prompts` are the same shape. * `group.call_tool(name, arguments)` looks the name up, finds the session that owns it, and forwards the call. You never say which server. diff --git a/docs/client/transports.md b/docs/client/transports.md index b453100655..36d9f3b795 100644 --- a/docs/client/transports.md +++ b/docs/client/transports.md @@ -102,7 +102,7 @@ Leaving the `async with` block also shuts the subprocess down: close stdin, wait ## SSE -`sse_client(url)`, from `mcp.client.sse`, is the HTTP transport that Streamable HTTP superseded. Wrap it the same way, `Client(sse_client("http://localhost:8000/sse"))`, to talk to a server that still speaks it, and don't build anything new on it. +`sse_client(url)`, from `mcp.client.sse`, is the HTTP transport that Streamable HTTP superseded, and it is now formally deprecated ([SEP-2596](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2596)): calling it emits `MCPDeprecationWarning` (see **[Deprecated features](../deprecated.md)**). Wrap it the same way, `Client(sse_client("http://localhost:8000/sse"))`, to talk to a server that still speaks it, and don't build anything new on it. ## The `Transport` protocol diff --git a/docs/deprecated.md b/docs/deprecated.md index 9b879f1f6f..0a8e6122f1 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -1,6 +1,6 @@ # Deprecated features -The 2026-07-28 spec retires five things. The SDK still implements every one of them, and every one of them now carries a **deprecation warning**. +The 2026-07-28 spec retires five things, and the spec's deprecation registry carries a sixth from earlier: the legacy HTTP+SSE transport. The SDK still implements every one of them, and every one of them now carries a **deprecation warning**. The table below names each deprecated feature, why it is going away, and the replacement to build on. @@ -13,18 +13,20 @@ The table below names each deprecated feature, why it is going away, and the rep | **Protocol logging**: `ctx.log()`, `ctx.debug()`, `ctx.info()`, `ctx.warning()`, `ctx.error()`, `ctx.session.send_log_message()`, `client.set_logging_level()` | SEP-2577 retires the capability. Nothing in-protocol replaces it. | Ordinary `import logging` to stderr (see **[Logging](handlers/logging.md)**). | | **`ping`**: `client.send_ping()` | **Removed** from the protocol, not merely deprecated. There is no `ping` method in 2026-07-28. | Nothing. It only works against a `mode="legacy"` connection. | | **Client->server progress**: `client.send_progress_notification()` | 2026-07-28 makes progress server->client only. | Nothing to send. Your *server* reports progress with `ctx.report_progress()` (see **[Progress](handlers/progress.md)**). | +| **HTTP+SSE transport**: `sse_client()`, `SseServerParameters`, `mcp.sse_app()`, `mcp.run(transport="sse")`, `SseServerTransport` | Superseded by Streamable HTTP in the 2025-03-26 spec; [SEP-2596](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2596) formally classifies it as deprecated. | Streamable HTTP: `Client(url)` / `streamable_http_client()` on the client, `mcp.run(transport="streamable-http")` / `streamable_http_app()` on the server (see **[Running your server](run/index.md)** and **[Transports](client/transports.md)**). | -Three things fall out of that table: +Four things fall out of that table: * Roots, sampling, and logging go together. One proposal, **SEP-2577**, deprecates all three capabilities at once. * Sampling and roots share a deeper problem: they are places a **server** sends a **request** to the **client**. That whole direction is what 2026-07-28 replaces with **[Multi-round-trip requests](handlers/multi-round-trip.md)**. It is the standalone RPC methods (`sampling/createMessage`, `roots/list`, and push-style `elicitation/create`) that are gone; the `CreateMessageRequest` / `ListRootsRequest` / `ElicitRequest` payload types survive, embedded in `InputRequiredResult.input_requests`, and on the client they hit the same callbacks. * `ping` is the odd one out. The protocol does not deprecate it, it removes it. The SDK method still warns (its message says *removed*, not *deprecated*) and calling it on a modern connection answers with *"Method not found"*. +* The transport is odd in the other direction: it is not part of the 2026-07-28 story at all. Streamable HTTP superseded HTTP+SSE back in 2025-03-26, and it has been the "don't build on it" transport ever since; SEP-2596's feature-lifecycle policy simply put it on the formal register. It is also the only row that is a transport rather than a capability, so it doesn't turn on the negotiated version: the same `streamable_http_app()` serves both eras of client (see **[Serving legacy clients](run/legacy-clients.md)**), and nothing needs SSE. ## Deprecated is advisory Nothing breaks today. -Every method above keeps working against any session that negotiated **2025-11-25 or earlier**. Pin `mode="legacy"` on the client and you get exactly the pre-2026 behaviour. There are no wire changes and capability negotiation is unchanged. +Every method above keeps working against any session that negotiated **2025-11-25 or earlier**. Pin `mode="legacy"` on the client and you get exactly the pre-2026 behaviour. There are no wire changes and capability negotiation is unchanged. The transport row is simpler still: `sse_client()` and `sse_app()` do exactly what they always did, wire and all; they just warn when you call them. What changes is that you get a visible warning the first time each one runs: @@ -82,7 +84,8 @@ That is the whole API. There is no per-method switch, and you don't want one: th ## Recap * The 2026-07-28 spec deprecates **roots**, server-initiated **sampling**, and protocol **logging** (all [SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577)), restricts **progress** to server-to-client, and removes **`ping`**. -* The replacement column points you onward: **[Multi-round-trip requests](handlers/multi-round-trip.md)** for sampling and roots, **[Logging](handlers/logging.md)** for logging, **[Progress](handlers/progress.md)** for progress. `ping` needs nothing at all. +* The legacy **HTTP+SSE transport** is deprecated too ([SEP-2596](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2596)), superseded by Streamable HTTP since 2025-03-26. +* The replacement column points you onward: **[Multi-round-trip requests](handlers/multi-round-trip.md)** for sampling and roots, **[Logging](handlers/logging.md)** for logging, **[Progress](handlers/progress.md)** for progress, Streamable HTTP for the transport. `ping` needs nothing at all. * Deprecated is advisory: no wire changes, everything keeps working against pre-2026 sessions, and you get a visible `MCPDeprecationWarning` (a `UserWarning`, so it is on by default). * Sampling and roots additionally need a back-channel that a 2026-07-28 session does not have. On a modern connection they warn and then they raise. * `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` silences the whole category; `"error::mcp.MCPDeprecationWarning"` in pytest turns it into a test failure. diff --git a/docs/index.md b/docs/index.md index 3d10fc9bca..58a700babb 100644 --- a/docs/index.md +++ b/docs/index.md @@ -11,7 +11,7 @@ This is the official Python SDK for it. With it you can: * **Build MCP servers** that expose tools, resources, and prompts to any MCP host. * **Build MCP clients** that connect to any MCP server. -* Speak every standard transport: stdio, Streamable HTTP, and SSE. +* Speak every standard transport: stdio and Streamable HTTP, plus the deprecated legacy HTTP+SSE. ## Requirements diff --git a/docs/migration.md b/docs/migration.md index 931d470d1a..65824c7155 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -27,6 +27,7 @@ Every section heading below names the API it affects, so searching this page for | Lowlevel return value wrapping removed | bare list or dict returns fail result validation instead of being wrapped | [wrapping removed](#lowlevel-server-automatic-return-value-wrapping-removed) | | Lowlevel tool exceptions no longer become `isError: true` results | clients raise a JSON-RPC error instead of seeing the error text | [tool exceptions](#lowlevel-server-tool-handler-exceptions-no-longer-become-calltoolresultis_errortrue) | | Roots, Sampling, and Logging deprecated (SEP-2577) | `MCPDeprecationWarning` at call sites | [SEP-2577](#roots-sampling-and-logging-methods-deprecated-sep-2577) | +| HTTP+SSE transport deprecated (SEP-2596) | `MCPDeprecationWarning` from `sse_client`, `sse_app`, `run(transport="sse")` | [SEP-2596](#httpsse-transport-deprecated-sep-2596) | ### Find your area @@ -41,7 +42,7 @@ Every section heading below names the API it affects, so searching this page for | maintain OAuth client auth or a protected server | [OAuth and server auth](#oauth-and-server-auth) | | relied on lenient handling of off-schema traffic, or assert on exact wire bytes | [Stricter protocol validation and wire behavior](#stricter-protocol-validation-and-wire-behavior) | | test against in-memory server/client pairs | [Testing utilities](#testing-utilities) | -| use roots, sampling, logging, or client-to-server progress | [Deprecations](#deprecations) | +| use roots, sampling, logging, client-to-server progress, or the HTTP+SSE transport | [Deprecations](#deprecations) | | operate servers that 2026-era clients will also connect to | [Notes for 2026-era connections](#notes-for-2026-era-connections) | ## Suggested migration order @@ -2792,6 +2793,15 @@ The 2026-07-28 spec restricts `notifications/progress` to the server-to-client d On the server side, prefer the new dispatcher-agnostic `ServerSession.report_progress(progress, total, message)` (and `Context.report_progress()` on `MCPServer`) over the raw `ServerSession.send_progress_notification(progress_token, …)`. `report_progress` encapsulates the "no-op when the caller did not request progress" rule and works on every dispatcher; the raw token-taking form remains for handlers that read `_meta.progressToken` directly. +### HTTP+SSE transport deprecated (SEP-2596) + +The legacy HTTP+SSE transport (a `GET` that opens an event stream, plus a separate `POST` message endpoint) was superseded by Streamable HTTP in protocol revision 2025-03-26, and the spec's feature-lifecycle policy ([SEP-2596](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2596)) now formally classifies it as Deprecated with Streamable HTTP as the migration path. The SDK still ships it, and every entry point now carries `typing_extensions.deprecated` and emits `mcp.MCPDeprecationWarning`: + +- Client: `mcp.client.sse.sse_client()`, and `SseServerParameters` for `ClientSessionGroup.connect_to_server()`. +- Server: `MCPServer.sse_app()`, `MCPServer.run_sse_async()`, `mcp.run(transport="sse")`, and the lowlevel `mcp.server.sse.SseServerTransport`. + +Streamable HTTP is not deprecated: `streamable_http_client()` / `Client(url)` on the client, `streamable_http_app()` / `mcp.run(transport="streamable-http")` on the server. Migrate when you can; a deployment that must keep answering unmigrated SSE clients can keep serving `sse_app()` alongside the Streamable HTTP app and filter the warning as [above](#roots-sampling-and-logging-methods-deprecated-sep-2577). Two adjacent housekeeping changes: `mcp run --transport` now advertises `stdio` and `streamable-http` only, and `python -m mcp.client ` connects with Streamable HTTP (it previously used SSE for any `http(s)` URL). + ## Notes for 2026-era connections Everything below this heading describes behavior that only activates on connections diff --git a/docs/run/asgi.md b/docs/run/asgi.md index 2eca9273cd..41288813f1 100644 --- a/docs/run/asgi.md +++ b/docs/run/asgi.md @@ -33,7 +33,7 @@ Run the app on its own (`uvicorn server:app`) and you never think about either. nothing here; **[Deploy & scale](deploy.md)** explains what it actually controls. **[Running your server](index.md)** covers the options themselves. -`mcp.sse_app()` does the same for the superseded SSE transport. +`mcp.sse_app()` does the same for the deprecated HTTP+SSE transport, and warns when you call it (**[Deprecated features](../deprecated.md)**). ## Localhost only, until you say otherwise diff --git a/docs/run/index.md b/docs/run/index.md index dbea20d0fe..a0ba0f66f1 100644 --- a/docs/run/index.md +++ b/docs/run/index.md @@ -10,12 +10,15 @@ The only decision you make is the **transport**: how the bytes between your serv |---|---|---| | `stdio` | The host launches your file as a subprocess and speaks over its stdin and stdout. | Local servers. The default. | | `streamable-http` | A real HTTP server listening on a port. | Anything you deploy. | -| `sse` | The older HTTP transport. | You don't. | +| `sse` | The older HTTP transport. **Deprecated.** | You don't. | !!! warning - SSE was superseded by Streamable HTTP in the 2025-03-26 protocol revision. + HTTP+SSE was superseded by Streamable HTTP in the 2025-03-26 protocol revision and is + now formally deprecated ([SEP-2596](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2596)). `mcp.run(transport="sse")` still works, with its own `sse_path=` and `message_path=` - options, but it exists for clients that haven't moved. Don't build anything new on it. + options, but it warns (`MCPDeprecationWarning`) and it exists only for clients that + haven't moved. Don't build anything new on it; **[Deprecated features](../deprecated.md)** + has the full row. ## `mcp.run()` diff --git a/examples/clients/simple-auth-client/mcp_simple_auth_client/main.py b/examples/clients/simple-auth-client/mcp_simple_auth_client/main.py index a190b89970..3c68fc3cd2 100644 --- a/examples/clients/simple-auth-client/mcp_simple_auth_client/main.py +++ b/examples/clients/simple-auth-client/mcp_simple_auth_client/main.py @@ -21,7 +21,9 @@ from mcp.client._transport import ReadStream, WriteStream from mcp.client.auth import AuthorizationCodeResult, OAuthClientProvider, TokenStorage from mcp.client.session import ClientSession -from mcp.client.sse import sse_client +from mcp.client.sse import ( + sse_client, # pyright: ignore[reportDeprecated] # deprecated HTTP+SSE, kept for legacy servers +) from mcp.client.streamable_http import streamable_http_client from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken from mcp.shared.message import SessionMessage @@ -224,8 +226,8 @@ async def _default_redirect_handler(authorization_url: str) -> None: # Create transport with auth handler based on transport type if self.transport_type == "sse": - print("📡 Opening SSE transport connection with auth...") - async with sse_client( + print("📡 Opening SSE transport connection with auth (deprecated transport)...") + async with sse_client( # pyright: ignore[reportDeprecated] url=self.server_url, auth=oauth_auth, timeout=60.0, diff --git a/examples/servers/simple-auth/mcp_simple_auth/legacy_as_server.py b/examples/servers/simple-auth/mcp_simple_auth/legacy_as_server.py index ab7773b5bb..b321ba1ab8 100644 --- a/examples/servers/simple-auth/mcp_simple_auth/legacy_as_server.py +++ b/examples/servers/simple-auth/mcp_simple_auth/legacy_as_server.py @@ -110,7 +110,7 @@ async def get_time() -> dict[str, Any]: "--transport", default="streamable-http", type=click.Choice(["sse", "streamable-http"]), - help="Transport protocol to use ('sse' or 'streamable-http')", + help="Transport protocol to use ('streamable-http', or the deprecated 'sse')", ) def main(port: int, transport: Literal["sse", "streamable-http"]) -> int: """Run the simple auth MCP server.""" @@ -129,7 +129,8 @@ def main(port: int, transport: Literal["sse", "streamable-http"]) -> int: mcp_server = create_simple_mcp_server(server_settings, auth_settings) logger.info(f"🚀 MCP Legacy Server running on {server_url}") - mcp_server.run(transport=transport, host=host, port=port) + # transport may be the deprecated HTTP+SSE transport, kept here for legacy clients + mcp_server.run(transport=transport, host=host, port=port) # pyright: ignore[reportDeprecated] return 0 diff --git a/examples/servers/simple-auth/mcp_simple_auth/server.py b/examples/servers/simple-auth/mcp_simple_auth/server.py index 0320871b12..e9a1b71205 100644 --- a/examples/servers/simple-auth/mcp_simple_auth/server.py +++ b/examples/servers/simple-auth/mcp_simple_auth/server.py @@ -103,7 +103,7 @@ async def get_time() -> dict[str, Any]: "--transport", default="streamable-http", type=click.Choice(["sse", "streamable-http"]), - help="Transport protocol to use ('sse' or 'streamable-http')", + help="Transport protocol to use ('streamable-http', or the deprecated 'sse')", ) @click.option( "--oauth-strict", @@ -149,7 +149,8 @@ def main(port: int, auth_server: str, transport: Literal["sse", "streamable-http logger.info(f"🔑 Using Authorization Server: {settings.auth_server_url}") # Run the server - this should block and keep running - mcp_server.run(transport=transport, host=host, port=port) + # transport may be the deprecated HTTP+SSE transport, kept here for legacy clients + mcp_server.run(transport=transport, host=host, port=port) # pyright: ignore[reportDeprecated] logger.info("Server stopped") return 0 except Exception: diff --git a/examples/snippets/clients/url_elicitation_client.py b/examples/snippets/clients/url_elicitation_client.py index 14fc08d9f9..403fa81a49 100644 --- a/examples/snippets/clients/url_elicitation_client.py +++ b/examples/snippets/clients/url_elicitation_client.py @@ -31,7 +31,7 @@ import mcp.types as types from mcp import ClientSession from mcp.client.context import ClientRequestContext -from mcp.client.sse import sse_client +from mcp.client.streamable_http import streamable_http_client from mcp.shared.exceptions import MCPError, UrlElicitationRequiredError from mcp.types import URL_ELICITATION_REQUIRED @@ -280,16 +280,16 @@ async def run_command_loop(session: ClientSession) -> None: async def main() -> None: """Run the interactive URL elicitation client.""" - server_url = "http://localhost:8000/sse" + server_url = "http://localhost:8000/mcp" print("=" * 60) print("URL Elicitation Client Example") print("=" * 60) print(f"\nConnecting to: {server_url}") - print("(Start server with: cd examples/snippets && uv run server elicitation sse)") + print("(Start server with: cd examples/snippets && uv run server elicitation streamable-http)") try: - async with sse_client(server_url) as (read, write): + async with streamable_http_client(server_url) as (read, write): async with ClientSession( read, write, diff --git a/pyproject.toml b/pyproject.toml index 3c814106d1..860e087d42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -266,6 +266,9 @@ filterwarnings = [ # 2026-07-28 drops ping; Client.send_ping() is advisory-deprecated and the # legacy interaction/transport tests still drive it. "ignore:ping is removed as of 2026-07-28.*:mcp.MCPDeprecationWarning", + # SEP-2596 deprecates the HTTP+SSE transport; the SDK still ships it for + # servers/clients that haven't migrated, and the tests still exercise it. + "ignore:.*HTTP\\+SSE transport is deprecated as of 2025-03-26.*:mcp.MCPDeprecationWarning", ] [tool.markdown.lint] diff --git a/src/mcp/cli/cli.py b/src/mcp/cli/cli.py index eb06bf087a..1896d9e2d6 100644 --- a/src/mcp/cli/cli.py +++ b/src/mcp/cli/cli.py @@ -311,7 +311,7 @@ def run( typer.Option( "--transport", "-t", - help="Transport protocol to use (stdio or sse)", + help="Transport protocol to use (stdio or streamable-http)", ), ] = None, ) -> None: # pragma: no cover diff --git a/src/mcp/client/__main__.py b/src/mcp/client/__main__.py index 60e3b02390..7065b3b16d 100644 --- a/src/mcp/client/__main__.py +++ b/src/mcp/client/__main__.py @@ -10,8 +10,8 @@ from mcp.client._transport import ReadStream, WriteStream from mcp.client.session import ClientSession, IncomingMessage -from mcp.client.sse import sse_client from mcp.client.stdio import StdioServerParameters, stdio_client +from mcp.client.streamable_http import streamable_http_client from mcp.shared.message import SessionMessage if not sys.warnoptions: @@ -49,8 +49,8 @@ async def main(command_or_url: str, args: list[str], env: list[tuple[str, str]]) env_dict = dict(env) if urlparse(command_or_url).scheme in ("http", "https"): - # Use SSE client for HTTP(S) URLs - async with sse_client(command_or_url) as streams: + # Use Streamable HTTP client for HTTP(S) URLs + async with streamable_http_client(command_or_url) as streams: await run_session(*streams) else: # Use stdio client for commands diff --git a/src/mcp/client/session_group.py b/src/mcp/client/session_group.py index a544cecbe8..3bd2e7c649 100644 --- a/src/mcp/client/session_group.py +++ b/src/mcp/client/session_group.py @@ -8,6 +8,7 @@ import contextlib import logging +import warnings from collections.abc import Callable from dataclasses import dataclass from types import TracebackType @@ -17,20 +18,27 @@ import httpx2 import mcp_types as types from pydantic import BaseModel, Field -from typing_extensions import Self +from typing_extensions import Self, deprecated import mcp from mcp.client.session import ElicitationFnT, ListRootsFnT, LoggingFnT, MessageHandlerFnT, SamplingFnT -from mcp.client.sse import sse_client +from mcp.client.sse import sse_client # pyright: ignore[reportDeprecated] from mcp.client.stdio import StdioServerParameters from mcp.client.streamable_http import streamable_http_client from mcp.shared._httpx_utils import create_mcp_http_client from mcp.shared.dispatcher import ProgressFnT -from mcp.shared.exceptions import MCPError +from mcp.shared.exceptions import MCPDeprecationWarning, MCPError +@deprecated( + "The HTTP+SSE transport is deprecated as of 2025-03-26 (SEP-2596). Use StreamableHttpParameters instead.", + category=MCPDeprecationWarning, +) class SseServerParameters(BaseModel): - """Parameters for initializing an sse_client.""" + """Parameters for initializing an sse_client (the deprecated HTTP+SSE transport). + + Deprecated: use `StreamableHttpParameters`. + """ # The endpoint URL. url: str @@ -64,7 +72,11 @@ class StreamableHttpParameters(BaseModel): terminate_on_close: bool = True -ServerParameters: TypeAlias = StdioServerParameters | SseServerParameters | StreamableHttpParameters +ServerParameters: TypeAlias = ( + StdioServerParameters + | SseServerParameters # pyright: ignore[reportDeprecated] + | StreamableHttpParameters +) # Use dataclass instead of Pydantic BaseModel @@ -313,13 +325,17 @@ async def _establish_session( if isinstance(server_params, StdioServerParameters): client = mcp.stdio_client(server_params) read, write = await session_stack.enter_async_context(client) - elif isinstance(server_params, SseServerParameters): - client = sse_client( - url=server_params.url, - headers=server_params.headers, - timeout=server_params.timeout, - sse_read_timeout=server_params.sse_read_timeout, - ) + elif isinstance(server_params, SseServerParameters): # pyright: ignore[reportDeprecated] + # The caller already saw the deprecation warning when they built + # SseServerParameters; suppress the nested one from sse_client. + with warnings.catch_warnings(): + warnings.simplefilter("ignore", MCPDeprecationWarning) + client = sse_client( # pyright: ignore[reportDeprecated] + url=server_params.url, + headers=server_params.headers, + timeout=server_params.timeout, + sse_read_timeout=server_params.sse_read_timeout, + ) read, write = await session_stack.enter_async_context(client) else: httpx_client = create_mcp_http_client( diff --git a/src/mcp/client/sse.py b/src/mcp/client/sse.py index 31d0f35391..d851ebe60c 100644 --- a/src/mcp/client/sse.py +++ b/src/mcp/client/sse.py @@ -9,10 +9,12 @@ import mcp_types as types from anyio.abc import TaskStatus from httpx2 import SSEError +from typing_extensions import deprecated from mcp.shared._compat import resync_tracer from mcp.shared._context_streams import create_context_streams from mcp.shared._httpx_utils import McpHttpClientFactory, create_mcp_http_client +from mcp.shared.exceptions import MCPDeprecationWarning from mcp.shared.message import SessionMessage logger = logging.getLogger(__name__) @@ -28,6 +30,10 @@ def _extract_session_id_from_endpoint(endpoint_url: str) -> str | None: @asynccontextmanager +@deprecated( + "The HTTP+SSE transport is deprecated as of 2025-03-26 (SEP-2596). Use streamable_http_client instead.", + category=MCPDeprecationWarning, +) async def sse_client( url: str, headers: dict[str, Any] | None = None, @@ -37,7 +43,11 @@ async def sse_client( auth: httpx2.Auth | None = None, on_session_created: Callable[[str], None] | None = None, ): - """Client transport for SSE. + """Client transport for the legacy HTTP+SSE protocol. + + Deprecated: HTTP+SSE was superseded by Streamable HTTP in protocol revision 2025-03-26 and + formally deprecated by SEP-2596. It remains for connecting to servers that still speak it; + use `mcp.client.streamable_http.streamable_http_client` for anything new. `sse_read_timeout` determines how long (in seconds) the client will wait for a new event before disconnecting. All other HTTP operations are controlled by `timeout`. diff --git a/src/mcp/server/mcpserver/server.py b/src/mcp/server/mcpserver/server.py index 7bf1993e29..2504ec58d7 100644 --- a/src/mcp/server/mcpserver/server.py +++ b/src/mcp/server/mcpserver/server.py @@ -4,6 +4,7 @@ import base64 import inspect +import warnings from collections.abc import AsyncIterator, Awaitable, Callable, Iterable, Mapping, Sequence from contextlib import AbstractAsyncContextManager, asynccontextmanager from typing import Any, Generic, Literal, TypeVar, overload @@ -53,6 +54,7 @@ from starlette.responses import Response from starlette.routing import Mount, Route from starlette.types import Receive, Scope, Send +from typing_extensions import deprecated from mcp.server.auth.middleware.auth_context import AuthContextMiddleware from mcp.server.auth.middleware.bearer_auth import BearerAuthBackend, RequireAuthMiddleware @@ -84,13 +86,13 @@ from mcp.server.mcpserver.utilities.context_injection import find_context_parameter from mcp.server.mcpserver.utilities.logging import configure_logging, get_logger from mcp.server.request_state import RequestStateBoundary, RequestStateSecurity -from mcp.server.sse import SseServerTransport +from mcp.server.sse import SseServerTransport # pyright: ignore[reportDeprecated] from mcp.server.stdio import stdio_server from mcp.server.streamable_http import EventStore from mcp.server.streamable_http_manager import DEFAULT_MAX_REQUEST_BODY_SIZE, StreamableHTTPSessionManager from mcp.server.subscriptions import InMemorySubscriptionBus, ListenHandler, SubscriptionBus from mcp.server.transport_security import TransportSecuritySettings -from mcp.shared.exceptions import MCPError +from mcp.shared.exceptions import MCPDeprecationWarning, MCPError from mcp.shared.uri_template import UriTemplate logger = get_logger(__name__) @@ -357,6 +359,11 @@ def _install_extension_interceptor(self) -> None: def run(self, transport: Literal["stdio"] = ...) -> None: ... @overload + @deprecated( + 'transport="sse" is deprecated: the HTTP+SSE transport is deprecated as of 2025-03-26 ' + '(SEP-2596). Use transport="streamable-http" instead.', + category=MCPDeprecationWarning, + ) def run( self, transport: Literal["sse"], @@ -392,7 +399,8 @@ def run( """Run the MCP server. Note this is a synchronous function. Args: - transport: Transport protocol to use ("stdio", "sse", or "streamable-http") + transport: Transport protocol to use ("stdio" or "streamable-http"; "sse" is the + deprecated HTTP+SSE transport and warns). **kwargs: Transport-specific options (see overloads for details) """ TRANSPORTS = Literal["stdio", "sse", "streamable-http"] @@ -403,7 +411,7 @@ def run( case "stdio": anyio.run(self.run_stdio_async) case "sse": # pragma: no cover - anyio.run(lambda: self.run_sse_async(**kwargs)) + anyio.run(lambda: self.run_sse_async(**kwargs)) # pyright: ignore[reportDeprecated] case "streamable-http": # pragma: no cover anyio.run(lambda: self.run_streamable_http_async(**kwargs)) @@ -1024,6 +1032,10 @@ async def run_stdio_async(self) -> None: self._lowlevel_server.create_initialization_options(), ) + @deprecated( + "The HTTP+SSE transport is deprecated as of 2025-03-26 (SEP-2596). Use run_streamable_http_async instead.", + category=MCPDeprecationWarning, + ) async def run_sse_async( # pragma: no cover self, *, @@ -1033,15 +1045,22 @@ async def run_sse_async( # pragma: no cover message_path: str = "/messages/", transport_security: TransportSecuritySettings | None = None, ) -> None: - """Run the server using SSE transport.""" + """Run the server using the deprecated HTTP+SSE transport. + + Deprecated: use `run_streamable_http_async()`. + """ import uvicorn - starlette_app = self.sse_app( - sse_path=sse_path, - message_path=message_path, - transport_security=transport_security, - host=host, - ) + # This method already emitted its own deprecation warning; suppress the + # nested one from sse_app so the caller sees exactly one. + with warnings.catch_warnings(): + warnings.simplefilter("ignore", MCPDeprecationWarning) + starlette_app = self.sse_app( # pyright: ignore[reportDeprecated] + sse_path=sse_path, + message_path=message_path, + transport_security=transport_security, + host=host, + ) config = uvicorn.Config( starlette_app, @@ -1088,6 +1107,10 @@ async def run_streamable_http_async( # pragma: no cover server = uvicorn.Server(config) await server.serve() + @deprecated( + "The HTTP+SSE transport is deprecated as of 2025-03-26 (SEP-2596). Use streamable_http_app instead.", + category=MCPDeprecationWarning, + ) def sse_app( self, *, @@ -1096,7 +1119,10 @@ def sse_app( transport_security: TransportSecuritySettings | None = None, host: str = "127.0.0.1", ) -> Starlette: - """Return an instance of the SSE server app.""" + """Return an instance of the deprecated HTTP+SSE server app. + + Deprecated: use `streamable_http_app()`. + """ # Auto-enable DNS rebinding protection for localhost (IPv4 and IPv6) if transport_security is None and host in ("127.0.0.1", "localhost", "::1"): transport_security = TransportSecuritySettings( @@ -1105,7 +1131,13 @@ def sse_app( allowed_origins=["http://127.0.0.1:*", "http://localhost:*", "http://[::1]:*"], ) - sse = SseServerTransport(message_path, security_settings=transport_security) + # This method already emitted its own deprecation warning; suppress the + # nested one from SseServerTransport so the caller sees exactly one. + with warnings.catch_warnings(): + warnings.simplefilter("ignore", MCPDeprecationWarning) + sse = SseServerTransport( # pyright: ignore[reportDeprecated] + message_path, security_settings=transport_security + ) async def handle_sse(scope: Scope, receive: Receive, send: Send): # pragma: no cover # Add client ID from auth context into request context if available diff --git a/src/mcp/server/sse.py b/src/mcp/server/sse.py index 4d02fc4a73..8db74a2faf 100644 --- a/src/mcp/server/sse.py +++ b/src/mcp/server/sse.py @@ -1,6 +1,11 @@ """SSE Server Transport Module -This module implements a Server-Sent Events (SSE) transport layer for MCP servers. +This module implements the legacy HTTP+SSE transport layer for MCP servers. + +Deprecated: HTTP+SSE was superseded by Streamable HTTP in protocol revision 2025-03-26 and +formally deprecated by SEP-2596. Serve `MCPServer.streamable_http_app()` (or drive +`StreamableHTTPSessionManager` directly) instead; this module remains for servers that must +keep answering clients that have not migrated. Example: ```python @@ -49,6 +54,7 @@ async def handle_sse(request): from starlette.requests import Request from starlette.responses import Response from starlette.types import Receive, Scope, Send +from typing_extensions import deprecated from mcp.server.auth.middleware.bearer_auth import AuthenticatedUser, AuthorizationContext, authorization_context from mcp.server.transport_security import ( @@ -56,14 +62,25 @@ async def handle_sse(request): TransportSecuritySettings, ) from mcp.shared._context_streams import ContextSendStream, create_context_streams +from mcp.shared.exceptions import MCPDeprecationWarning from mcp.shared.message import ServerMessageMetadata, SessionMessage logger = logging.getLogger(__name__) +@deprecated( + "The HTTP+SSE transport is deprecated as of 2025-03-26 (SEP-2596). Use Streamable HTTP instead.", + category=MCPDeprecationWarning, +) class SseServerTransport: - """SSE server transport for MCP. This class provides two ASGI applications, - suitable for use with a framework like Starlette and a server like Hypercorn: + """Server transport for the legacy HTTP+SSE protocol. + + Deprecated: HTTP+SSE was superseded by Streamable HTTP in protocol revision 2025-03-26 and + formally deprecated by SEP-2596. Use `MCPServer.streamable_http_app()` or + `StreamableHTTPSessionManager` instead. + + This class provides two ASGI applications, suitable for use with a framework like + Starlette and a server like Hypercorn: 1. connect_sse() is an ASGI application which receives incoming GET requests, and sets up a new SSE stream to send server messages to the client. diff --git a/tests/client/test_session_group.py b/tests/client/test_session_group.py index b75d22b7a0..45a0705895 100644 --- a/tests/client/test_session_group.py +++ b/tests/client/test_session_group.py @@ -4,16 +4,17 @@ import httpx2 import mcp_types as types import pytest +from inline_snapshot import snapshot import mcp from mcp.client.session_group import ( ClientSessionGroup, ClientSessionParameters, - SseServerParameters, + SseServerParameters, # pyright: ignore[reportDeprecated] StreamableHttpParameters, ) from mcp.client.stdio import StdioServerParameters -from mcp.shared.exceptions import MCPError +from mcp.shared.exceptions import MCPDeprecationWarning, MCPError @pytest.fixture @@ -306,7 +307,7 @@ async def test_client_session_group_disconnect_non_existent_server(): "mcp.client.session_group.mcp.stdio_client", ), ( - SseServerParameters(url="http://test.com/sse", timeout=10.0), + SseServerParameters(url="http://test.com/sse", timeout=10.0), # pyright: ignore[reportDeprecated] "sse", "mcp.client.session_group.sse_client", ), # url, headers, timeout, sse_read_timeout @@ -318,7 +319,7 @@ async def test_client_session_group_disconnect_non_existent_server(): ], ) async def test_client_session_group_establish_session_parameterized( - server_params_instance: StdioServerParameters | SseServerParameters | StreamableHttpParameters, + server_params_instance: StdioServerParameters | SseServerParameters | StreamableHttpParameters, # pyright: ignore[reportDeprecated] client_type_name: str, # Just for clarity or conditional logic if needed patch_target_for_client_func: str, ): @@ -366,7 +367,7 @@ async def test_client_session_group_establish_session_parameterized( assert isinstance(server_params_instance, StdioServerParameters) mock_specific_client_func.assert_called_once_with(server_params_instance) elif client_type_name == "sse": - assert isinstance(server_params_instance, SseServerParameters) + assert isinstance(server_params_instance, SseServerParameters) # pyright: ignore[reportDeprecated] mock_specific_client_func.assert_called_once_with( url=server_params_instance.url, headers=server_params_instance.headers, @@ -402,3 +403,14 @@ async def test_client_session_group_establish_session_parameterized( # 3. Assert returned values assert returned_server_info is mock_initialize_result.server_info assert returned_session is mock_entered_session + + +def test_sse_server_parameters_warn_that_the_http_sse_transport_is_deprecated(): + """SDK-defined (SEP-2596): choosing the legacy HTTP+SSE transport in a session group warns as + soon as its parameters object is built.""" + with pytest.warns(MCPDeprecationWarning, match=r"SEP-2596") as records: + SseServerParameters(url="http://test.com/sse") # pyright: ignore[reportDeprecated] + + assert [str(w.message) for w in records if issubclass(w.category, MCPDeprecationWarning)] == snapshot( + ["The HTTP+SSE transport is deprecated as of 2025-03-26 (SEP-2596). Use StreamableHttpParameters instead."] + ) diff --git a/tests/client/test_transport_stream_cleanup.py b/tests/client/test_transport_stream_cleanup.py index c3d012bb23..317d10c238 100644 --- a/tests/client/test_transport_stream_cleanup.py +++ b/tests/client/test_transport_stream_cleanup.py @@ -19,7 +19,7 @@ import httpx2 import pytest -from mcp.client.sse import sse_client +from mcp.client.sse import sse_client # pyright: ignore[reportDeprecated] from mcp.client.streamable_http import streamable_http_client @@ -65,7 +65,7 @@ async def test_sse_client_closes_all_streams_on_connection_error(free_tcp_port: """ with _assert_no_memory_stream_leak(): with pytest.raises(httpx2.ConnectError): - async with sse_client(f"http://127.0.0.1:{free_tcp_port}/sse"): + async with sse_client(f"http://127.0.0.1:{free_tcp_port}/sse"): # pyright: ignore[reportDeprecated] pytest.fail("should not reach here") # pragma: no cover @@ -88,7 +88,7 @@ def mock_factory( with _assert_no_memory_stream_leak(): with pytest.raises(httpx2.HTTPStatusError): - async with sse_client("http://test/sse", httpx_client_factory=mock_factory): + async with sse_client("http://test/sse", httpx_client_factory=mock_factory): # pyright: ignore[reportDeprecated] pytest.fail("should not reach here") # pragma: no cover diff --git a/tests/interaction/_connect.py b/tests/interaction/_connect.py index 6a8094e2d6..b6e33ff0b7 100644 --- a/tests/interaction/_connect.py +++ b/tests/interaction/_connect.py @@ -33,13 +33,13 @@ from mcp.client.client import Client from mcp.client.extension import ClientExtension from mcp.client.session import ElicitationFnT, ListRootsFnT, LoggingFnT, MessageHandlerFnT, SamplingFnT -from mcp.client.sse import sse_client +from mcp.client.sse import sse_client # pyright: ignore[reportDeprecated] from mcp.client.streamable_http import streamable_http_client from mcp.server import Server from mcp.server.auth.provider import OAuthAuthorizationServerProvider, TokenVerifier from mcp.server.auth.settings import AuthSettings from mcp.server.mcpserver import MCPServer -from mcp.server.sse import SseServerTransport +from mcp.server.sse import SseServerTransport # pyright: ignore[reportDeprecated] from mcp.server.streamable_http import EventStore from mcp.server.streamable_http_manager import StreamableHTTPSessionManager from mcp.server.transport_security import TransportSecuritySettings @@ -334,14 +334,14 @@ async def initialize_via_http(http: httpx2.AsyncClient) -> str: return session_id -def build_sse_app(server: Server | MCPServer) -> tuple[Starlette, SseServerTransport]: +def build_sse_app(server: Server | MCPServer) -> tuple[Starlette, SseServerTransport]: # pyright: ignore[reportDeprecated] """Mount a server on a Starlette app exposing the legacy SSE transport at /sse and /messages/. `MCPServer.sse_app()` exists but does not expose the underlying `SseServerTransport`, which the SSE-specific tests need; building the app explicitly here gives both server flavours the same routing while keeping that handle. """ - sse = SseServerTransport( + sse = SseServerTransport( # pyright: ignore[reportDeprecated] "/messages/", security_settings=TransportSecuritySettings(enable_dns_rebinding_protection=False) ) lowlevel = server._lowlevel_server if isinstance(server, MCPServer) else server @@ -394,7 +394,7 @@ def httpx_client_factory( auth=auth, ) - transport = sse_client(f"{BASE_URL}/sse", httpx_client_factory=httpx_client_factory) + transport = sse_client(f"{BASE_URL}/sse", httpx_client_factory=httpx_client_factory) # pyright: ignore[reportDeprecated] async with Client( transport, # SSE is a legacy-only transport; the modern path has no SSE story. diff --git a/tests/interaction/transports/test_sse.py b/tests/interaction/transports/test_sse.py index ffa5c5728e..28d3567250 100644 --- a/tests/interaction/transports/test_sse.py +++ b/tests/interaction/transports/test_sse.py @@ -6,6 +6,8 @@ rejects requests it cannot route to a session. Every test drives the server's real Starlette app through the suite's streaming ASGI bridge. """ +# The HTTP+SSE transport is deprecated (SEP-2596); these tests keep it working while it ships. +# pyright: reportDeprecated=false from uuid import UUID, uuid4 diff --git a/tests/server/mcpserver/test_server.py b/tests/server/mcpserver/test_server.py index 48e900dcab..e3bd6dc712 100644 --- a/tests/server/mcpserver/test_server.py +++ b/tests/server/mcpserver/test_server.py @@ -61,7 +61,7 @@ ToolsListChanged, ) from mcp.server.transport_security import TransportSecuritySettings -from mcp.shared.exceptions import MCPError +from mcp.shared.exceptions import MCPDeprecationWarning, MCPError from mcp.shared.uri_template import InvalidUriTemplate pytestmark = pytest.mark.anyio @@ -100,7 +100,9 @@ async def test_sse_app_returns_starlette_app(self): """Test that sse_app returns a Starlette application with correct routes.""" mcp = MCPServer("test") # Use host="0.0.0.0" to avoid auto DNS protection - app = mcp.sse_app(host="0.0.0.0") + app = mcp.sse_app( # pyright: ignore[reportDeprecated] + host="0.0.0.0" + ) assert isinstance(app, Starlette) @@ -186,7 +188,9 @@ def test_auto_enabled_for_127_0_0_1_sse(self): # Call sse_app with host=127.0.0.1 to trigger auto-config # We can't directly inspect the transport_security, but we can verify # the app is created without error - app = mcp.sse_app(host="127.0.0.1") + app = mcp.sse_app( # pyright: ignore[reportDeprecated] + host="127.0.0.1" + ) assert app is not None def test_auto_enabled_for_127_0_0_1_streamable_http(self): @@ -198,19 +202,25 @@ def test_auto_enabled_for_127_0_0_1_streamable_http(self): def test_auto_enabled_for_localhost_sse(self): """DNS rebinding protection should auto-enable for host=localhost in SSE app.""" mcp = MCPServer() - app = mcp.sse_app(host="localhost") + app = mcp.sse_app( # pyright: ignore[reportDeprecated] + host="localhost" + ) assert app is not None def test_auto_enabled_for_ipv6_localhost_sse(self): """DNS rebinding protection should auto-enable for host=::1 (IPv6 localhost) in SSE app.""" mcp = MCPServer() - app = mcp.sse_app(host="::1") + app = mcp.sse_app( # pyright: ignore[reportDeprecated] + host="::1" + ) assert app is not None def test_not_auto_enabled_for_other_hosts_sse(self): """DNS rebinding protection should NOT auto-enable for other hosts in SSE app.""" mcp = MCPServer() - app = mcp.sse_app(host="0.0.0.0") + app = mcp.sse_app( # pyright: ignore[reportDeprecated] + host="0.0.0.0" + ) assert app is not None def test_explicit_settings_not_overridden_sse(self): @@ -220,7 +230,9 @@ def test_explicit_settings_not_overridden_sse(self): ) mcp = MCPServer() # Explicit transport_security passed to sse_app should be used as-is - app = mcp.sse_app(host="127.0.0.1", transport_security=custom_settings) + app = mcp.sse_app( # pyright: ignore[reportDeprecated] + host="127.0.0.1", transport_security=custom_settings + ) assert app is not None def test_explicit_settings_not_overridden_streamable_http(self): @@ -2396,3 +2408,14 @@ async def refuse_listen(ctx: ServerRequestContext[Any, Any], call_next: Any) -> pass # pragma: no cover - the refusal precedes the stream assert exc_info.value.error.code == INVALID_REQUEST assert exc_info.value.error.message == "not permitted to watch the requested resources" + + +def test_sse_app_warns_once_that_the_http_sse_transport_is_deprecated(): + """SDK-defined (SEP-2596): building the legacy HTTP+SSE app warns exactly once, naming the + method the caller used; the nested transport's own warning is suppressed.""" + with pytest.warns(MCPDeprecationWarning, match=r"SEP-2596") as records: + MCPServer("test").sse_app(host="0.0.0.0") # pyright: ignore[reportDeprecated] + + assert [str(w.message) for w in records if issubclass(w.category, MCPDeprecationWarning)] == snapshot( + ["The HTTP+SSE transport is deprecated as of 2025-03-26 (SEP-2596). Use streamable_http_app instead."] + ) diff --git a/tests/server/test_sse_security.py b/tests/server/test_sse_security.py index 824bd16aba..558bc5089a 100644 --- a/tests/server/test_sse_security.py +++ b/tests/server/test_sse_security.py @@ -1,4 +1,6 @@ """Tests for SSE server request validation.""" +# The HTTP+SSE transport is deprecated (SEP-2596); these tests keep it working while it ships. +# pyright: reportDeprecated=false import logging import re diff --git a/tests/shared/test_sse.py b/tests/shared/test_sse.py index c27dd69db3..c805f90665 100644 --- a/tests/shared/test_sse.py +++ b/tests/shared/test_sse.py @@ -1,4 +1,6 @@ """Tests for the SSE client and server transports, driven entirely in process.""" +# The HTTP+SSE transport is deprecated (SEP-2596); these tests keep it working while it ships. +# pyright: reportDeprecated=false import json from collections.abc import AsyncGenerator @@ -40,7 +42,7 @@ from mcp.server.sse import SseServerTransport from mcp.server.transport_security import TransportSecuritySettings from mcp.shared._httpx_utils import McpHttpClientFactory -from mcp.shared.exceptions import MCPError +from mcp.shared.exceptions import MCPDeprecationWarning, MCPError from tests.interaction.transports import StreamingASGITransport SERVER_NAME = "test_server_for_SSE" @@ -480,3 +482,24 @@ async def test_sse_session_cleanup_on_disconnect() -> None: headers={"Content-Type": "application/json"}, ) assert response.status_code == 404 + + +def test_sse_client_warns_that_the_http_sse_transport_is_deprecated() -> None: + """SDK-defined (SEP-2596): constructing the legacy HTTP+SSE client transport warns, before + any connection is attempted.""" + with pytest.warns(MCPDeprecationWarning, match=r"SEP-2596") as records: + sse_client("http://localhost/sse") + + assert [str(w.message) for w in records if issubclass(w.category, MCPDeprecationWarning)] == snapshot( + ["The HTTP+SSE transport is deprecated as of 2025-03-26 (SEP-2596). Use streamable_http_client instead."] + ) + + +def test_sse_server_transport_warns_that_the_http_sse_transport_is_deprecated() -> None: + """SDK-defined (SEP-2596): constructing the legacy HTTP+SSE server transport warns.""" + with pytest.warns(MCPDeprecationWarning, match=r"SEP-2596") as records: + SseServerTransport("/messages/") + + assert [str(w.message) for w in records if issubclass(w.category, MCPDeprecationWarning)] == snapshot( + ["The HTTP+SSE transport is deprecated as of 2025-03-26 (SEP-2596). Use Streamable HTTP instead."] + )