Skip to content

Deprecate the legacy HTTP+SSE transport - #3204

Open
maxisbey wants to merge 1 commit into
mainfrom
sse-deprecate
Open

Deprecate the legacy HTTP+SSE transport#3204
maxisbey wants to merge 1 commit into
mainfrom
sse-deprecate

Conversation

@maxisbey

Copy link
Copy Markdown
Contributor

Marks every public entry point of the legacy HTTP+SSE transport as deprecated. Streamable HTTP superseded it in the 2025-03-26 spec revision, and it now sits in the spec's deprecated-features registry under SEP-2596. Streamable HTTP (and its request-scoped SSE streaming) is untouched.

Motivation and Context

The SDK still ships HTTP+SSE for clients and servers that haven't migrated, but nothing told users at the call site or in the type checker that they were building on a deprecated transport. This uses the same mechanism as the SEP-2577 deprecations: typing_extensions.deprecated + MCPDeprecationWarning, so pyright/IDEs flag the call and a visible warning fires at runtime.

Deprecated:

  • mcp.client.sse.sse_client()
  • mcp.client.session_group.SseServerParameters
  • mcp.server.sse.SseServerTransport
  • MCPServer.sse_app(), MCPServer.run_sse_async(), and the run(transport="sse") overload

Nested constructions (sse_app()SseServerTransport, run_sse_async()sse_app(), connect_to_server()sse_client()) suppress the inner warning so the caller sees exactly one, naming the thing they called.

Adjacent housekeeping: mcp run --transport help now advertises stdio or streamable-http, python -m mcp.client <url> connects with Streamable HTTP (it used SSE for any http(s) URL, unlike Client(str)), and the url_elicitation_client snippet moves to Streamable HTTP.

How Has This Been Tested?

Three new tests assert the warning fires (client transport, server transport, and that sse_app() warns exactly once). A scoped filterwarnings ignore keeps the existing SSE suites running under error. Full suite green locally.

Breaking Changes

None. Everything keeps working; it warns. Documented in docs/migration.md (### HTTP+SSE transport deprecated (SEP-2596)) and docs/deprecated.md.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

Message text lines up with the spec registry row (deprecated in 2025-03-26, migration path Streamable HTTP): "The HTTP+SSE transport is deprecated as of 2025-03-26 (SEP-2596). Use <replacement> instead."

Other SDKs for reference: TS marks SSEClientTransport / SSEServerTransport @deprecated; C# gates EnableLegacySse behind an [Obsolete] diagnostic; Java 2.0 deprecates its SSE transports.

AI Disclaimer

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

📚 Documentation preview

Preview https://pr-3204.mcp-python-docs.pages.dev
Deployment https://2bde8861.mcp-python-docs.pages.dev
Commit f0e6057
Triggered by @maxisbey
Updated 2026-07-28 15:21:52 UTC

@maxisbey
maxisbey marked this pull request as ready for review July 28, 2026 15:18
The HTTP+SSE transport was superseded by Streamable HTTP in the 2025-03-26
spec revision and is formally deprecated under SEP-2596. Mark every public
entry point with typing_extensions.deprecated so type checkers flag it and a
runtime MCPDeprecationWarning fires: sse_client, SseServerParameters,
SseServerTransport, MCPServer.sse_app / run_sse_async, and the
run(transport="sse") overload. Nested constructions suppress the inner
warning so callers see exactly one.

Also: mcp run --transport help text drops sse, python -m mcp.client
connects with Streamable HTTP, and the url_elicitation client snippet
moves to Streamable HTTP. Docs and migration guide updated.

No-Verification-Needed: user requested immediate push, verification explicitly waived

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

3 issues found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/mcp/client/session_group.py">

<violation number="1" location="src/mcp/client/session_group.py:331">
P2: Session-group SSE connections still emit both deprecation warnings: one constructing `SseServerParameters` and one entering `sse_client`. Keep the targeted warning suppression active through `enter_async_context(client)` (or make `sse_client` warn when its context manager is constructed).</violation>
</file>

<file name="examples/snippets/clients/url_elicitation_client.py">

<violation number="1" location="examples/snippets/clients/url_elicitation_client.py:289">
P3: When the initial Streamable HTTP connection is refused, the recovery message still tells users to start the server with `sse`. That starts the legacy transport instead of the `/mcp` endpoint this client now targets, so following the error guidance leaves the retry unable to connect. Updating the fallback (and the module usage text) to `streamable-http` would keep the example runnable.</violation>
</file>

<file name="pyproject.toml">

<violation number="1" location="pyproject.toml:271">
P2: The suite-wide ignore masks this SDK's new deprecation warning at every call site, so future tests can accidentally introduce legacy HTTP+SSE usage without failing. The existing legacy-transport tests can keep a module- or test-level `filterwarnings` marker instead, which preserves the warning-as-error guard for all other code paths.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment on lines +331 to 339
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Session-group SSE connections still emit both deprecation warnings: one constructing SseServerParameters and one entering sse_client. Keep the targeted warning suppression active through enter_async_context(client) (or make sse_client warn when its context manager is constructed).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/mcp/client/session_group.py, line 331:

<comment>Session-group SSE connections still emit both deprecation warnings: one constructing `SseServerParameters` and one entering `sse_client`. Keep the targeted warning suppression active through `enter_async_context(client)` (or make `sse_client` warn when its context manager is constructed).</comment>

<file context>
@@ -313,13 +325,17 @@ async def _establish_session(
+            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]
</file context>
Suggested change
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)
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)

Comment thread pyproject.toml
"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",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: The suite-wide ignore masks this SDK's new deprecation warning at every call site, so future tests can accidentally introduce legacy HTTP+SSE usage without failing. The existing legacy-transport tests can keep a module- or test-level filterwarnings marker instead, which preserves the warning-as-error guard for all other code paths.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At pyproject.toml, line 271:

<comment>The suite-wide ignore masks this SDK's new deprecation warning at every call site, so future tests can accidentally introduce legacy HTTP+SSE usage without failing. The existing legacy-transport tests can keep a module- or test-level `filterwarnings` marker instead, which preserves the warning-as-error guard for all other code paths.</comment>

<file context>
@@ -266,6 +266,9 @@ filterwarnings = [
     "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",
 ]
 
</file context>

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)")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: When the initial Streamable HTTP connection is refused, the recovery message still tells users to start the server with sse. That starts the legacy transport instead of the /mcp endpoint this client now targets, so following the error guidance leaves the retry unable to connect. Updating the fallback (and the module usage text) to streamable-http would keep the example runnable.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/snippets/clients/url_elicitation_client.py, line 289:

<comment>When the initial Streamable HTTP connection is refused, the recovery message still tells users to start the server with `sse`. That starts the legacy transport instead of the `/mcp` endpoint this client now targets, so following the error guidance leaves the retry unable to connect. Updating the fallback (and the module usage text) to `streamable-http` would keep the example runnable.</comment>

<file context>
@@ -280,16 +280,16 @@ async def run_command_loop(session: ClientSession) -> None:
     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:
</file context>

@claude claude Bot left a comment

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.

Additional findings (outside current diff — PR may have been updated during review):

  • 🟡 src/mcp/server/mcpserver/server.py:396-399 — The @deprecated on the run(transport="sse") overload is typing-only (overload stubs never execute), so at runtime a mcp.run(transport="sse") caller instead gets run_sse_async's warning — "Use run_streamable_http_async instead" — naming an internal async method they never called, fired from inside the anyio.run lambda rather than their call site. Consider emitting the overload's message explicitly in the case "sse" branch and suppressing the nested run_sse_async warning, matching the pattern used elsewhere in this PR.

    Extended reasoning...

    This PR's stated design (PR description and the suppression comments in server.py) is that nested constructions suppress inner warnings "so the caller sees exactly one, naming the thing they called." That pattern is correctly implemented for sse_app()SseServerTransport, run_sse_async()sse_app(), and _establish_session()sse_client() — but not for the documented entry point mcp.run(transport="sse").

    The reason is that typing_extensions.deprecated applied to an @overload stub is a typing-only construct: overload stubs are rebound by the implementation and never execute at runtime, so the overload's message ('Use transport="streamable-http" instead') is only ever delivered by pyright/IDEs, never as a runtime warning. Verifiers confirmed this empirically with a minimal reproduction of the @deprecated + @overload pattern.

    Step-by-step proof. A user writes mcp.run(transport="sse"):

    1. The run() implementation at src/mcp/server/mcpserver/server.py:383 executes; the @deprecated overload at lines 346–357 is never invoked, so no warning fires here.
    2. The case "sse" branch (line 398–399) calls anyio.run(lambda: self.run_sse_async(**kwargs)) with only a # pyright: ignore[reportDeprecated] comment — unlike every other nested call site in this PR, it neither emits its own warning nor suppresses the inner one.
    3. Inside the event loop, run_sse_async's @deprecated (lines 1020–1023) fires the only runtime MCPDeprecationWarning: "The HTTP+SSE transport is deprecated as of 2025-03-26 (SEP-2596). Use run_streamable_http_async instead."
    4. The user is told to migrate to run_streamable_http_async — an internal async method they never called — while the overload's typing-only message and docs/migration.md both advertise transport="streamable-http" as the migration path. The warning also originates inside the anyio.run lambda, so its stacklevel points at SDK internals rather than the user's run() call.

    Nothing existing prevents this: the filterwarnings ignore in pyproject.toml keeps the test suite green, and the case "sse" branch carries # pragma: no cover, so no test observes which message fires on this path.

    Impact is limited: exactly one warning still fires (run_sse_async suppresses sse_app's), the category is correct, and the guidance is directionally right (Streamable HTTP, SEP-2596). Only the replacement pointer and the warning's attribution are off relative to the PR's own one-warning-naming-the-caller design.

    Fix: in the case "sse" branch of run(), emit the overload's message explicitly with warnings.warn(..., category=MCPDeprecationWarning, stacklevel=2) and wrap the run_sse_async call in warnings.catch_warnings() + simplefilter("ignore", MCPDeprecationWarning) — exactly the pattern run_sse_async and sse_app already use for their own nested calls.

Comment thread src/mcp/client/sse.py
Comment on lines 32 to 37
@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(

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 decorator order on sse_client (@asynccontextmanager outermost, @deprecated inner) causes the runtime MCPDeprecationWarning to be attributed to CPython's contextlib.py instead of the user's call site, and the default warning filter then dedupes all call sites process-wide to a single warning. Swapping the two decorators (@deprecated outermost) restores correct call-site attribution; sse_client is the only @asynccontextmanager function in this PR carrying @deprecated — the other deprecated entry points attribute correctly.

Extended reasoning...

What happens

Decorators apply bottom-up, so in src/mcp/client/sse.py the @deprecated wrapper wraps the raw async-generator function, and @asynccontextmanager wraps that. When a user calls sse_client(url), the call chain is: user frame → asynccontextmanager helper → _AsyncGeneratorContextManager.__init__ (self.gen = func(*args, **kwds)) → the deprecated wrapper. typing_extensions.deprecated emits warnings.warn(msg, stacklevel=2), and two frames up from the wrapper is _AsyncGeneratorContextManager.__init__ — so the warning is attributed to CPython's contextlib.py, not the caller's file and line.

Step-by-step proof (reproduced independently by three verifiers on Python 3.11):

  1. Define sse_client exactly as in this PR (@asynccontextmanager over @deprecated(...)).
  2. Call sse_client("http://x/sse") from a user module and capture the warning with warnings.catch_warnings(record=True).
  3. Inspect the record: filename == "/usr/lib/python3.11/contextlib.py", lineno == 105. The user sees .../contextlib.py:105: MCPDeprecationWarning: The HTTP+SSE transport is deprecated... with no pointer to their own code.
  4. Swap the decorator order (@deprecated outermost) and repeat: the record now points at the user's call line, and the returned object still works as an async context manager (async with sse_client(url) as streams verified to behave identically).

Downstream consequences

  • Python's default "default" filter action dedupes per (message, category, module, lineno). Since every sse_client call in a process shares the contextlib.py:105 location, an application with several distinct call sites sees the warning once total, not once per call site — verified: two calls from different lines emitted one warning.
  • module=-scoped filters (warnings.filterwarnings(..., module="myapp.legacy")) cannot target this warning, because its module resolves to contextlib, not the caller's module.
  • This partially undercuts the PR's stated motivation ("nothing told users at the call site … a visible warning fires at runtime"): the warning fires, but it doesn't tell users where in their code the deprecated transport is used.

Why the PR's tests don't catch it

test_sse_client_warns_that_the_http_sse_transport_is_deprecated in tests/shared/test_sse.py uses pytest.warns and asserts only category and message text — never the attributed filename/lineno.

Why this is unique to sse_client

Every other entry point deprecated by this PR (SseServerParameters, SseServerTransport, sse_app, run_sse_async, the run(transport="sse") overload) is a plain class or method whose deprecated wrapper is invoked directly from the user's frame, so those warnings attribute correctly.

Fix

Swap the decorator order so @deprecated is outermost:

@deprecated(
    "The HTTP+SSE transport is deprecated as of 2025-03-26 (SEP-2596). Use streamable_http_client instead.",
    category=MCPDeprecationWarning,
)
@asynccontextmanager
async def sse_client(...):

The warning still fires eagerly at sse_client(url) construction (before any connection), the returned context manager is unchanged, and both pyright's reportDeprecated flagging and the existing pytest.warns tests are unaffected. (Alternatively, stacklevel=3 would work, but typing_extensions.deprecated doesn't expose that knob — the swap is the clean one-line fix.)

Severity

Nit: the warning does fire visibly with the correct category and its message names the transport, so nothing breaks — only the file/line attribution, per-call-site dedup, and module-scoped filtering are degraded relative to the PR's call-site-visibility goal. One-line fix, not worth blocking merge.

Comment on lines +283 to 295
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,

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 migration of this client to Streamable HTTP left two stale instructions that still say to start the server in SSE mode: the module docstring (uv run server elicitation sse, ~line 20) and the ConnectionRefusedError handler (line 305). Following either starts the server on the legacy /sse endpoint while this client now connects to /mcp, so the connection fails — and the failure message then repeats the wrong command. Both should say streamable-http to match the updated hint in main().

Extended reasoning...

What the bug is. This PR migrates examples/snippets/clients/url_elicitation_client.py from sse_client to streamable_http_client: the import changes, server_url becomes http://localhost:8000/mcp, and the startup hint printed in main() (line 289) is updated to uv run server elicitation streamable-http. But two other references to the old command in the same file were missed:

  1. The module docstring (~line 20): uv run server elicitation sse
  2. The ConnectionRefusedError handler (line 305): print(" cd examples/snippets && uv run server elicitation sse")

The code path that triggers it. The snippets server runner (examples/snippets/servers/__init__.py) dispatches its transport argument to mcp.run(transport), so uv run server elicitation sse really does start the server on the legacy HTTP+SSE transport, serving /sse and /messages/ — there is no /mcp endpoint in that mode.

Step-by-step proof.

  1. A user reads the module docstring and runs cd examples/snippets && uv run server elicitation sse. The server starts, listening for legacy SSE clients at http://localhost:8000/sse.
  2. They run the client. It calls streamable_http_client("http://localhost:8000/mcp") (lines 283, 292) and POSTs to /mcp.
  3. The SSE-mode Starlette app has no /mcp route, so the request fails (404 → connection error surfaced to the user).
  4. The client's error path prints Make sure the elicitation server is running: cd examples/snippets && uv run server elicitation sse — the exact command the user already ran. The recovery instruction loops them back into the failure.

Beyond the connection failure, following the stale instruction also puts the user on the transport this very PR is deprecating: the server run emits MCPDeprecationWarning, the opposite of what the snippet is meant to demonstrate post-migration.

Why nothing catches it. These are string literals in comments and print statements — no type checker, test, or the new deprecation warnings can flag them. The example only fails when run interactively against a real server, which CI doesn't do.

Impact and fix. Impact is confined to this example: the primary hint in main() (line 289) is correct, so a user who starts from the printed banner is fine; only users who follow the docstring or the error-path message get misdirected. The fix is a single root cause with two one-line string changes — replace sse with streamable-http in the docstring and in the ConnectionRefusedError handler, matching line 289 exactly (the runner accepts streamable-http as a transport argument, verified against examples/snippets/servers/__init__.py).

@claude claude Bot left a comment

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.

Beyond the inline findings, I also checked cubic's P2 claim that ClientSessionGroup._establish_session still emits two deprecation warnings (one from SseServerParameters, one from sse_client) — it does not. With @asynccontextmanager outermost, the @deprecated wrapper runs at context-manager construction, i.e. inside the catch_warnings block; entering the CM later via enter_async_context emits nothing (verified empirically). The caller sees exactly one warning, from building SseServerParameters.

Extended reasoning...

Bugs from this run are already attached as inline comments, so no full review body is warranted. This note records one additional concern that was examined and ruled out: cubic-dev-ai's P2 on src/mcp/client/session_group.py claims the suppression must extend through enter_async_context, but the deprecation warning for sse_client fires when the context manager object is constructed (the deprecated wrapper is invoked from _AsyncGeneratorContextManager.__init__), which happens inside the existing catch_warnings block — entering the context manager afterwards re-runs nothing that warns. Reproduced with a minimal replica of the exact decorator stack and call pattern: zero warnings escape the suppression block.

Comment on lines 361 to 369
@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"],

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 @deprecated decorator on the run(transport="sse") overload has no runtime effect (PEP 702: a decorated @overload stub only informs static checkers), so at runtime a mcp.run(transport="sse") caller instead sees the nested run_sse_async warning — "Use run_streamable_http_async instead" — naming an async API they never called, when the direct migration is transport="streamable-http" in the same run() call. Fix by warnings.warn()-ing the overload's transport-oriented message in the case "sse" branch and suppressing the nested run_sse_async warning with catch_warnings, matching the pattern run_sse_async already uses for sse_app.

Extended reasoning...

What the bug is. This PR stacks @overload over @deprecated on the run(transport: Literal["sse"]) overload (src/mcp/server/mcpserver/server.py:361-369). Per PEP 702 / typing_extensions, @deprecated applied to an @overload stub only informs static checkers: typing.overload registers the stub and returns a dummy that is never called, and the stub function object is discarded when the implementation def run(...) executes. So the overload's carefully worded message — 'transport="sse" is deprecated … Use transport="streamable-http" instead.' — is never emitted at runtime. Three independent verifiers reproduced this empirically with a minimal replica of the exact decorator structure: zero warnings fire from the overload decorator.

The code path that triggers it. mcp.run(transport="sse") reaches the implementation's case "sse" branch, which dispatches anyio.run(lambda: self.run_sse_async(**kwargs)). run_sse_async is genuinely decorated, so its warning fires — but that is the wrong message for this caller.

Step-by-step proof.

  1. A user calls mcp.run(transport="sse") — a synchronous, top-level entry point.
  2. The overload's @deprecated emits nothing (stub discarded; the implementation has no __deprecated__ attribute).
  3. The case "sse" branch invokes run_sse_async via a lambda; its @deprecated wrapper fires: MCPDeprecationWarning: The HTTP+SSE transport is deprecated as of 2025-03-26 (SEP-2596). Use run_streamable_http_async instead.
  4. The user is told to migrate to an async method they never touched, when the actual one-token fix is transport="streamable-http" in the call they already have. The warning is also attributed to the lambda inside server.py, not the user's call site.

Why existing code doesn't prevent it. The PR's stated design is that nested constructions suppress inner warnings "so the caller sees exactly one, naming the thing they called" — and the authors did add catch_warnings suppression for run_sse_asyncsse_app and sse_appSseServerTransport. The run()run_sse_async hop is the one link in the chain with no explicit runtime warning above it, presumably because the overload decorator was assumed to fire at runtime. No test catches it: the case "sse" branch is # pragma: no cover, and the three new warning tests cover sse_client, SseServerTransport, and sse_app() only.

Impact. Limited: exactly one MCPDeprecationWarning still fires with the correct category, the correct SEP-2596 reference, and a workable (if indirect) migration path — and pyright still flags the call site correctly via the overload. Nothing breaks; the message is merely misdirected for the highest-level, most-documented entry point (run() is what mcp run --transport sse and every quickstart-style server use).

How to fix. In the implementation's case "sse" branch, explicitly warnings.warn() the overload's transport-oriented message (as MCPDeprecationWarning, with a stacklevel pointing at the caller), then suppress the nested run_sse_async warning with warnings.catch_warnings() + simplefilter("ignore", MCPDeprecationWarning) — the exact pattern run_sse_async already applies to its own nested sse_app() call. Keeping the @deprecated on the overload is still right for static checkers; it just needs a runtime counterpart.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant