diff --git a/docs/migration.md b/docs/migration.md index cc49638c0..d8f64cdf9 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -1341,6 +1341,8 @@ The user-facing methods for these features now carry `typing_extensions.deprecat - 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()` +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. + The runtime warning is emitted as `mcp.MCPDeprecationWarning`, which subclasses `UserWarning` (not `DeprecationWarning`) so it is visible by default. To silence it, filter that category: ```python diff --git a/examples/stories/streaming/server_lowlevel.py b/examples/stories/streaming/server_lowlevel.py index 07daf641b..6d9add0b6 100644 --- a/examples/stories/streaming/server_lowlevel.py +++ b/examples/stories/streaming/server_lowlevel.py @@ -57,7 +57,7 @@ async def set_logging_level( """Registered so the server advertises the `logging` capability; never called.""" raise NotImplementedError - return Server( + return Server( # pyright: ignore[reportDeprecated] "streaming-example", on_list_tools=list_tools, on_call_tool=call_tool, diff --git a/src/mcp/server/lowlevel/server.py b/src/mcp/server/lowlevel/server.py index ea6ea77df..778e89e45 100644 --- a/src/mcp/server/lowlevel/server.py +++ b/src/mcp/server/lowlevel/server.py @@ -37,11 +37,12 @@ async def main(): from __future__ import annotations import logging +import warnings from collections.abc import AsyncIterator, Awaitable, Callable from contextlib import AbstractAsyncContextManager, asynccontextmanager from dataclasses import dataclass from importlib.metadata import version as importlib_version -from typing import Any, Generic +from typing import Any, Generic, overload import mcp_types as types from mcp_types.version import MODERN_PROTOCOL_VERSIONS @@ -50,7 +51,7 @@ async def main(): from starlette.middleware import Middleware from starlette.middleware.authentication import AuthenticationMiddleware from starlette.routing import Mount, Route -from typing_extensions import TypeVar +from typing_extensions import TypeVar, deprecated from mcp.server._otel import OpenTelemetryMiddleware from mcp.server.auth.middleware.auth_context import AuthContextMiddleware @@ -65,6 +66,7 @@ async def main(): from mcp.server.streamable_http_manager import StreamableHTTPASGIApp, StreamableHTTPSessionManager from mcp.server.transport_security import TransportSecuritySettings from mcp.shared._stream_protocols import ReadStream, WriteStream +from mcp.shared.exceptions import MCPDeprecationWarning from mcp.shared.message import SessionMessage logger = logging.getLogger(__name__) @@ -127,6 +129,89 @@ def _package_version(package: str) -> str: class Server(Generic[LifespanResultT]): + @overload + def __init__( + self, + name: str, + *, + version: str | None = None, + title: str | None = None, + description: str | None = None, + instructions: str | None = None, + website_url: str | None = None, + icons: list[types.Icon] | None = None, + lifespan: Callable[ + [Server[LifespanResultT]], + AbstractAsyncContextManager[LifespanResultT], + ] = lifespan, + # Request handlers + on_list_tools: Callable[ + [ServerRequestContext[LifespanResultT], types.PaginatedRequestParams | None], + Awaitable[types.ListToolsResult], + ] + | None = None, + on_call_tool: Callable[ + [ServerRequestContext[LifespanResultT], types.CallToolRequestParams], + Awaitable[types.CallToolResult | types.InputRequiredResult], + ] + | None = None, + on_list_resources: Callable[ + [ServerRequestContext[LifespanResultT], types.PaginatedRequestParams | None], + Awaitable[types.ListResourcesResult], + ] + | None = None, + on_list_resource_templates: Callable[ + [ServerRequestContext[LifespanResultT], types.PaginatedRequestParams | None], + Awaitable[types.ListResourceTemplatesResult], + ] + | None = None, + on_read_resource: Callable[ + [ServerRequestContext[LifespanResultT], types.ReadResourceRequestParams], + Awaitable[types.ReadResourceResult | types.InputRequiredResult], + ] + | None = None, + on_subscribe_resource: Callable[ + [ServerRequestContext[LifespanResultT], types.SubscribeRequestParams], + Awaitable[types.EmptyResult], + ] + | None = None, + on_unsubscribe_resource: Callable[ + [ServerRequestContext[LifespanResultT], types.UnsubscribeRequestParams], + Awaitable[types.EmptyResult], + ] + | None = None, + on_subscriptions_listen: Callable[ + [ServerRequestContext[LifespanResultT], types.SubscriptionsListenRequestParams], + Awaitable[types.EmptyResult], + ] + | None = None, + on_list_prompts: Callable[ + [ServerRequestContext[LifespanResultT], types.PaginatedRequestParams | None], + Awaitable[types.ListPromptsResult], + ] + | None = None, + on_get_prompt: Callable[ + [ServerRequestContext[LifespanResultT], types.GetPromptRequestParams], + Awaitable[types.GetPromptResult | types.InputRequiredResult], + ] + | None = None, + on_completion: Callable[ + [ServerRequestContext[LifespanResultT], types.CompleteRequestParams], + Awaitable[types.CompleteResult], + ] + | None = None, + on_ping: Callable[ + [ServerRequestContext[LifespanResultT], types.RequestParams | None], + Awaitable[types.EmptyResult], + ] = _ping_handler, + ) -> None: ... + @overload + @deprecated( + "on_set_logging_level (Logging) and on_roots_list_changed (Roots) are deprecated as of 2026-07-28 " + "(SEP-2577); on_progress (client-to-server progress) is deprecated as of 2026-07-28. Passing any of " + "them emits an MCPDeprecationWarning at runtime.", + category=MCPDeprecationWarning, + ) def __init__( self, name: str, @@ -217,7 +302,117 @@ def __init__( Awaitable[None], ] | None = None, - ): + ) -> None: ... + def __init__( + self, + name: str, + *, + version: str | None = None, + title: str | None = None, + description: str | None = None, + instructions: str | None = None, + website_url: str | None = None, + icons: list[types.Icon] | None = None, + lifespan: Callable[ + [Server[LifespanResultT]], + AbstractAsyncContextManager[LifespanResultT], + ] = lifespan, + # Request handlers + on_list_tools: Callable[ + [ServerRequestContext[LifespanResultT], types.PaginatedRequestParams | None], + Awaitable[types.ListToolsResult], + ] + | None = None, + on_call_tool: Callable[ + [ServerRequestContext[LifespanResultT], types.CallToolRequestParams], + Awaitable[types.CallToolResult | types.InputRequiredResult], + ] + | None = None, + on_list_resources: Callable[ + [ServerRequestContext[LifespanResultT], types.PaginatedRequestParams | None], + Awaitable[types.ListResourcesResult], + ] + | None = None, + on_list_resource_templates: Callable[ + [ServerRequestContext[LifespanResultT], types.PaginatedRequestParams | None], + Awaitable[types.ListResourceTemplatesResult], + ] + | None = None, + on_read_resource: Callable[ + [ServerRequestContext[LifespanResultT], types.ReadResourceRequestParams], + Awaitable[types.ReadResourceResult | types.InputRequiredResult], + ] + | None = None, + on_subscribe_resource: Callable[ + [ServerRequestContext[LifespanResultT], types.SubscribeRequestParams], + Awaitable[types.EmptyResult], + ] + | None = None, + on_unsubscribe_resource: Callable[ + [ServerRequestContext[LifespanResultT], types.UnsubscribeRequestParams], + Awaitable[types.EmptyResult], + ] + | None = None, + on_subscriptions_listen: Callable[ + [ServerRequestContext[LifespanResultT], types.SubscriptionsListenRequestParams], + Awaitable[types.EmptyResult], + ] + | None = None, + on_list_prompts: Callable[ + [ServerRequestContext[LifespanResultT], types.PaginatedRequestParams | None], + Awaitable[types.ListPromptsResult], + ] + | None = None, + on_get_prompt: Callable[ + [ServerRequestContext[LifespanResultT], types.GetPromptRequestParams], + Awaitable[types.GetPromptResult | types.InputRequiredResult], + ] + | None = None, + on_completion: Callable[ + [ServerRequestContext[LifespanResultT], types.CompleteRequestParams], + Awaitable[types.CompleteResult], + ] + | None = None, + on_set_logging_level: Callable[ + [ServerRequestContext[LifespanResultT], types.SetLevelRequestParams], + Awaitable[types.EmptyResult], + ] + | None = None, + on_ping: Callable[ + [ServerRequestContext[LifespanResultT], types.RequestParams | None], + Awaitable[types.EmptyResult], + ] = _ping_handler, + # Notification handlers + on_roots_list_changed: Callable[ + [ServerRequestContext[LifespanResultT], types.NotificationParams | None], + Awaitable[None], + ] + | None = None, + on_progress: Callable[ + [ServerRequestContext[LifespanResultT], types.ProgressNotificationParams], + Awaitable[None], + ] + | None = None, + ) -> None: + if on_set_logging_level is not None: + warnings.warn( + "The logging capability is deprecated as of 2026-07-28 (SEP-2577).", + MCPDeprecationWarning, + stacklevel=2, + ) + if on_roots_list_changed is not None: + warnings.warn( + "The roots capability is deprecated as of 2026-07-28 (SEP-2577).", + MCPDeprecationWarning, + stacklevel=2, + ) + if on_progress is not None: + warnings.warn( + "Client-to-server progress is deprecated as of 2026-07-28.", + MCPDeprecationWarning, + stacklevel=2, + ) + self.name = name self.version = version self.title = title diff --git a/tests/client/test_client.py b/tests/client/test_client.py index e8557bcec..83df95f47 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -73,7 +73,7 @@ async def handle_set_logging_level(ctx: ServerRequestContext, params: types.SetL async def handle_completion(ctx: ServerRequestContext, params: types.CompleteRequestParams) -> types.CompleteResult: return types.CompleteResult(completion=types.Completion(values=[])) - return Server( + return Server( # pyright: ignore[reportDeprecated] name="test_server", on_list_resources=handle_list_resources, on_subscribe_resource=handle_subscribe_resource, @@ -277,7 +277,7 @@ async def handle_progress(ctx: ServerRequestContext, params: types.ProgressNotif received_from_client = {"progress_token": params.progress_token, "progress": params.progress} event.set() - server = Server(name="test_server", on_progress=handle_progress) + server = Server(name="test_server", on_progress=handle_progress) # pyright: ignore[reportDeprecated] with anyio.fail_after(5): async with Client(server, mode="legacy") as client: diff --git a/tests/interaction/lowlevel/test_flows.py b/tests/interaction/lowlevel/test_flows.py index 77cf78888..19788db4a 100644 --- a/tests/interaction/lowlevel/test_flows.py +++ b/tests/interaction/lowlevel/test_flows.py @@ -173,7 +173,7 @@ async def set_logging_level(ctx: ServerRequestContext, params: types.SetLevelReq """Registered so the logging capability is advertised; the client never sets a level.""" raise NotImplementedError - server = Server( + server = Server( # pyright: ignore[reportDeprecated] "gatekeeper", on_list_tools=_list_tools("read_files"), on_call_tool=call_tool, diff --git a/tests/interaction/lowlevel/test_initialize.py b/tests/interaction/lowlevel/test_initialize.py index 29eb7297c..f3fd9589a 100644 --- a/tests/interaction/lowlevel/test_initialize.py +++ b/tests/interaction/lowlevel/test_initialize.py @@ -127,7 +127,7 @@ async def completion(ctx: ServerRequestContext, params: types.CompleteRequestPar """Registered only so the completions capability is advertised; never called.""" raise NotImplementedError - server = Server( + server = Server( # pyright: ignore[reportDeprecated] "full", on_list_tools=list_tools, on_list_resources=list_resources, diff --git a/tests/interaction/lowlevel/test_logging.py b/tests/interaction/lowlevel/test_logging.py index 90af931cf..bfc86509d 100644 --- a/tests/interaction/lowlevel/test_logging.py +++ b/tests/interaction/lowlevel/test_logging.py @@ -36,7 +36,7 @@ async def set_logging_level(ctx: ServerRequestContext, params: types.SetLevelReq assert params.level == "warning" return EmptyResult() - server = Server("logger", on_set_logging_level=set_logging_level) + server = Server("logger", on_set_logging_level=set_logging_level) # pyright: ignore[reportDeprecated] async with connect(server) as client: result = await client.set_logging_level("warning") # pyright: ignore[reportDeprecated] @@ -76,7 +76,9 @@ async def set_logging_level(ctx: ServerRequestContext, params: types.SetLevelReq """Registered so the logging capability is advertised; the client never sets a level.""" raise NotImplementedError - server = Server("logger", on_list_tools=list_tools, on_call_tool=call_tool, on_set_logging_level=set_logging_level) + server = Server( # pyright: ignore[reportDeprecated] + "logger", on_list_tools=list_tools, on_call_tool=call_tool, on_set_logging_level=set_logging_level + ) async with connect(server, logging_callback=collect) as client: result = await client.call_tool("chatty", {}) @@ -115,7 +117,9 @@ async def set_logging_level(ctx: ServerRequestContext, params: types.SetLevelReq """Registered so the logging capability is advertised; the client never sets a level.""" raise NotImplementedError - server = Server("logger", on_list_tools=list_tools, on_call_tool=call_tool, on_set_logging_level=set_logging_level) + server = Server( # pyright: ignore[reportDeprecated] + "logger", on_list_tools=list_tools, on_call_tool=call_tool, on_set_logging_level=set_logging_level + ) async with connect(server, logging_callback=collect) as client: await client.call_tool("siren", {}) diff --git a/tests/interaction/lowlevel/test_progress.py b/tests/interaction/lowlevel/test_progress.py index b2e94bae3..7f75e18ee 100644 --- a/tests/interaction/lowlevel/test_progress.py +++ b/tests/interaction/lowlevel/test_progress.py @@ -118,7 +118,7 @@ async def on_progress(ctx: ServerRequestContext, params: ProgressNotificationPar received.append(params) delivered.set() - server = Server("observer", on_progress=on_progress) + server = Server("observer", on_progress=on_progress) # pyright: ignore[reportDeprecated] async with connect(server) as client: await client.send_progress_notification("upload-1", 0.5, total=1.0, message="halfway") # pyright: ignore[reportDeprecated] diff --git a/tests/interaction/lowlevel/test_roots.py b/tests/interaction/lowlevel/test_roots.py index 5e38b8aab..bfd6cc90a 100644 --- a/tests/interaction/lowlevel/test_roots.py +++ b/tests/interaction/lowlevel/test_roots.py @@ -153,7 +153,7 @@ async def roots_list_changed(ctx: ServerRequestContext, params: types.Notificati received.append(params) delivered.set() - server = Server("rooted", on_roots_list_changed=roots_list_changed) + server = Server("rooted", on_roots_list_changed=roots_list_changed) # pyright: ignore[reportDeprecated] async def list_roots(context: ClientRequestContext) -> ListRootsResult: """Registered so the client declares the roots capability; the server never asks for roots.""" diff --git a/tests/interaction/lowlevel/test_wire.py b/tests/interaction/lowlevel/test_wire.py index cddbfdfa5..73452f1af 100644 --- a/tests/interaction/lowlevel/test_wire.py +++ b/tests/interaction/lowlevel/test_wire.py @@ -262,7 +262,7 @@ async def set_logging_level(ctx: ServerRequestContext, params: types.SetLevelReq """Registered so the logging capability is advertised; never called -- params validation fails first.""" raise NotImplementedError - server = Server("logger", on_set_logging_level=set_logging_level) + server = Server("logger", on_set_logging_level=set_logging_level) # pyright: ignore[reportDeprecated] errors: list[ErrorData] = [] async with create_client_server_memory_streams() as (client_streams, server_streams): diff --git a/tests/interaction/transports/_stdio_server.py b/tests/interaction/transports/_stdio_server.py index cc3dc3a8a..811de1540 100644 --- a/tests/interaction/transports/_stdio_server.py +++ b/tests/interaction/transports/_stdio_server.py @@ -53,7 +53,11 @@ async def set_logging_level(ctx: ServerRequestContext, params: SetLevelRequestPa raise NotImplementedError -server = Server("stdio-echo", on_list_tools=list_tools, on_call_tool=call_tool, on_set_logging_level=set_logging_level) +with warnings.catch_warnings(): + warnings.simplefilter("ignore", MCPDeprecationWarning) + server = Server( # pyright: ignore[reportDeprecated] + "stdio-echo", on_list_tools=list_tools, on_call_tool=call_tool, on_set_logging_level=set_logging_level + ) async def main() -> None: diff --git a/tests/interaction/transports/test_hosting_http.py b/tests/interaction/transports/test_hosting_http.py index 9c83e213c..6331c2dae 100644 --- a/tests/interaction/transports/test_hosting_http.py +++ b/tests/interaction/transports/test_hosting_http.py @@ -73,7 +73,7 @@ async def subscribe_resource(ctx: ServerRequestContext, params: SubscribeRequest """Registered so the resources subscribe sub-capability is advertised; the client never subscribes.""" raise NotImplementedError - return Server( + return Server( # pyright: ignore[reportDeprecated] "hosted", on_list_tools=list_tools, on_call_tool=call_tool,