From 86bd4f34088154056e2b240fb02368a47e4f111f Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 20 Jun 2026 17:27:47 +0200 Subject: [PATCH 1/7] Deprecate roots, sampling, and logging methods per SEP-2577 SEP-2577 deprecates the Roots, Sampling, and Logging features as of the 2026-07-28 spec. The deprecation is advisory only: no wire-level changes, capability negotiation is unchanged, and every method keeps working for sessions negotiating 2025-11-25 and earlier. Mark the user-facing methods for these features with `typing_extensions.deprecated` so type checkers, IDEs, and the runtime warn at the call site: `create_message`/`sample` (sampling), `list_roots` / `send_roots_list_changed` (roots), `send_log_message` / `set_logging_level` and the `MCPServer` `Context` log helpers (logging). Unlike decorating the schema types, this keeps the footprint small while still making users aware. The advisory runtime warning is silenced via a scoped `filterwarnings` entry since the SDK calls these methods internally (e.g. `ctx.debug` -> `log` -> `send_log_message`) to serve older sessions. --- README.v2.md | 18 ++++++------- docs/migration.md | 12 ++++++++- .../mcp_everything_server/server.py | 12 ++++----- .../server.py | 2 +- .../mcp_simple_streamablehttp/server.py | 2 +- .../mcp_sse_polling_demo/server.py | 6 ++--- examples/snippets/servers/notifications.py | 8 +++--- examples/snippets/servers/sampling.py | 2 +- examples/snippets/servers/tool_progress.py | 4 +-- pyproject.toml | 4 +++ src/mcp/client/client.py | 8 ++++-- src/mcp/client/session.py | 4 ++- src/mcp/server/context.py | 3 ++- src/mcp/server/mcpserver/context.py | 16 ++++++++---- src/mcp/server/session.py | 6 +++++ src/mcp/shared/peer.py | 5 ++++ tests/client/test_client.py | 2 +- tests/client/test_list_roots_callback.py | 2 +- tests/client/test_logging_callback.py | 4 +-- tests/client/test_sampling_callback.py | 4 +-- tests/client/test_session_concurrency.py | 2 +- tests/interaction/lowlevel/test_flows.py | 2 +- tests/interaction/lowlevel/test_logging.py | 8 +++--- tests/interaction/lowlevel/test_roots.py | 10 +++---- tests/interaction/lowlevel/test_sampling.py | 26 +++++++++---------- tests/interaction/lowlevel/test_wire.py | 2 +- tests/interaction/mcpserver/test_context.py | 16 ++++++------ tests/interaction/mcpserver/test_tools.py | 2 +- tests/interaction/transports/_stdio_server.py | 5 +++- tests/interaction/transports/test_flows.py | 2 +- .../transports/test_hosting_http.py | 2 +- .../transports/test_hosting_resume.py | 18 ++++++------- .../transports/test_streamable_http.py | 2 +- tests/server/mcpserver/test_server.py | 8 +++--- tests/server/test_cancel_handling.py | 2 +- tests/server/test_server_context.py | 4 +-- tests/server/test_session.py | 2 +- tests/server/test_stateless_mode.py | 10 +++---- tests/shared/test_peer.py | 20 ++++++++++---- tests/shared/test_streamable_http.py | 18 ++++++------- 40 files changed, 168 insertions(+), 117 deletions(-) diff --git a/README.v2.md b/README.v2.md index 25cf5ac959..e631745d76 100644 --- a/README.v2.md +++ b/README.v2.md @@ -360,7 +360,7 @@ mcp = MCPServer(name="Progress Example") @mcp.tool() async def long_running_task(task_name: str, ctx: Context, steps: int = 5) -> str: """Execute a task with progress updates.""" - await ctx.info(f"Starting: {task_name}") + await ctx.info(f"Starting: {task_name}") # pyright: ignore[reportDeprecated] for i in range(steps): progress = (i + 1) / steps @@ -369,7 +369,7 @@ async def long_running_task(task_name: str, ctx: Context, steps: int = 5) -> str total=1.0, message=f"Step {i + 1}/{steps}", ) - await ctx.debug(f"Completed step {i + 1}") + await ctx.debug(f"Completed step {i + 1}") # pyright: ignore[reportDeprecated] return f"Task '{task_name}' completed" ``` @@ -707,7 +707,7 @@ mcp = MCPServer(name="Progress Example") @mcp.tool() async def long_running_task(task_name: str, ctx: Context, steps: int = 5) -> str: """Execute a task with progress updates.""" - await ctx.info(f"Starting: {task_name}") + await ctx.info(f"Starting: {task_name}") # pyright: ignore[reportDeprecated] for i in range(steps): progress = (i + 1) / steps @@ -716,7 +716,7 @@ async def long_running_task(task_name: str, ctx: Context, steps: int = 5) -> str total=1.0, message=f"Step {i + 1}/{steps}", ) - await ctx.debug(f"Completed step {i + 1}") + await ctx.debug(f"Completed step {i + 1}") # pyright: ignore[reportDeprecated] return f"Task '{task_name}' completed" ``` @@ -948,7 +948,7 @@ async def generate_poem(topic: str, ctx: Context) -> str: """Generate a poem using LLM sampling.""" prompt = f"Write a short poem about {topic}" - result = await ctx.session.create_message( + result = await ctx.session.create_message( # pyright: ignore[reportDeprecated] messages=[ SamplingMessage( role="user", @@ -982,10 +982,10 @@ mcp = MCPServer(name="Notifications Example") async def process_data(data: str, ctx: Context) -> str: """Process data with logging.""" # Different log levels - await ctx.debug(f"Debug: Processing '{data}'") - await ctx.info("Info: Starting processing") - await ctx.warning("Warning: This is experimental") - await ctx.error("Error: (This is just a demo)") + await ctx.debug(f"Debug: Processing '{data}'") # pyright: ignore[reportDeprecated] + await ctx.info("Info: Starting processing") # pyright: ignore[reportDeprecated] + await ctx.warning("Warning: This is experimental") # pyright: ignore[reportDeprecated] + await ctx.error("Error: (This is just a demo)") # pyright: ignore[reportDeprecated] # Notify about resource changes await ctx.session.send_resource_list_changed() diff --git a/docs/migration.md b/docs/migration.md index 675c5b747a..0563097bea 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -1216,7 +1216,17 @@ Tasks are expected to return as a separate MCP extension in a future release. ## Deprecations - +### Roots, Sampling, and Logging methods deprecated (SEP-2577) + +[SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577) deprecates the Roots, Sampling, and Logging features as of the 2026-07-28 spec. The deprecation is advisory only: there are no wire-level changes, capability negotiation is unchanged, and every method keeps working for sessions negotiating 2025-11-25 and earlier. + +The user-facing methods for these features now carry `typing_extensions.deprecated`, so type checkers, IDEs, and the runtime surface a deprecation warning where they are called: + +- Sampling: `ServerSession.create_message()`, `ClientPeer.sample()` +- Roots: `ServerSession.list_roots()`, `ClientPeer.list_roots()`, `ClientSession.send_roots_list_changed()`, `Client.send_roots_list_changed()` +- Logging: `ServerSession.send_log_message()`, `ClientSession.set_logging_level()`, `Client.set_logging_level()`, and the `MCPServer` `Context` helpers `log()`, `debug()`, `info()`, `warning()`, `error()` + +No migration is required during the deprecation window. New code should avoid building on these features, since they may be removed in a future spec version. ## Bug Fixes diff --git a/examples/servers/everything-server/mcp_everything_server/server.py b/examples/servers/everything-server/mcp_everything_server/server.py index b37ff3e950..01baa56340 100644 --- a/examples/servers/everything-server/mcp_everything_server/server.py +++ b/examples/servers/everything-server/mcp_everything_server/server.py @@ -143,13 +143,13 @@ def test_multiple_content_types() -> list[TextContent | ImageContent | EmbeddedR @mcp.tool() async def test_tool_with_logging(ctx: Context) -> str: """Tests tool that emits log messages during execution""" - await ctx.info("Tool execution started") + await ctx.info("Tool execution started") # pyright: ignore[reportDeprecated] await asyncio.sleep(0.05) - await ctx.info("Tool processing data") + await ctx.info("Tool processing data") # pyright: ignore[reportDeprecated] await asyncio.sleep(0.05) - await ctx.info("Tool execution completed") + await ctx.info("Tool execution completed") # pyright: ignore[reportDeprecated] return "Tool with logging executed successfully" @@ -176,7 +176,7 @@ async def test_sampling(prompt: str, ctx: Context) -> str: """Tests server-initiated sampling (LLM completion request)""" try: # Request sampling from client - result = await ctx.session.create_message( + result = await ctx.session.create_message( # pyright: ignore[reportDeprecated] messages=[SamplingMessage(role="user", content=TextContent(type="text", text=prompt))], max_tokens=100, ) @@ -314,13 +314,13 @@ def test_error_handling() -> str: @mcp.tool() async def test_reconnection(ctx: Context) -> str: """Tests SSE polling by closing stream mid-call (SEP-1699)""" - await ctx.info("Before disconnect") + await ctx.info("Before disconnect") # pyright: ignore[reportDeprecated] await ctx.close_sse_stream() await asyncio.sleep(0.2) # Wait for client to reconnect - await ctx.info("After reconnect") + await ctx.info("After reconnect") # pyright: ignore[reportDeprecated] return "Reconnection test completed" diff --git a/examples/servers/simple-streamablehttp-stateless/mcp_simple_streamablehttp_stateless/server.py b/examples/servers/simple-streamablehttp-stateless/mcp_simple_streamablehttp_stateless/server.py index e2b8d2ef2f..feffb057eb 100644 --- a/examples/servers/simple-streamablehttp-stateless/mcp_simple_streamablehttp_stateless/server.py +++ b/examples/servers/simple-streamablehttp-stateless/mcp_simple_streamablehttp_stateless/server.py @@ -49,7 +49,7 @@ async def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequ # Send the specified number of notifications with the given interval for i in range(count): - await ctx.session.send_log_message( + await ctx.session.send_log_message( # pyright: ignore[reportDeprecated] level="info", data=f"Notification {i + 1}/{count} from caller: {caller}", logger="notification_stream", diff --git a/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/server.py b/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/server.py index ec9761d1b0..9ab5b4d1f6 100644 --- a/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/server.py +++ b/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/server.py @@ -54,7 +54,7 @@ async def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequ for i in range(count): # Include more detailed message for resumability demonstration notification_msg = f"[{i + 1}/{count}] Event from '{caller}' - Use Last-Event-ID to resume if disconnected" - await ctx.session.send_log_message( + await ctx.session.send_log_message( # pyright: ignore[reportDeprecated] level="info", data=notification_msg, logger="notification_stream", diff --git a/examples/servers/sse-polling-demo/mcp_sse_polling_demo/server.py b/examples/servers/sse-polling-demo/mcp_sse_polling_demo/server.py index 14bc174c47..54ed960de3 100644 --- a/examples/servers/sse-polling-demo/mcp_sse_polling_demo/server.py +++ b/examples/servers/sse-polling-demo/mcp_sse_polling_demo/server.py @@ -74,7 +74,7 @@ async def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequ content=[types.TextContent(type="text", text="Error: checkpoint_every must be between 1 and 20")] ) - await ctx.session.send_log_message( + await ctx.session.send_log_message( # pyright: ignore[reportDeprecated] level="info", data=f"Starting batch processing of {items} items...", logger="process_batch", @@ -86,7 +86,7 @@ async def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequ await anyio.sleep(0.5) # Report progress - await ctx.session.send_log_message( + await ctx.session.send_log_message( # pyright: ignore[reportDeprecated] level="info", data=f"[{i}/{items}] Processing item {i}", logger="process_batch", @@ -95,7 +95,7 @@ async def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequ # Checkpoint: close stream to trigger client reconnect if i % checkpoint_every == 0 and i < items: - await ctx.session.send_log_message( + await ctx.session.send_log_message( # pyright: ignore[reportDeprecated] level="info", data=f"Checkpoint at item {i} - closing SSE stream for polling", logger="process_batch", diff --git a/examples/snippets/servers/notifications.py b/examples/snippets/servers/notifications.py index d6d903cc7f..05c0fbf331 100644 --- a/examples/snippets/servers/notifications.py +++ b/examples/snippets/servers/notifications.py @@ -7,10 +7,10 @@ async def process_data(data: str, ctx: Context) -> str: """Process data with logging.""" # Different log levels - await ctx.debug(f"Debug: Processing '{data}'") - await ctx.info("Info: Starting processing") - await ctx.warning("Warning: This is experimental") - await ctx.error("Error: (This is just a demo)") + await ctx.debug(f"Debug: Processing '{data}'") # pyright: ignore[reportDeprecated] + await ctx.info("Info: Starting processing") # pyright: ignore[reportDeprecated] + await ctx.warning("Warning: This is experimental") # pyright: ignore[reportDeprecated] + await ctx.error("Error: (This is just a demo)") # pyright: ignore[reportDeprecated] # Notify about resource changes await ctx.session.send_resource_list_changed() diff --git a/examples/snippets/servers/sampling.py b/examples/snippets/servers/sampling.py index 43259589a4..a3f6d5c7bd 100644 --- a/examples/snippets/servers/sampling.py +++ b/examples/snippets/servers/sampling.py @@ -9,7 +9,7 @@ async def generate_poem(topic: str, ctx: Context) -> str: """Generate a poem using LLM sampling.""" prompt = f"Write a short poem about {topic}" - result = await ctx.session.create_message( + result = await ctx.session.create_message( # pyright: ignore[reportDeprecated] messages=[ SamplingMessage( role="user", diff --git a/examples/snippets/servers/tool_progress.py b/examples/snippets/servers/tool_progress.py index 376dbc5db8..78703416af 100644 --- a/examples/snippets/servers/tool_progress.py +++ b/examples/snippets/servers/tool_progress.py @@ -6,7 +6,7 @@ @mcp.tool() async def long_running_task(task_name: str, ctx: Context, steps: int = 5) -> str: """Execute a task with progress updates.""" - await ctx.info(f"Starting: {task_name}") + await ctx.info(f"Starting: {task_name}") # pyright: ignore[reportDeprecated] for i in range(steps): progress = (i + 1) / steps @@ -15,6 +15,6 @@ async def long_running_task(task_name: str, ctx: Context, steps: int = 5) -> str total=1.0, message=f"Step {i + 1}/{steps}", ) - await ctx.debug(f"Completed step {i + 1}") + await ctx.debug(f"Completed step {i + 1}") # pyright: ignore[reportDeprecated] return f"Task '{task_name}' completed" diff --git a/pyproject.toml b/pyproject.toml index 8f8b8c37e1..aa67601d42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -213,6 +213,10 @@ filterwarnings = [ "error", # pywin32 internal deprecation warning "ignore:getargs.*The 'u' format is deprecated:DeprecationWarning", + # SEP-2577 deprecates the roots/sampling/logging methods; the SDK still calls + # them internally (e.g. `ctx.debug` -> `log` -> `send_log_message`), so the + # advisory warning is silenced. Tests asserting it opt back in with pytest.warns. + "ignore:`.*` is deprecated as of 2026-07-28 \\(SEP-2577\\).:DeprecationWarning", ] [tool.markdown.lint] diff --git a/src/mcp/client/client.py b/src/mcp/client/client.py index b5ae59daa3..47dc8c9c16 100644 --- a/src/mcp/client/client.py +++ b/src/mcp/client/client.py @@ -6,6 +6,8 @@ from dataclasses import KW_ONLY, dataclass, field from typing import Any +from typing_extensions import deprecated + from mcp.client._memory import InMemoryTransport from mcp.client._transport import Transport from mcp.client.session import ClientSession, ElicitationFnT, ListRootsFnT, LoggingFnT, MessageHandlerFnT, SamplingFnT @@ -195,9 +197,10 @@ async def send_progress_notification( message=message, ) + @deprecated("`set_logging_level` is deprecated as of 2026-07-28 (SEP-2577).") async def set_logging_level(self, level: LoggingLevel, *, meta: RequestParamsMeta | None = None) -> EmptyResult: """Set the logging level on the server.""" - return await self.session.set_logging_level(level=level, meta=meta) + return await self.session.set_logging_level(level=level, meta=meta) # pyright: ignore[reportDeprecated] async def list_resources( self, @@ -312,7 +315,8 @@ async def list_tools(self, *, cursor: str | None = None, meta: RequestParamsMeta """List available tools from the server.""" return await self.session.list_tools(params=PaginatedRequestParams(cursor=cursor, _meta=meta)) + @deprecated("`send_roots_list_changed` is deprecated as of 2026-07-28 (SEP-2577).") async def send_roots_list_changed(self) -> None: """Send a notification that the roots list has changed.""" # TODO(Marcelo): Currently, there is no way for the server to handle this. We should add support. - await self.session.send_roots_list_changed() + await self.session.send_roots_list_changed() # pyright: ignore[reportDeprecated] diff --git a/src/mcp/client/session.py b/src/mcp/client/session.py index cd18a67541..e55f5698c5 100644 --- a/src/mcp/client/session.py +++ b/src/mcp/client/session.py @@ -10,7 +10,7 @@ import anyio.abc import anyio.lowlevel from pydantic import BaseModel, TypeAdapter, ValidationError -from typing_extensions import Self, TypeVar +from typing_extensions import Self, TypeVar, deprecated from mcp import types from mcp.client._transport import ReadStream, WriteStream @@ -386,6 +386,7 @@ async def send_progress_notification( ) ) + @deprecated("`set_logging_level` is deprecated as of 2026-07-28 (SEP-2577).") async def set_logging_level( self, level: types.LoggingLevel, @@ -550,6 +551,7 @@ async def list_tools(self, *, params: types.PaginatedRequestParams | None = None return result + @deprecated("`send_roots_list_changed` is deprecated as of 2026-07-28 (SEP-2577).") async def send_roots_list_changed(self) -> None: """Send a roots/list_changed notification.""" await self.send_notification(types.RootsListChangedNotification()) diff --git a/src/mcp/server/context.py b/src/mcp/server/context.py index b7effb70f3..15f0351499 100644 --- a/src/mcp/server/context.py +++ b/src/mcp/server/context.py @@ -3,7 +3,7 @@ from typing import Any, Generic, Protocol from pydantic import BaseModel -from typing_extensions import TypeVar +from typing_extensions import TypeVar, deprecated from mcp.server.connection import Connection from mcp.server.session import ServerSession @@ -92,6 +92,7 @@ def headers(self) -> Mapping[str, str] | None: """ return self.transport.headers + @deprecated("`log` is deprecated as of 2026-07-28 (SEP-2577).") async def log(self, level: LoggingLevel, data: Any, logger: str | None = None, *, meta: Meta | None = None) -> None: """Send a request-scoped `notifications/message` log entry. diff --git a/src/mcp/server/mcpserver/context.py b/src/mcp/server/mcpserver/context.py index e853605496..5ff16d171b 100644 --- a/src/mcp/server/mcpserver/context.py +++ b/src/mcp/server/mcpserver/context.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Any, Generic from pydantic import AnyUrl, BaseModel +from typing_extensions import deprecated from mcp.server.context import LifespanContextT, RequestT, ServerRequestContext from mcp.server.elicitation import ( @@ -189,6 +190,7 @@ async def elicit_url( related_request_id=self.request_id, ) + @deprecated("`log` is deprecated as of 2026-07-28 (SEP-2577).") async def log( self, level: LoggingLevel, @@ -205,7 +207,7 @@ async def log( (string, dict, list, number, bool, etc.) per the MCP specification. logger_name: Optional logger name """ - await self.request_context.session.send_log_message( + await self.request_context.session.send_log_message( # pyright: ignore[reportDeprecated] level=level, data=data, logger=logger_name, @@ -265,18 +267,22 @@ async def close_standalone_sse_stream(self) -> None: await self._request_context.close_standalone_sse_stream() # Convenience methods for common log levels + @deprecated("`debug` is deprecated as of 2026-07-28 (SEP-2577).") async def debug(self, data: Any, *, logger_name: str | None = None) -> None: """Send a debug log message.""" - await self.log("debug", data, logger_name=logger_name) + await self.log("debug", data, logger_name=logger_name) # pyright: ignore[reportDeprecated] + @deprecated("`info` is deprecated as of 2026-07-28 (SEP-2577).") async def info(self, data: Any, *, logger_name: str | None = None) -> None: """Send an info log message.""" - await self.log("info", data, logger_name=logger_name) + await self.log("info", data, logger_name=logger_name) # pyright: ignore[reportDeprecated] + @deprecated("`warning` is deprecated as of 2026-07-28 (SEP-2577).") async def warning(self, data: Any, *, logger_name: str | None = None) -> None: """Send a warning log message.""" - await self.log("warning", data, logger_name=logger_name) + await self.log("warning", data, logger_name=logger_name) # pyright: ignore[reportDeprecated] + @deprecated("`error` is deprecated as of 2026-07-28 (SEP-2577).") async def error(self, data: Any, *, logger_name: str | None = None) -> None: """Send an error log message.""" - await self.log("error", data, logger_name=logger_name) + await self.log("error", data, logger_name=logger_name) # pyright: ignore[reportDeprecated] diff --git a/src/mcp/server/session.py b/src/mcp/server/session.py index f56b87c9a3..e02366ac2f 100644 --- a/src/mcp/server/session.py +++ b/src/mcp/server/session.py @@ -12,6 +12,7 @@ from typing import Any, TypeVar, cast, overload from pydantic import AnyUrl, BaseModel +from typing_extensions import deprecated from mcp import types from mcp.server.connection import Connection @@ -126,6 +127,7 @@ def check_client_capability(self, capability: types.ClientCapabilities) -> bool: """Check if the client supports a specific capability.""" return self._connection.check_capability(capability) + @deprecated("`send_log_message` is deprecated as of 2026-07-28 (SEP-2577).") async def send_log_message( self, level: types.LoggingLevel, @@ -154,6 +156,7 @@ async def send_resource_updated(self, uri: str | AnyUrl) -> None: ) @overload + @deprecated("`create_message` is deprecated as of 2026-07-28 (SEP-2577).") async def create_message( self, messages: list[types.SamplingMessage], @@ -173,6 +176,7 @@ async def create_message( ... @overload + @deprecated("`create_message` is deprecated as of 2026-07-28 (SEP-2577).") async def create_message( self, messages: list[types.SamplingMessage], @@ -191,6 +195,7 @@ async def create_message( """Overload: With tools, returns array-capable content.""" ... + @deprecated("`create_message` is deprecated as of 2026-07-28 (SEP-2577).") async def create_message( self, messages: list[types.SamplingMessage], @@ -267,6 +272,7 @@ async def create_message( metadata=metadata_obj, ) + @deprecated("`list_roots` is deprecated as of 2026-07-28 (SEP-2577).") async def list_roots(self) -> types.ListRootsResult: """Send a roots/list request.""" if self._stateless: diff --git a/src/mcp/shared/peer.py b/src/mcp/shared/peer.py index 410b96a975..2b48e65acc 100644 --- a/src/mcp/shared/peer.py +++ b/src/mcp/shared/peer.py @@ -13,6 +13,7 @@ from typing import Any, cast, overload from pydantic import BaseModel +from typing_extensions import deprecated from mcp.shared.dispatcher import CallOptions, Outbound from mcp.types import ( @@ -83,6 +84,7 @@ async def notify(self, method: str, params: Mapping[str, Any] | None) -> None: await self._outbound.notify(method, params) @overload + @deprecated("`sample` is deprecated as of 2026-07-28 (SEP-2577).") async def sample( self, messages: list[SamplingMessage], @@ -100,6 +102,7 @@ async def sample( opts: CallOptions | None = None, ) -> CreateMessageResult: ... @overload + @deprecated("`sample` is deprecated as of 2026-07-28 (SEP-2577).") async def sample( self, messages: list[SamplingMessage], @@ -116,6 +119,7 @@ async def sample( meta: Meta | None = None, opts: CallOptions | None = None, ) -> CreateMessageResultWithTools: ... + @deprecated("`sample` is deprecated as of 2026-07-28 (SEP-2577).") async def sample( self, messages: list[SamplingMessage], @@ -195,6 +199,7 @@ async def elicit_url( result = await self.send_raw_request("elicitation/create", dump_params(params, meta), opts) return ElicitResult.model_validate(result, by_name=False) + @deprecated("`list_roots` is deprecated as of 2026-07-28 (SEP-2577).") async def list_roots(self, *, meta: Meta | None = None, opts: CallOptions | None = None) -> ListRootsResult: """Send a `roots/list` request. diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 64b6666eca..3680639e0f 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -259,7 +259,7 @@ async def test_client_unsubscribe_resource(simple_server: Server): async def test_client_set_logging_level(simple_server: Server): """Test setting logging level.""" async with Client(simple_server) as client: - result = await client.set_logging_level("debug") + result = await client.set_logging_level("debug") # pyright: ignore[reportDeprecated] assert result == snapshot(EmptyResult()) diff --git a/tests/client/test_list_roots_callback.py b/tests/client/test_list_roots_callback.py index a26ef45b27..f597ef7c09 100644 --- a/tests/client/test_list_roots_callback.py +++ b/tests/client/test_list_roots_callback.py @@ -25,7 +25,7 @@ async def list_roots_callback( @server.tool("test_list_roots") async def test_list_roots(context: Context, message: str): - roots = await context.session.list_roots() + roots = await context.session.list_roots() # pyright: ignore[reportDeprecated] assert roots == callback_return return True diff --git a/tests/client/test_logging_callback.py b/tests/client/test_logging_callback.py index 454c1d3382..7a870bcd55 100644 --- a/tests/client/test_logging_callback.py +++ b/tests/client/test_logging_callback.py @@ -36,7 +36,7 @@ async def test_tool_with_log( message: str, level: Literal["debug", "info", "warning", "error"], logger: str, ctx: Context ) -> bool: """Send a log notification to the client.""" - await ctx.log(level=level, data=message, logger_name=logger) + await ctx.log(level=level, data=message, logger_name=logger) # pyright: ignore[reportDeprecated] return True @server.tool("test_tool_with_log_dict") @@ -46,7 +46,7 @@ async def test_tool_with_log_dict( ctx: Context, ) -> bool: """Send a log notification with a dict payload.""" - await ctx.log( + await ctx.log( # pyright: ignore[reportDeprecated] level=level, data={"message": "Test log message", "extra_string": "example", "extra_dict": {"a": 1, "b": 2, "c": 3}}, logger_name=logger, diff --git a/tests/client/test_sampling_callback.py b/tests/client/test_sampling_callback.py index 2b90b00afa..5163ef043a 100644 --- a/tests/client/test_sampling_callback.py +++ b/tests/client/test_sampling_callback.py @@ -32,7 +32,7 @@ async def sampling_callback( @server.tool("test_sampling") async def test_sampling_tool(message: str, ctx: Context) -> bool: - value = await ctx.session.create_message( + value = await ctx.session.create_message( # pyright: ignore[reportDeprecated] messages=[SamplingMessage(role="user", content=TextContent(type="text", text=message))], max_tokens=100, ) @@ -78,7 +78,7 @@ async def sampling_callback( @server.tool("test_backwards_compat") async def test_tool(message: str, ctx: Context) -> bool: # Call create_message WITHOUT tools - result = await ctx.session.create_message( + result = await ctx.session.create_message( # pyright: ignore[reportDeprecated] messages=[SamplingMessage(role="user", content=TextContent(type="text", text=message))], max_tokens=100, ) diff --git a/tests/client/test_session_concurrency.py b/tests/client/test_session_concurrency.py index dc91bee258..7072325104 100644 --- a/tests/client/test_session_concurrency.py +++ b/tests/client/test_session_concurrency.py @@ -95,7 +95,7 @@ async def fan_out(ctx: Context) -> str: echoes: dict[str, str] = {} async def sample(tag: str) -> None: - result = await ctx.session.create_message( + result = await ctx.session.create_message( # pyright: ignore[reportDeprecated] messages=[SamplingMessage(role="user", content=TextContent(text=tag))], max_tokens=10, ) diff --git a/tests/interaction/lowlevel/test_flows.py b/tests/interaction/lowlevel/test_flows.py index 8d96582341..75b8aa61ea 100644 --- a/tests/interaction/lowlevel/test_flows.py +++ b/tests/interaction/lowlevel/test_flows.py @@ -156,7 +156,7 @@ async def call_tool(ctx: ServerRequestContext, params: types.CallToolRequestPara if not authorised[0]: # The log line gives the message handler a non-completion notification, so the test's # filtering branch is exercised in both directions and the wait remains specific. - await ctx.session.send_log_message(level="warning", data="authorisation required", logger="gate") + await ctx.session.send_log_message(level="warning", data="authorisation required", logger="gate") # pyright: ignore[reportDeprecated] raise UrlElicitationRequiredError( [ ElicitRequestURLParams( diff --git a/tests/interaction/lowlevel/test_logging.py b/tests/interaction/lowlevel/test_logging.py index b8f9d3d776..4b8c3ebb97 100644 --- a/tests/interaction/lowlevel/test_logging.py +++ b/tests/interaction/lowlevel/test_logging.py @@ -39,7 +39,7 @@ async def set_logging_level(ctx: ServerRequestContext, params: types.SetLevelReq server = Server("logger", on_set_logging_level=set_logging_level) async with connect(server) as client: - result = await client.set_logging_level("warning") + result = await client.set_logging_level("warning") # pyright: ignore[reportDeprecated] assert result == snapshot(EmptyResult()) @@ -64,10 +64,10 @@ async def list_tools( async def call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> CallToolResult: assert params.name == "chatty" - await ctx.session.send_log_message( + await ctx.session.send_log_message( # pyright: ignore[reportDeprecated] level="info", data="starting up", logger="app.lifecycle", related_request_id=ctx.request_id ) - await ctx.session.send_log_message( + await ctx.session.send_log_message( # pyright: ignore[reportDeprecated] level="error", data={"code": 502, "retryable": True}, related_request_id=ctx.request_id ) return CallToolResult(content=[TextContent(text="done")]) @@ -106,7 +106,7 @@ async def list_tools( async def call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> CallToolResult: assert params.name == "siren" for level in ALL_LEVELS: - await ctx.session.send_log_message( + await ctx.session.send_log_message( # pyright: ignore[reportDeprecated] level=level, data=f"a {level} message", related_request_id=ctx.request_id ) return CallToolResult(content=[TextContent(text="logged")]) diff --git a/tests/interaction/lowlevel/test_roots.py b/tests/interaction/lowlevel/test_roots.py index deb24cbf55..391fc8ec61 100644 --- a/tests/interaction/lowlevel/test_roots.py +++ b/tests/interaction/lowlevel/test_roots.py @@ -29,7 +29,7 @@ async def list_tools( async def call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> CallToolResult: assert params.name == "show_roots" - result = await ctx.session.list_roots() + result = await ctx.session.list_roots() # pyright: ignore[reportDeprecated] lines = [f"{root.uri} name={root.name}" for root in result.roots] return CallToolResult(content=[TextContent(text="\n".join(lines))]) @@ -64,7 +64,7 @@ async def list_tools( async def call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> CallToolResult: assert params.name == "count_roots" - result = await ctx.session.list_roots() + result = await ctx.session.list_roots() # pyright: ignore[reportDeprecated] return CallToolResult(content=[TextContent(text=str(len(result.roots)))]) server = Server("rooted", on_list_tools=list_tools, on_call_tool=call_tool) @@ -94,7 +94,7 @@ async def list_tools( async def call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> CallToolResult: assert params.name == "show_roots" try: - await ctx.session.list_roots() + await ctx.session.list_roots() # pyright: ignore[reportDeprecated] except MCPError as exc: return CallToolResult(content=[TextContent(text=f"{exc.error.code}: {exc.error.message}")]) raise NotImplementedError # list_roots cannot succeed without a client callback @@ -122,7 +122,7 @@ async def list_tools( async def call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> CallToolResult: assert params.name == "show_roots" try: - await ctx.session.list_roots() + await ctx.session.list_roots() # pyright: ignore[reportDeprecated] except MCPError as exc: return CallToolResult(content=[TextContent(text=f"{exc.error.code}: {exc.error.message}")]) raise NotImplementedError # the callback always answers with an error @@ -159,7 +159,7 @@ async def list_roots(context: ClientRequestContext) -> ListRootsResult: raise NotImplementedError async with connect(server, list_roots_callback=list_roots) as client: - await client.send_roots_list_changed() + await client.send_roots_list_changed() # pyright: ignore[reportDeprecated] with anyio.fail_after(5): await delivered.wait() diff --git a/tests/interaction/lowlevel/test_sampling.py b/tests/interaction/lowlevel/test_sampling.py index 260e564192..fb66c3ad81 100644 --- a/tests/interaction/lowlevel/test_sampling.py +++ b/tests/interaction/lowlevel/test_sampling.py @@ -49,7 +49,7 @@ async def list_tools( async def call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> CallToolResult: assert params.name == "ask_model" - result = await ctx.session.create_message( + result = await ctx.session.create_message( # pyright: ignore[reportDeprecated] messages=[SamplingMessage(role="user", content=TextContent(text="Say hello."))], max_tokens=100, ) @@ -104,7 +104,7 @@ async def list_tools( async def call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> CallToolResult: assert params.name == "ask_model" - result = await ctx.session.create_message( + result = await ctx.session.create_message( # pyright: ignore[reportDeprecated] messages=[SamplingMessage(role="user", content=TextContent(text="Pick a model."))], max_tokens=50, system_prompt="You are terse.", @@ -170,7 +170,7 @@ async def list_tools( async def call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> CallToolResult: assert params.name == "describe_image" - result = await ctx.session.create_message( + result = await ctx.session.create_message( # pyright: ignore[reportDeprecated] messages=[SamplingMessage(role="user", content=ImageContent(data="aW1n", mime_type="image/png"))], max_tokens=100, ) @@ -220,7 +220,7 @@ async def list_tools( async def call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> CallToolResult: assert params.name == "draw" - result = await ctx.session.create_message( + result = await ctx.session.create_message( # pyright: ignore[reportDeprecated] messages=[SamplingMessage(role="user", content=TextContent(text="Draw a cat."))], max_tokens=100, ) @@ -261,7 +261,7 @@ async def list_tools( async def call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> CallToolResult: assert params.name == "ask_model" try: - await ctx.session.create_message( + await ctx.session.create_message( # pyright: ignore[reportDeprecated] messages=[SamplingMessage(role="user", content=TextContent(text="Say hello."))], max_tokens=100, ) @@ -292,7 +292,7 @@ async def list_tools( async def call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> CallToolResult: assert params.name == "ask_model" try: - await ctx.session.create_message( + await ctx.session.create_message( # pyright: ignore[reportDeprecated] messages=[SamplingMessage(role="user", content=TextContent(text="Say hello."))], max_tokens=100, ) @@ -324,7 +324,7 @@ async def list_tools( async def call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> CallToolResult: assert params.name == "ask_model" try: - await ctx.session.create_message( + await ctx.session.create_message( # pyright: ignore[reportDeprecated] messages=[SamplingMessage(role="user", content=TextContent(text="What is the weather?"))], max_tokens=100, tools=[types.Tool(name="get_weather", input_schema={"type": "object"})], @@ -366,7 +366,7 @@ async def list_tools( async def call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> CallToolResult: assert params.name == "summarise_tools" try: - await ctx.session.create_message( + await ctx.session.create_message( # pyright: ignore[reportDeprecated] messages=[ SamplingMessage( role="user", @@ -452,7 +452,7 @@ async def list_tools( async def call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> CallToolResult: assert params.name == "transcribe" - result = await ctx.session.create_message( + result = await ctx.session.create_message( # pyright: ignore[reportDeprecated] messages=[SamplingMessage(role="user", content=AudioContent(data="c25k", mime_type="audio/wav"))], max_tokens=100, ) @@ -502,7 +502,7 @@ async def list_tools( async def call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> CallToolResult: assert params.name == "speak" - result = await ctx.session.create_message( + result = await ctx.session.create_message( # pyright: ignore[reportDeprecated] messages=[SamplingMessage(role="user", content=TextContent(text="Say hello, aloud."))], max_tokens=100, ) @@ -539,7 +539,7 @@ async def list_tools( async def call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> CallToolResult: assert params.name == "caption" - result = await ctx.session.create_message( + result = await ctx.session.create_message( # pyright: ignore[reportDeprecated] messages=[ SamplingMessage( role="user", @@ -606,7 +606,7 @@ async def list_tools( async def call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> CallToolResult: assert params.name == "continue_tools" try: - await ctx.session.create_message( + await ctx.session.create_message( # pyright: ignore[reportDeprecated] messages=[ SamplingMessage( role="assistant", @@ -662,7 +662,7 @@ async def list_tools( async def call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> CallToolResult: assert params.name == "ask_model" try: - await ctx.session.create_message( + await ctx.session.create_message( # pyright: ignore[reportDeprecated] messages=[SamplingMessage(role="user", content=TextContent(text="Two thoughts, please."))], max_tokens=100, ) diff --git a/tests/interaction/lowlevel/test_wire.py b/tests/interaction/lowlevel/test_wire.py index 178c2c1c38..ace780d7ec 100644 --- a/tests/interaction/lowlevel/test_wire.py +++ b/tests/interaction/lowlevel/test_wire.py @@ -96,7 +96,7 @@ async def list_roots(context: ClientRequestContext) -> ListRootsResult: recording = RecordingTransport(InMemoryTransport(_echo_server())) async with Client(recording, list_roots_callback=list_roots) as client: - await client.send_roots_list_changed() + await client.send_roots_list_changed() # pyright: ignore[reportDeprecated] await client.send_ping() sent = [message.message for message in recording.sent] diff --git a/tests/interaction/mcpserver/test_context.py b/tests/interaction/mcpserver/test_context.py index 3e6bb542e6..f3ee3f52e4 100644 --- a/tests/interaction/mcpserver/test_context.py +++ b/tests/interaction/mcpserver/test_context.py @@ -41,10 +41,10 @@ async def test_context_logging_helpers_send_log_notifications(connect: Connect) @mcp.tool() async def narrate(ctx: Context) -> str: - await ctx.debug("d") - await ctx.info("i") - await ctx.warning("w") - await ctx.error("e") + await ctx.debug("d") # pyright: ignore[reportDeprecated] + await ctx.info("i") # pyright: ignore[reportDeprecated] + await ctx.warning("w") # pyright: ignore[reportDeprecated] + await ctx.error("e") # pyright: ignore[reportDeprecated] return "done" async def collect(params: LoggingMessageNotificationParams) -> None: @@ -136,7 +136,7 @@ async def test_report_progress_without_a_progress_token_sends_nothing(connect: C @mcp.tool() async def mill(ctx: Context) -> str: await ctx.report_progress(1, 3) - await ctx.info("milling done") + await ctx.info("milling done") # pyright: ignore[reportDeprecated] return "milled" async def collect(message: IncomingMessage) -> None: @@ -250,8 +250,8 @@ async def test_set_logging_level_is_rejected_and_messages_are_never_filtered(con @mcp.tool() async def chatter(ctx: Context) -> str: - await ctx.debug("noise") - await ctx.error("signal") + await ctx.debug("noise") # pyright: ignore[reportDeprecated] + await ctx.error("signal") # pyright: ignore[reportDeprecated] return "done" async def collect(params: LoggingMessageNotificationParams) -> None: @@ -259,7 +259,7 @@ async def collect(params: LoggingMessageNotificationParams) -> None: async with connect(mcp, logging_callback=collect) as client: with pytest.raises(MCPError) as exc_info: - await client.set_logging_level("error") + await client.set_logging_level("error") # pyright: ignore[reportDeprecated] await client.call_tool("chatter", {}) diff --git a/tests/interaction/mcpserver/test_tools.py b/tests/interaction/mcpserver/test_tools.py index 05135c1286..1314d85587 100644 --- a/tests/interaction/mcpserver/test_tools.py +++ b/tests/interaction/mcpserver/test_tools.py @@ -414,7 +414,7 @@ def doomed() -> str: async def grow(ctx: Context) -> str: mcp.add_tool(extra, name="extra") mcp.remove_tool("doomed") - await ctx.info("tool set changed") + await ctx.info("tool set changed") # pyright: ignore[reportDeprecated] return "mutated" async def collect(message: IncomingMessage) -> None: diff --git a/tests/interaction/transports/_stdio_server.py b/tests/interaction/transports/_stdio_server.py index a6dad4772d..4f5bd000ae 100644 --- a/tests/interaction/transports/_stdio_server.py +++ b/tests/interaction/transports/_stdio_server.py @@ -7,6 +7,7 @@ """ import sys +import warnings import anyio import coverage @@ -40,7 +41,9 @@ async def call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> assert params.name == "echo" assert params.arguments is not None text = params.arguments["text"] - await ctx.session.send_log_message(level="info", data=f"echoing {text}", logger="echo") + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + await ctx.session.send_log_message(level="info", data=f"echoing {text}", logger="echo") # pyright: ignore[reportDeprecated] return CallToolResult(content=[TextContent(text=text)]) diff --git a/tests/interaction/transports/test_flows.py b/tests/interaction/transports/test_flows.py index c428fe2d68..e8081a7a1d 100644 --- a/tests/interaction/transports/test_flows.py +++ b/tests/interaction/transports/test_flows.py @@ -32,7 +32,7 @@ async def test_concurrent_clients_on_one_stateful_server_receive_only_their_own_ @mcp.tool() async def announce(label: str, ctx: Context) -> str: """Emit one info-level log carrying the caller's label, then return it.""" - await ctx.info(label) + await ctx.info(label) # pyright: ignore[reportDeprecated] return label received_a: list[object] = [] diff --git a/tests/interaction/transports/test_hosting_http.py b/tests/interaction/transports/test_hosting_http.py index e4a92ea7e4..9b46dc533d 100644 --- a/tests/interaction/transports/test_hosting_http.py +++ b/tests/interaction/transports/test_hosting_http.py @@ -52,7 +52,7 @@ async def list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | async def call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult: assert params.name == "narrate" - await ctx.session.send_log_message(level="info", data="related", logger=None, related_request_id=ctx.request_id) + await ctx.session.send_log_message(level="info", data="related", logger=None, related_request_id=ctx.request_id) # pyright: ignore[reportDeprecated] await ctx.session.send_resource_updated("file:///watched.txt") return CallToolResult(content=[TextContent(text="done")]) diff --git a/tests/interaction/transports/test_hosting_resume.py b/tests/interaction/transports/test_hosting_resume.py index c7945d56c3..b22df0ff2b 100644 --- a/tests/interaction/transports/test_hosting_resume.py +++ b/tests/interaction/transports/test_hosting_resume.py @@ -55,7 +55,7 @@ def _counting_server() -> MCPServer: async def count(ctx: Context, n: int) -> str: """Emit n log notifications related to this call, plus one unrelated resource update.""" for i in range(1, n + 1): - await ctx.info(f"tick {i}") + await ctx.info(f"tick {i}") # pyright: ignore[reportDeprecated] await ctx.session.send_resource_updated("file:///elsewhere.txt") return f"counted to {n}" @@ -138,10 +138,10 @@ async def test_get_with_last_event_id_replays_only_that_streams_missed_events() @mcp.tool() async def count(ctx: Context) -> str: """Emit one related notification, wait for the test, then emit two more plus an unrelated one.""" - await ctx.info("tick 1") + await ctx.info("tick 1") # pyright: ignore[reportDeprecated] await release.wait() - await ctx.info("tick 2") - await ctx.info("tick 3") + await ctx.info("tick 2") # pyright: ignore[reportDeprecated] + await ctx.info("tick 3") # pyright: ignore[reportDeprecated] await ctx.session.send_resource_updated("file:///elsewhere.txt") return "counted" @@ -219,7 +219,7 @@ async def hold(ctx: Context) -> str: """Signal start, wait for the test, signal completion.""" started.set() await release.wait() - await ctx.info("released") + await ctx.info("released") # pyright: ignore[reportDeprecated] finished.set() return "held" @@ -263,10 +263,10 @@ async def test_a_call_whose_stream_the_server_closes_is_resumed_by_the_client() @mcp.tool() async def interrupt(ctx: Context) -> str: """Emit, close this call's SSE stream, then emit again after the test releases the gate.""" - await ctx.info("before close") + await ctx.info("before close") # pyright: ignore[reportDeprecated] await ctx.close_sse_stream() await gate.wait() - await ctx.info("after close") + await ctx.info("after close") # pyright: ignore[reportDeprecated] done.set() return "resumed" @@ -321,9 +321,9 @@ async def test_a_captured_resumption_token_replays_missed_messages_on_a_new_conn @mcp.tool() async def hold(ctx: Context) -> str: """Emit one notification, wait for the test, emit another, return.""" - await ctx.info("first") + await ctx.info("first") # pyright: ignore[reportDeprecated] await release.wait() - await ctx.info("second") + await ctx.info("second") # pyright: ignore[reportDeprecated] return "done" async def on_token(token: str) -> None: diff --git a/tests/interaction/transports/test_streamable_http.py b/tests/interaction/transports/test_streamable_http.py index d38e2a0bb3..bf5a32f5ba 100644 --- a/tests/interaction/transports/test_streamable_http.py +++ b/tests/interaction/transports/test_streamable_http.py @@ -55,7 +55,7 @@ async def ask(ctx: Context) -> str: @mcp.tool() async def announce(ctx: Context) -> str: """Send one notification related to this request and one that is not.""" - await ctx.info("about to announce") + await ctx.info("about to announce") # pyright: ignore[reportDeprecated] await ctx.session.send_resource_updated("file:///watched.txt") return "announced" diff --git a/tests/server/mcpserver/test_server.py b/tests/server/mcpserver/test_server.py index 98d4a23261..d1816e6400 100644 --- a/tests/server/mcpserver/test_server.py +++ b/tests/server/mcpserver/test_server.py @@ -1110,10 +1110,10 @@ async def test_context_logging(self): mcp = MCPServer() async def logging_tool(msg: str, ctx: Context) -> str: - await ctx.debug("Debug message") - await ctx.info("Info message") - await ctx.warning("Warning message") - await ctx.error("Error message") + await ctx.debug("Debug message") # pyright: ignore[reportDeprecated] + await ctx.info("Info message") # pyright: ignore[reportDeprecated] + await ctx.warning("Warning message") # pyright: ignore[reportDeprecated] + await ctx.error("Error message") # pyright: ignore[reportDeprecated] return f"Logged messages for {msg}" mcp.add_tool(logging_tool) diff --git a/tests/server/test_cancel_handling.py b/tests/server/test_cancel_handling.py index 10531fad93..0744e63022 100644 --- a/tests/server/test_cancel_handling.py +++ b/tests/server/test_cancel_handling.py @@ -195,7 +195,7 @@ async def handle_call_tool(ctx: ServerRequestContext, params: CallToolRequestPar both_started.set() # Blocks on send_request waiting for a client response that never comes. # _receive_loop's finally will wake this with CONNECTION_CLOSED. - await ctx.session.list_roots() + await ctx.session.list_roots() # pyright: ignore[reportDeprecated] raise AssertionError # pragma: no cover server = Server("test", on_call_tool=handle_call_tool) diff --git a/tests/server/test_server_context.py b/tests/server/test_server_context.py index 01e96ff379..5665d2ff77 100644 --- a/tests/server/test_server_context.py +++ b/tests/server/test_server_context.py @@ -61,7 +61,7 @@ async def server_on_request(dctx: DCtx, method: str, params: Mapping[str, Any] | ctx: Context[_Lifespan] = Context( dctx, lifespan=_Lifespan("app"), connection=Connection(dctx, has_standalone_channel=True) ) - await ctx.log("debug", "hello") + await ctx.log("debug", "hello") # pyright: ignore[reportDeprecated] return {} async with running_pair(direct_pair, server_on_request=server_on_request, client_on_notify=c_notify) as ( @@ -85,7 +85,7 @@ async def server_on_request(dctx: DCtx, method: str, params: Mapping[str, Any] | ctx: Context[_Lifespan] = Context( dctx, lifespan=_Lifespan("app"), connection=Connection(dctx, has_standalone_channel=True) ) - await ctx.log("info", "x", logger="my.log", meta={"traceId": "t"}) + await ctx.log("info", "x", logger="my.log", meta={"traceId": "t"}) # pyright: ignore[reportDeprecated] return {} async with running_pair(direct_pair, server_on_request=server_on_request, client_on_notify=c_notify) as ( diff --git a/tests/server/test_session.py b/tests/server/test_session.py index cddf3c19ec..cb664d5b4e 100644 --- a/tests/server/test_session.py +++ b/tests/server/test_session.py @@ -175,7 +175,7 @@ async def test_create_message_with_tools_returns_with_tools_result(): session = _make_session( dispatcher, capabilities=ClientCapabilities(sampling=SamplingCapability(tools=SamplingToolsCapability())) ) - result = await session.create_message( + result = await session.create_message( # pyright: ignore[reportDeprecated] messages=[types.SamplingMessage(role="user", content=types.TextContent(type="text", text="hi"))], max_tokens=10, tools=[types.Tool(name="t", input_schema={"type": "object"})], diff --git a/tests/server/test_stateless_mode.py b/tests/server/test_stateless_mode.py index 1b628e2388..7002fe1cf4 100644 --- a/tests/server/test_stateless_mode.py +++ b/tests/server/test_stateless_mode.py @@ -42,14 +42,14 @@ def stateless_session() -> ServerSession: async def test_list_roots_fails_in_stateless_mode(stateless_session: ServerSession): """Test that list_roots raises StatelessModeNotSupported in stateless mode.""" with pytest.raises(StatelessModeNotSupported, match="list_roots"): - await stateless_session.list_roots() + await stateless_session.list_roots() # pyright: ignore[reportDeprecated] @pytest.mark.anyio async def test_create_message_fails_in_stateless_mode(stateless_session: ServerSession): """Test that create_message raises StatelessModeNotSupported in stateless mode.""" with pytest.raises(StatelessModeNotSupported, match="sampling"): - await stateless_session.create_message( + await stateless_session.create_message( # pyright: ignore[reportDeprecated] messages=[ types.SamplingMessage( role="user", @@ -95,7 +95,7 @@ async def test_elicit_deprecated_fails_in_stateless_mode(stateless_session: Serv async def test_stateless_error_message_is_actionable(stateless_session: ServerSession): """Test that the error message provides actionable guidance.""" with pytest.raises(StatelessModeNotSupported) as exc_info: - await stateless_session.list_roots() + await stateless_session.list_roots() # pyright: ignore[reportDeprecated] error_message = str(exc_info.value) # Should mention it's stateless mode @@ -110,7 +110,7 @@ async def test_stateless_error_message_is_actionable(stateless_session: ServerSe async def test_exception_has_method_attribute(stateless_session: ServerSession): """Test that the exception has a method attribute for programmatic access.""" with pytest.raises(StatelessModeNotSupported) as exc_info: - await stateless_session.list_roots() + await stateless_session.list_roots() # pyright: ignore[reportDeprecated] assert exc_info.value.method == "list_roots" @@ -139,7 +139,7 @@ async def mock_send_request(*_: Any, **__: Any) -> types.ListRootsResult: monkeypatch.setattr(stateful_session, "send_request", mock_send_request) # This should NOT raise StatelessModeNotSupported - result = await stateful_session.list_roots() + result = await stateful_session.list_roots() # pyright: ignore[reportDeprecated] assert send_request_called assert isinstance(result, types.ListRootsResult) diff --git a/tests/shared/test_peer.py b/tests/shared/test_peer.py index a839991a6c..691a6a7352 100644 --- a/tests/shared/test_peer.py +++ b/tests/shared/test_peer.py @@ -46,7 +46,7 @@ async def test_peer_sample_sends_create_message_and_returns_typed_result(): async with running_pair(direct_pair, server_on_request=rec.on_request) as (client, *_): peer = ClientPeer(client) with anyio.fail_after(5): - result = await peer.sample( + result = await peer.sample( # pyright: ignore[reportDeprecated] [SamplingMessage(role="user", content=TextContent(type="text", text="hello"))], max_tokens=10, ) @@ -66,7 +66,7 @@ async def test_peer_sample_validates_result_alias_only(): async with running_pair(direct_pair, server_on_request=rec.on_request) as (client, *_): peer = ClientPeer(client) with anyio.fail_after(5): - result = await peer.sample( + result = await peer.sample( # pyright: ignore[reportDeprecated] [SamplingMessage(role="user", content=TextContent(type="text", text="q"))], max_tokens=1 ) assert isinstance(result, CreateMessageResult) @@ -79,7 +79,7 @@ async def test_peer_sample_with_tools_returns_with_tools_result(): async with running_pair(direct_pair, server_on_request=rec.on_request) as (client, *_): peer = ClientPeer(client) with anyio.fail_after(5): - result = await peer.sample( + result = await peer.sample( # pyright: ignore[reportDeprecated] [SamplingMessage(role="user", content=TextContent(type="text", text="q"))], max_tokens=5, tools=[Tool(name="t", input_schema={"type": "object"})], @@ -124,7 +124,7 @@ async def test_peer_list_roots_sends_roots_list_and_returns_typed_result(): async with running_pair(direct_pair, server_on_request=rec.on_request) as (client, *_): peer = ClientPeer(client) with anyio.fail_after(5): - result = await peer.list_roots() + result = await peer.list_roots() # pyright: ignore[reportDeprecated] method, _ = rec.seen[0] assert method == "roots/list" assert isinstance(result, ListRootsResult) @@ -138,12 +138,22 @@ async def test_peer_list_roots_with_meta_sends_meta_in_params(): async with running_pair(direct_pair, server_on_request=rec.on_request) as (client, *_): peer = ClientPeer(client) with anyio.fail_after(5): - await peer.list_roots(meta={"traceId": "t1"}) + await peer.list_roots(meta={"traceId": "t1"}) # pyright: ignore[reportDeprecated] method, params = rec.seen[0] assert method == "roots/list" assert params == {"_meta": {"traceId": "t1"}} +@pytest.mark.anyio +async def test_peer_list_roots_is_deprecated_sep_2577(): + rec = _Recorder({"roots": []}) + async with running_pair(direct_pair, server_on_request=rec.on_request) as (client, *_): + peer = ClientPeer(client) + with pytest.warns(DeprecationWarning, match=r"`list_roots` is deprecated as of 2026-07-28 \(SEP-2577\)\."): + with anyio.fail_after(5): + await peer.list_roots() # pyright: ignore[reportDeprecated] + + def test_dump_params_merges_meta_over_model_meta(): out = dump_params(None, None) assert out is None diff --git a/tests/shared/test_streamable_http.py b/tests/shared/test_streamable_http.py index 6aadf6ff88..a3273add58 100644 --- a/tests/shared/test_streamable_http.py +++ b/tests/shared/test_streamable_http.py @@ -212,7 +212,7 @@ async def _handle_call_tool(ctx: ServerRequestContext[ServerState], params: Call return CallToolResult(content=[TextContent(type="text", text=f"Called {name}")]) elif name == "test_sampling_tool": - sampling_result = await ctx.session.create_message( + sampling_result = await ctx.session.create_message( # pyright: ignore[reportDeprecated] messages=[ types.SamplingMessage( role="user", @@ -234,7 +234,7 @@ async def _handle_call_tool(ctx: ServerRequestContext[ServerState], params: Call ) elif name == "wait_for_lock_with_notification": - await ctx.session.send_log_message( + await ctx.session.send_log_message( # pyright: ignore[reportDeprecated] level="info", data="First notification before lock", logger="lock_tool", @@ -243,7 +243,7 @@ async def _handle_call_tool(ctx: ServerRequestContext[ServerState], params: Call await ctx.lifespan_context.lock.wait() - await ctx.session.send_log_message( + await ctx.session.send_log_message( # pyright: ignore[reportDeprecated] level="info", data="Second notification after lock", logger="lock_tool", @@ -257,7 +257,7 @@ async def _handle_call_tool(ctx: ServerRequestContext[ServerState], params: Call return CallToolResult(content=[TextContent(type="text", text="Lock released")]) elif name == "tool_with_stream_close": - await ctx.session.send_log_message( + await ctx.session.send_log_message( # pyright: ignore[reportDeprecated] level="info", data="Before close", logger="stream_close_tool", @@ -266,7 +266,7 @@ async def _handle_call_tool(ctx: ServerRequestContext[ServerState], params: Call assert ctx.close_sse_stream is not None await ctx.close_sse_stream() await anyio.sleep(0.1) - await ctx.session.send_log_message( + await ctx.session.send_log_message( # pyright: ignore[reportDeprecated] level="info", data="After close", logger="stream_close_tool", @@ -275,7 +275,7 @@ async def _handle_call_tool(ctx: ServerRequestContext[ServerState], params: Call return CallToolResult(content=[TextContent(type="text", text="Done")]) elif name == "tool_with_multiple_notifications_and_close": - await ctx.session.send_log_message( + await ctx.session.send_log_message( # pyright: ignore[reportDeprecated] level="info", data="notification1", logger="multi_notif_tool", @@ -284,13 +284,13 @@ async def _handle_call_tool(ctx: ServerRequestContext[ServerState], params: Call assert ctx.close_sse_stream is not None await ctx.close_sse_stream() await anyio.sleep(0.1) - await ctx.session.send_log_message( + await ctx.session.send_log_message( # pyright: ignore[reportDeprecated] level="info", data="notification2", logger="multi_notif_tool", related_request_id=ctx.request_id, ) - await ctx.session.send_log_message( + await ctx.session.send_log_message( # pyright: ignore[reportDeprecated] level="info", data="notification3", logger="multi_notif_tool", @@ -2085,7 +2085,7 @@ async def on_resumption_token(token: str) -> None: async def handle_call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult: assert params.name == "multi_close_tool" for i, milestone in enumerate(milestones.values()): - await ctx.session.send_log_message( + await ctx.session.send_log_message( # pyright: ignore[reportDeprecated] level="info", data=f"checkpoint_{i}", logger="multi_close_tool", From a5c1522cac46a7e3a1d1c753e751a8395aa8768c Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 20 Jun 2026 17:40:08 +0200 Subject: [PATCH 2/7] Emit deprecation warnings as a visible MCPDeprecationWarning The SEP-2577 method deprecations now warn via `mcp.MCPDeprecationWarning`, a `UserWarning` subclass, instead of the built-in `DeprecationWarning`. Python silences `DeprecationWarning` by default outside `__main__`, so most users never see it; inheriting from `UserWarning` makes the deprecation visible by default. Pass `category=MCPDeprecationWarning` to every `@deprecated` decorator, export the class from `mcp`, and document how to silence it. --- docs/migration.md | 9 +++++++++ pyproject.toml | 2 +- src/mcp/__init__.py | 3 ++- src/mcp/client/client.py | 5 +++-- src/mcp/client/session.py | 6 +++--- src/mcp/server/context.py | 3 ++- src/mcp/server/mcpserver/context.py | 11 ++++++----- src/mcp/server/session.py | 12 ++++++------ src/mcp/shared/exceptions.py | 11 +++++++++++ src/mcp/shared/peer.py | 9 +++++---- tests/interaction/transports/_stdio_server.py | 3 ++- tests/shared/test_peer.py | 3 ++- 12 files changed, 52 insertions(+), 25 deletions(-) diff --git a/docs/migration.md b/docs/migration.md index 0563097bea..9dd103e1fb 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -1226,6 +1226,15 @@ 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()`, `ClientSession.set_logging_level()`, `Client.set_logging_level()`, and the `MCPServer` `Context` helpers `log()`, `debug()`, `info()`, `warning()`, `error()` +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 +import warnings +from mcp import MCPDeprecationWarning + +warnings.filterwarnings("ignore", category=MCPDeprecationWarning) +``` + No migration is required during the deprecation window. New code should avoid building on these features, since they may be removed in a future spec version. ## Bug Fixes diff --git a/pyproject.toml b/pyproject.toml index aa67601d42..bd29744dc8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -216,7 +216,7 @@ filterwarnings = [ # SEP-2577 deprecates the roots/sampling/logging methods; the SDK still calls # them internally (e.g. `ctx.debug` -> `log` -> `send_log_message`), so the # advisory warning is silenced. Tests asserting it opt back in with pytest.warns. - "ignore:`.*` is deprecated as of 2026-07-28 \\(SEP-2577\\).:DeprecationWarning", + "ignore:`.*` is deprecated as of 2026-07-28 \\(SEP-2577\\).:mcp.MCPDeprecationWarning", ] [tool.markdown.lint] diff --git a/src/mcp/__init__.py b/src/mcp/__init__.py index 4b5caa9cca..20cc64aac5 100644 --- a/src/mcp/__init__.py +++ b/src/mcp/__init__.py @@ -4,7 +4,7 @@ from .client.stdio import StdioServerParameters, stdio_client from .server.session import ServerSession from .server.stdio import stdio_server -from .shared.exceptions import MCPError, UrlElicitationRequiredError +from .shared.exceptions import MCPDeprecationWarning, MCPError, UrlElicitationRequiredError from .types import ( CallToolRequest, ClientCapabilities, @@ -96,6 +96,7 @@ "ListToolsResult", "LoggingLevel", "LoggingMessageNotification", + "MCPDeprecationWarning", "MCPError", "Notification", "PingRequest", diff --git a/src/mcp/client/client.py b/src/mcp/client/client.py index 47dc8c9c16..9d636b5215 100644 --- a/src/mcp/client/client.py +++ b/src/mcp/client/client.py @@ -15,6 +15,7 @@ from mcp.server import Server from mcp.server.mcpserver import MCPServer from mcp.shared.dispatcher import ProgressFnT +from mcp.shared.exceptions import MCPDeprecationWarning from mcp.types import ( CallToolResult, CompleteResult, @@ -197,7 +198,7 @@ async def send_progress_notification( message=message, ) - @deprecated("`set_logging_level` is deprecated as of 2026-07-28 (SEP-2577).") + @deprecated("`set_logging_level` is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def set_logging_level(self, level: LoggingLevel, *, meta: RequestParamsMeta | None = None) -> EmptyResult: """Set the logging level on the server.""" return await self.session.set_logging_level(level=level, meta=meta) # pyright: ignore[reportDeprecated] @@ -315,7 +316,7 @@ async def list_tools(self, *, cursor: str | None = None, meta: RequestParamsMeta """List available tools from the server.""" return await self.session.list_tools(params=PaginatedRequestParams(cursor=cursor, _meta=meta)) - @deprecated("`send_roots_list_changed` is deprecated as of 2026-07-28 (SEP-2577).") + @deprecated("`send_roots_list_changed` is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def send_roots_list_changed(self) -> None: """Send a notification that the roots list has changed.""" # TODO(Marcelo): Currently, there is no way for the server to handle this. We should add support. diff --git a/src/mcp/client/session.py b/src/mcp/client/session.py index e55f5698c5..f7c682b461 100644 --- a/src/mcp/client/session.py +++ b/src/mcp/client/session.py @@ -16,7 +16,7 @@ from mcp.client._transport import ReadStream, WriteStream from mcp.shared._compat import resync_tracer from mcp.shared.dispatcher import CallOptions, DispatchContext, Dispatcher, ProgressFnT -from mcp.shared.exceptions import MCPError +from mcp.shared.exceptions import MCPDeprecationWarning, MCPError from mcp.shared.jsonrpc_dispatcher import JSONRPCDispatcher from mcp.shared.message import ClientMessageMetadata, SessionMessage from mcp.shared.session import RequestResponder @@ -386,7 +386,7 @@ async def send_progress_notification( ) ) - @deprecated("`set_logging_level` is deprecated as of 2026-07-28 (SEP-2577).") + @deprecated("`set_logging_level` is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def set_logging_level( self, level: types.LoggingLevel, @@ -551,7 +551,7 @@ async def list_tools(self, *, params: types.PaginatedRequestParams | None = None return result - @deprecated("`send_roots_list_changed` is deprecated as of 2026-07-28 (SEP-2577).") + @deprecated("`send_roots_list_changed` is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def send_roots_list_changed(self) -> None: """Send a roots/list_changed notification.""" await self.send_notification(types.RootsListChangedNotification()) diff --git a/src/mcp/server/context.py b/src/mcp/server/context.py index 15f0351499..0facbac2e1 100644 --- a/src/mcp/server/context.py +++ b/src/mcp/server/context.py @@ -9,6 +9,7 @@ from mcp.server.session import ServerSession from mcp.shared.context import BaseContext from mcp.shared.dispatcher import DispatchContext +from mcp.shared.exceptions import MCPDeprecationWarning from mcp.shared.message import CloseSSEStreamCallback from mcp.shared.peer import Meta from mcp.shared.transport_context import TransportContext @@ -92,7 +93,7 @@ def headers(self) -> Mapping[str, str] | None: """ return self.transport.headers - @deprecated("`log` is deprecated as of 2026-07-28 (SEP-2577).") + @deprecated("`log` is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def log(self, level: LoggingLevel, data: Any, logger: str | None = None, *, meta: Meta | None = None) -> None: """Send a request-scoped `notifications/message` log entry. diff --git a/src/mcp/server/mcpserver/context.py b/src/mcp/server/mcpserver/context.py index 5ff16d171b..6a8d813314 100644 --- a/src/mcp/server/mcpserver/context.py +++ b/src/mcp/server/mcpserver/context.py @@ -15,6 +15,7 @@ elicit_with_validation, ) from mcp.server.lowlevel.helper_types import ReadResourceContents +from mcp.shared.exceptions import MCPDeprecationWarning from mcp.types import LoggingLevel if TYPE_CHECKING: @@ -190,7 +191,7 @@ async def elicit_url( related_request_id=self.request_id, ) - @deprecated("`log` is deprecated as of 2026-07-28 (SEP-2577).") + @deprecated("`log` is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def log( self, level: LoggingLevel, @@ -267,22 +268,22 @@ async def close_standalone_sse_stream(self) -> None: await self._request_context.close_standalone_sse_stream() # Convenience methods for common log levels - @deprecated("`debug` is deprecated as of 2026-07-28 (SEP-2577).") + @deprecated("`debug` is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def debug(self, data: Any, *, logger_name: str | None = None) -> None: """Send a debug log message.""" await self.log("debug", data, logger_name=logger_name) # pyright: ignore[reportDeprecated] - @deprecated("`info` is deprecated as of 2026-07-28 (SEP-2577).") + @deprecated("`info` is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def info(self, data: Any, *, logger_name: str | None = None) -> None: """Send an info log message.""" await self.log("info", data, logger_name=logger_name) # pyright: ignore[reportDeprecated] - @deprecated("`warning` is deprecated as of 2026-07-28 (SEP-2577).") + @deprecated("`warning` is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def warning(self, data: Any, *, logger_name: str | None = None) -> None: """Send a warning log message.""" await self.log("warning", data, logger_name=logger_name) # pyright: ignore[reportDeprecated] - @deprecated("`error` is deprecated as of 2026-07-28 (SEP-2577).") + @deprecated("`error` is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def error(self, data: Any, *, logger_name: str | None = None) -> None: """Send an error log message.""" await self.log("error", data, logger_name=logger_name) # pyright: ignore[reportDeprecated] diff --git a/src/mcp/server/session.py b/src/mcp/server/session.py index e02366ac2f..fd41772b26 100644 --- a/src/mcp/server/session.py +++ b/src/mcp/server/session.py @@ -18,7 +18,7 @@ from mcp.server.connection import Connection from mcp.server.validation import validate_sampling_tools, validate_tool_use_result_messages from mcp.shared.dispatcher import CallOptions, Dispatcher, ProgressFnT -from mcp.shared.exceptions import NoBackChannelError, StatelessModeNotSupported +from mcp.shared.exceptions import MCPDeprecationWarning, NoBackChannelError, StatelessModeNotSupported from mcp.shared.message import ServerMessageMetadata from mcp.types import methods as _methods @@ -127,7 +127,7 @@ def check_client_capability(self, capability: types.ClientCapabilities) -> bool: """Check if the client supports a specific capability.""" return self._connection.check_capability(capability) - @deprecated("`send_log_message` is deprecated as of 2026-07-28 (SEP-2577).") + @deprecated("`send_log_message` is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def send_log_message( self, level: types.LoggingLevel, @@ -156,7 +156,7 @@ async def send_resource_updated(self, uri: str | AnyUrl) -> None: ) @overload - @deprecated("`create_message` is deprecated as of 2026-07-28 (SEP-2577).") + @deprecated("`create_message` is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def create_message( self, messages: list[types.SamplingMessage], @@ -176,7 +176,7 @@ async def create_message( ... @overload - @deprecated("`create_message` is deprecated as of 2026-07-28 (SEP-2577).") + @deprecated("`create_message` is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def create_message( self, messages: list[types.SamplingMessage], @@ -195,7 +195,7 @@ async def create_message( """Overload: With tools, returns array-capable content.""" ... - @deprecated("`create_message` is deprecated as of 2026-07-28 (SEP-2577).") + @deprecated("`create_message` is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def create_message( self, messages: list[types.SamplingMessage], @@ -272,7 +272,7 @@ async def create_message( metadata=metadata_obj, ) - @deprecated("`list_roots` is deprecated as of 2026-07-28 (SEP-2577).") + @deprecated("`list_roots` is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def list_roots(self) -> types.ListRootsResult: """Send a roots/list request.""" if self._stateless: diff --git a/src/mcp/shared/exceptions.py b/src/mcp/shared/exceptions.py index bb4cfc0d00..9c70588022 100644 --- a/src/mcp/shared/exceptions.py +++ b/src/mcp/shared/exceptions.py @@ -5,6 +5,17 @@ from mcp.types import INVALID_REQUEST, URL_ELICITATION_REQUIRED, ElicitRequestURLParams, ErrorData, JSONRPCError +class MCPDeprecationWarning(UserWarning): + """A custom deprecation warning for the MCP SDK. + + Unlike the built-in `DeprecationWarning`, this inherits from `UserWarning` so + it is shown by default, helping users discover deprecated features without + enabling warnings explicitly. + + Reference: https://sethmlarson.dev/deprecations-via-warnings-dont-work-for-python-libraries + """ + + class MCPError(Exception): """Exception type raised when an error arrives over an MCP connection.""" diff --git a/src/mcp/shared/peer.py b/src/mcp/shared/peer.py index 2b48e65acc..99e5fb2a01 100644 --- a/src/mcp/shared/peer.py +++ b/src/mcp/shared/peer.py @@ -16,6 +16,7 @@ from typing_extensions import deprecated from mcp.shared.dispatcher import CallOptions, Outbound +from mcp.shared.exceptions import MCPDeprecationWarning from mcp.types import ( CreateMessageRequestParams, CreateMessageResult, @@ -84,7 +85,7 @@ async def notify(self, method: str, params: Mapping[str, Any] | None) -> None: await self._outbound.notify(method, params) @overload - @deprecated("`sample` is deprecated as of 2026-07-28 (SEP-2577).") + @deprecated("`sample` is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def sample( self, messages: list[SamplingMessage], @@ -102,7 +103,7 @@ async def sample( opts: CallOptions | None = None, ) -> CreateMessageResult: ... @overload - @deprecated("`sample` is deprecated as of 2026-07-28 (SEP-2577).") + @deprecated("`sample` is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def sample( self, messages: list[SamplingMessage], @@ -119,7 +120,7 @@ async def sample( meta: Meta | None = None, opts: CallOptions | None = None, ) -> CreateMessageResultWithTools: ... - @deprecated("`sample` is deprecated as of 2026-07-28 (SEP-2577).") + @deprecated("`sample` is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def sample( self, messages: list[SamplingMessage], @@ -199,7 +200,7 @@ async def elicit_url( result = await self.send_raw_request("elicitation/create", dump_params(params, meta), opts) return ElicitResult.model_validate(result, by_name=False) - @deprecated("`list_roots` is deprecated as of 2026-07-28 (SEP-2577).") + @deprecated("`list_roots` is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def list_roots(self, *, meta: Meta | None = None, opts: CallOptions | None = None) -> ListRootsResult: """Send a `roots/list` request. diff --git a/tests/interaction/transports/_stdio_server.py b/tests/interaction/transports/_stdio_server.py index 4f5bd000ae..0faf0c80ad 100644 --- a/tests/interaction/transports/_stdio_server.py +++ b/tests/interaction/transports/_stdio_server.py @@ -14,6 +14,7 @@ from mcp.server import Server, ServerRequestContext from mcp.server.stdio import stdio_server +from mcp.shared.exceptions import MCPDeprecationWarning from mcp.types import ( CallToolRequestParams, CallToolResult, @@ -42,7 +43,7 @@ async def call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> assert params.arguments is not None text = params.arguments["text"] with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) + warnings.simplefilter("ignore", MCPDeprecationWarning) await ctx.session.send_log_message(level="info", data=f"echoing {text}", logger="echo") # pyright: ignore[reportDeprecated] return CallToolResult(content=[TextContent(text=text)]) diff --git a/tests/shared/test_peer.py b/tests/shared/test_peer.py index 691a6a7352..7c8009e68e 100644 --- a/tests/shared/test_peer.py +++ b/tests/shared/test_peer.py @@ -12,6 +12,7 @@ import pytest from mcp.shared.dispatcher import DispatchContext +from mcp.shared.exceptions import MCPDeprecationWarning from mcp.shared.peer import ClientPeer, dump_params from mcp.shared.transport_context import TransportContext from mcp.types import ( @@ -149,7 +150,7 @@ async def test_peer_list_roots_is_deprecated_sep_2577(): rec = _Recorder({"roots": []}) async with running_pair(direct_pair, server_on_request=rec.on_request) as (client, *_): peer = ClientPeer(client) - with pytest.warns(DeprecationWarning, match=r"`list_roots` is deprecated as of 2026-07-28 \(SEP-2577\)\."): + with pytest.warns(MCPDeprecationWarning, match=r"`list_roots` is deprecated as of 2026-07-28 \(SEP-2577\)\."): with anyio.fail_after(5): await peer.list_roots() # pyright: ignore[reportDeprecated] From a465678c42d79a920692c46d99789540e1c3e60c Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 20 Jun 2026 17:45:26 +0200 Subject: [PATCH 3/7] Deprecate only `Context.log` for logging, with a capability-level message The `debug`/`info`/`warning`/`error` convenience helpers delegate to `Context.log`, so deprecating `log` alone covers them - they warn through it instead of each carrying their own marker. Drop their `@deprecated` decorators and the now-stale `# pyright: ignore[reportDeprecated]` on their call sites. Reword every logging deprecation ("The logging capability is deprecated as of ...") so the message names the feature rather than the individual method, and widen the `filterwarnings` pattern to match the new wording. --- README.v2.md | 16 ++++++++-------- docs/migration.md | 2 +- .../mcp_everything_server/server.py | 10 +++++----- examples/snippets/servers/notifications.py | 8 ++++---- examples/snippets/servers/tool_progress.py | 4 ++-- pyproject.toml | 2 +- src/mcp/client/client.py | 2 +- src/mcp/client/session.py | 2 +- src/mcp/server/context.py | 2 +- src/mcp/server/mcpserver/context.py | 6 +----- src/mcp/server/session.py | 2 +- tests/interaction/mcpserver/test_context.py | 14 +++++++------- tests/interaction/mcpserver/test_tools.py | 2 +- tests/interaction/transports/test_flows.py | 2 +- .../transports/test_hosting_resume.py | 18 +++++++++--------- .../transports/test_streamable_http.py | 2 +- tests/server/mcpserver/test_server.py | 8 ++++---- 17 files changed, 49 insertions(+), 53 deletions(-) diff --git a/README.v2.md b/README.v2.md index e631745d76..33c045bff6 100644 --- a/README.v2.md +++ b/README.v2.md @@ -360,7 +360,7 @@ mcp = MCPServer(name="Progress Example") @mcp.tool() async def long_running_task(task_name: str, ctx: Context, steps: int = 5) -> str: """Execute a task with progress updates.""" - await ctx.info(f"Starting: {task_name}") # pyright: ignore[reportDeprecated] + await ctx.info(f"Starting: {task_name}") for i in range(steps): progress = (i + 1) / steps @@ -369,7 +369,7 @@ async def long_running_task(task_name: str, ctx: Context, steps: int = 5) -> str total=1.0, message=f"Step {i + 1}/{steps}", ) - await ctx.debug(f"Completed step {i + 1}") # pyright: ignore[reportDeprecated] + await ctx.debug(f"Completed step {i + 1}") return f"Task '{task_name}' completed" ``` @@ -707,7 +707,7 @@ mcp = MCPServer(name="Progress Example") @mcp.tool() async def long_running_task(task_name: str, ctx: Context, steps: int = 5) -> str: """Execute a task with progress updates.""" - await ctx.info(f"Starting: {task_name}") # pyright: ignore[reportDeprecated] + await ctx.info(f"Starting: {task_name}") for i in range(steps): progress = (i + 1) / steps @@ -716,7 +716,7 @@ async def long_running_task(task_name: str, ctx: Context, steps: int = 5) -> str total=1.0, message=f"Step {i + 1}/{steps}", ) - await ctx.debug(f"Completed step {i + 1}") # pyright: ignore[reportDeprecated] + await ctx.debug(f"Completed step {i + 1}") return f"Task '{task_name}' completed" ``` @@ -982,10 +982,10 @@ mcp = MCPServer(name="Notifications Example") async def process_data(data: str, ctx: Context) -> str: """Process data with logging.""" # Different log levels - await ctx.debug(f"Debug: Processing '{data}'") # pyright: ignore[reportDeprecated] - await ctx.info("Info: Starting processing") # pyright: ignore[reportDeprecated] - await ctx.warning("Warning: This is experimental") # pyright: ignore[reportDeprecated] - await ctx.error("Error: (This is just a demo)") # pyright: ignore[reportDeprecated] + await ctx.debug(f"Debug: Processing '{data}'") + await ctx.info("Info: Starting processing") + await ctx.warning("Warning: This is experimental") + await ctx.error("Error: (This is just a demo)") # Notify about resource changes await ctx.session.send_resource_list_changed() diff --git a/docs/migration.md b/docs/migration.md index 9dd103e1fb..1bcf93928b 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -1224,7 +1224,7 @@ The user-facing methods for these features now carry `typing_extensions.deprecat - Sampling: `ServerSession.create_message()`, `ClientPeer.sample()` - Roots: `ServerSession.list_roots()`, `ClientPeer.list_roots()`, `ClientSession.send_roots_list_changed()`, `Client.send_roots_list_changed()` -- Logging: `ServerSession.send_log_message()`, `ClientSession.set_logging_level()`, `Client.set_logging_level()`, and the `MCPServer` `Context` helpers `log()`, `debug()`, `info()`, `warning()`, `error()` +- Logging: `ServerSession.send_log_message()`, `ClientSession.set_logging_level()`, `Client.set_logging_level()`, and `MCPServer` `Context.log()`. The `Context.debug()` / `info()` / `warning()` / `error()` helpers delegate to `log()`, so they warn through it rather than carrying their own marker. 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: diff --git a/examples/servers/everything-server/mcp_everything_server/server.py b/examples/servers/everything-server/mcp_everything_server/server.py index 01baa56340..147ffbd6ba 100644 --- a/examples/servers/everything-server/mcp_everything_server/server.py +++ b/examples/servers/everything-server/mcp_everything_server/server.py @@ -143,13 +143,13 @@ def test_multiple_content_types() -> list[TextContent | ImageContent | EmbeddedR @mcp.tool() async def test_tool_with_logging(ctx: Context) -> str: """Tests tool that emits log messages during execution""" - await ctx.info("Tool execution started") # pyright: ignore[reportDeprecated] + await ctx.info("Tool execution started") await asyncio.sleep(0.05) - await ctx.info("Tool processing data") # pyright: ignore[reportDeprecated] + await ctx.info("Tool processing data") await asyncio.sleep(0.05) - await ctx.info("Tool execution completed") # pyright: ignore[reportDeprecated] + await ctx.info("Tool execution completed") return "Tool with logging executed successfully" @@ -314,13 +314,13 @@ def test_error_handling() -> str: @mcp.tool() async def test_reconnection(ctx: Context) -> str: """Tests SSE polling by closing stream mid-call (SEP-1699)""" - await ctx.info("Before disconnect") # pyright: ignore[reportDeprecated] + await ctx.info("Before disconnect") await ctx.close_sse_stream() await asyncio.sleep(0.2) # Wait for client to reconnect - await ctx.info("After reconnect") # pyright: ignore[reportDeprecated] + await ctx.info("After reconnect") return "Reconnection test completed" diff --git a/examples/snippets/servers/notifications.py b/examples/snippets/servers/notifications.py index 05c0fbf331..d6d903cc7f 100644 --- a/examples/snippets/servers/notifications.py +++ b/examples/snippets/servers/notifications.py @@ -7,10 +7,10 @@ async def process_data(data: str, ctx: Context) -> str: """Process data with logging.""" # Different log levels - await ctx.debug(f"Debug: Processing '{data}'") # pyright: ignore[reportDeprecated] - await ctx.info("Info: Starting processing") # pyright: ignore[reportDeprecated] - await ctx.warning("Warning: This is experimental") # pyright: ignore[reportDeprecated] - await ctx.error("Error: (This is just a demo)") # pyright: ignore[reportDeprecated] + await ctx.debug(f"Debug: Processing '{data}'") + await ctx.info("Info: Starting processing") + await ctx.warning("Warning: This is experimental") + await ctx.error("Error: (This is just a demo)") # Notify about resource changes await ctx.session.send_resource_list_changed() diff --git a/examples/snippets/servers/tool_progress.py b/examples/snippets/servers/tool_progress.py index 78703416af..376dbc5db8 100644 --- a/examples/snippets/servers/tool_progress.py +++ b/examples/snippets/servers/tool_progress.py @@ -6,7 +6,7 @@ @mcp.tool() async def long_running_task(task_name: str, ctx: Context, steps: int = 5) -> str: """Execute a task with progress updates.""" - await ctx.info(f"Starting: {task_name}") # pyright: ignore[reportDeprecated] + await ctx.info(f"Starting: {task_name}") for i in range(steps): progress = (i + 1) / steps @@ -15,6 +15,6 @@ async def long_running_task(task_name: str, ctx: Context, steps: int = 5) -> str total=1.0, message=f"Step {i + 1}/{steps}", ) - await ctx.debug(f"Completed step {i + 1}") # pyright: ignore[reportDeprecated] + await ctx.debug(f"Completed step {i + 1}") return f"Task '{task_name}' completed" diff --git a/pyproject.toml b/pyproject.toml index bd29744dc8..07bfff740e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -216,7 +216,7 @@ filterwarnings = [ # SEP-2577 deprecates the roots/sampling/logging methods; the SDK still calls # them internally (e.g. `ctx.debug` -> `log` -> `send_log_message`), so the # advisory warning is silenced. Tests asserting it opt back in with pytest.warns. - "ignore:`.*` is deprecated as of 2026-07-28 \\(SEP-2577\\).:mcp.MCPDeprecationWarning", + "ignore:.*is deprecated as of 2026-07-28 \\(SEP-2577\\).:mcp.MCPDeprecationWarning", ] [tool.markdown.lint] diff --git a/src/mcp/client/client.py b/src/mcp/client/client.py index 9d636b5215..d589d72d57 100644 --- a/src/mcp/client/client.py +++ b/src/mcp/client/client.py @@ -198,7 +198,7 @@ async def send_progress_notification( message=message, ) - @deprecated("`set_logging_level` is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) + @deprecated("The logging capability is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def set_logging_level(self, level: LoggingLevel, *, meta: RequestParamsMeta | None = None) -> EmptyResult: """Set the logging level on the server.""" return await self.session.set_logging_level(level=level, meta=meta) # pyright: ignore[reportDeprecated] diff --git a/src/mcp/client/session.py b/src/mcp/client/session.py index f7c682b461..044507ae93 100644 --- a/src/mcp/client/session.py +++ b/src/mcp/client/session.py @@ -386,7 +386,7 @@ async def send_progress_notification( ) ) - @deprecated("`set_logging_level` is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) + @deprecated("The logging capability is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def set_logging_level( self, level: types.LoggingLevel, diff --git a/src/mcp/server/context.py b/src/mcp/server/context.py index 0facbac2e1..eafb70d07c 100644 --- a/src/mcp/server/context.py +++ b/src/mcp/server/context.py @@ -93,7 +93,7 @@ def headers(self) -> Mapping[str, str] | None: """ return self.transport.headers - @deprecated("`log` is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) + @deprecated("The logging capability is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def log(self, level: LoggingLevel, data: Any, logger: str | None = None, *, meta: Meta | None = None) -> None: """Send a request-scoped `notifications/message` log entry. diff --git a/src/mcp/server/mcpserver/context.py b/src/mcp/server/mcpserver/context.py index 6a8d813314..4776197ff4 100644 --- a/src/mcp/server/mcpserver/context.py +++ b/src/mcp/server/mcpserver/context.py @@ -191,7 +191,7 @@ async def elicit_url( related_request_id=self.request_id, ) - @deprecated("`log` is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) + @deprecated("The logging capability is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def log( self, level: LoggingLevel, @@ -268,22 +268,18 @@ async def close_standalone_sse_stream(self) -> None: await self._request_context.close_standalone_sse_stream() # Convenience methods for common log levels - @deprecated("`debug` is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def debug(self, data: Any, *, logger_name: str | None = None) -> None: """Send a debug log message.""" await self.log("debug", data, logger_name=logger_name) # pyright: ignore[reportDeprecated] - @deprecated("`info` is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def info(self, data: Any, *, logger_name: str | None = None) -> None: """Send an info log message.""" await self.log("info", data, logger_name=logger_name) # pyright: ignore[reportDeprecated] - @deprecated("`warning` is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def warning(self, data: Any, *, logger_name: str | None = None) -> None: """Send a warning log message.""" await self.log("warning", data, logger_name=logger_name) # pyright: ignore[reportDeprecated] - @deprecated("`error` is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def error(self, data: Any, *, logger_name: str | None = None) -> None: """Send an error log message.""" await self.log("error", data, logger_name=logger_name) # pyright: ignore[reportDeprecated] diff --git a/src/mcp/server/session.py b/src/mcp/server/session.py index fd41772b26..df69d1ec17 100644 --- a/src/mcp/server/session.py +++ b/src/mcp/server/session.py @@ -127,7 +127,7 @@ def check_client_capability(self, capability: types.ClientCapabilities) -> bool: """Check if the client supports a specific capability.""" return self._connection.check_capability(capability) - @deprecated("`send_log_message` is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) + @deprecated("The logging capability is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def send_log_message( self, level: types.LoggingLevel, diff --git a/tests/interaction/mcpserver/test_context.py b/tests/interaction/mcpserver/test_context.py index f3ee3f52e4..fd7ef49d99 100644 --- a/tests/interaction/mcpserver/test_context.py +++ b/tests/interaction/mcpserver/test_context.py @@ -41,10 +41,10 @@ async def test_context_logging_helpers_send_log_notifications(connect: Connect) @mcp.tool() async def narrate(ctx: Context) -> str: - await ctx.debug("d") # pyright: ignore[reportDeprecated] - await ctx.info("i") # pyright: ignore[reportDeprecated] - await ctx.warning("w") # pyright: ignore[reportDeprecated] - await ctx.error("e") # pyright: ignore[reportDeprecated] + await ctx.debug("d") + await ctx.info("i") + await ctx.warning("w") + await ctx.error("e") return "done" async def collect(params: LoggingMessageNotificationParams) -> None: @@ -136,7 +136,7 @@ async def test_report_progress_without_a_progress_token_sends_nothing(connect: C @mcp.tool() async def mill(ctx: Context) -> str: await ctx.report_progress(1, 3) - await ctx.info("milling done") # pyright: ignore[reportDeprecated] + await ctx.info("milling done") return "milled" async def collect(message: IncomingMessage) -> None: @@ -250,8 +250,8 @@ async def test_set_logging_level_is_rejected_and_messages_are_never_filtered(con @mcp.tool() async def chatter(ctx: Context) -> str: - await ctx.debug("noise") # pyright: ignore[reportDeprecated] - await ctx.error("signal") # pyright: ignore[reportDeprecated] + await ctx.debug("noise") + await ctx.error("signal") return "done" async def collect(params: LoggingMessageNotificationParams) -> None: diff --git a/tests/interaction/mcpserver/test_tools.py b/tests/interaction/mcpserver/test_tools.py index 1314d85587..05135c1286 100644 --- a/tests/interaction/mcpserver/test_tools.py +++ b/tests/interaction/mcpserver/test_tools.py @@ -414,7 +414,7 @@ def doomed() -> str: async def grow(ctx: Context) -> str: mcp.add_tool(extra, name="extra") mcp.remove_tool("doomed") - await ctx.info("tool set changed") # pyright: ignore[reportDeprecated] + await ctx.info("tool set changed") return "mutated" async def collect(message: IncomingMessage) -> None: diff --git a/tests/interaction/transports/test_flows.py b/tests/interaction/transports/test_flows.py index e8081a7a1d..c428fe2d68 100644 --- a/tests/interaction/transports/test_flows.py +++ b/tests/interaction/transports/test_flows.py @@ -32,7 +32,7 @@ async def test_concurrent_clients_on_one_stateful_server_receive_only_their_own_ @mcp.tool() async def announce(label: str, ctx: Context) -> str: """Emit one info-level log carrying the caller's label, then return it.""" - await ctx.info(label) # pyright: ignore[reportDeprecated] + await ctx.info(label) return label received_a: list[object] = [] diff --git a/tests/interaction/transports/test_hosting_resume.py b/tests/interaction/transports/test_hosting_resume.py index b22df0ff2b..c7945d56c3 100644 --- a/tests/interaction/transports/test_hosting_resume.py +++ b/tests/interaction/transports/test_hosting_resume.py @@ -55,7 +55,7 @@ def _counting_server() -> MCPServer: async def count(ctx: Context, n: int) -> str: """Emit n log notifications related to this call, plus one unrelated resource update.""" for i in range(1, n + 1): - await ctx.info(f"tick {i}") # pyright: ignore[reportDeprecated] + await ctx.info(f"tick {i}") await ctx.session.send_resource_updated("file:///elsewhere.txt") return f"counted to {n}" @@ -138,10 +138,10 @@ async def test_get_with_last_event_id_replays_only_that_streams_missed_events() @mcp.tool() async def count(ctx: Context) -> str: """Emit one related notification, wait for the test, then emit two more plus an unrelated one.""" - await ctx.info("tick 1") # pyright: ignore[reportDeprecated] + await ctx.info("tick 1") await release.wait() - await ctx.info("tick 2") # pyright: ignore[reportDeprecated] - await ctx.info("tick 3") # pyright: ignore[reportDeprecated] + await ctx.info("tick 2") + await ctx.info("tick 3") await ctx.session.send_resource_updated("file:///elsewhere.txt") return "counted" @@ -219,7 +219,7 @@ async def hold(ctx: Context) -> str: """Signal start, wait for the test, signal completion.""" started.set() await release.wait() - await ctx.info("released") # pyright: ignore[reportDeprecated] + await ctx.info("released") finished.set() return "held" @@ -263,10 +263,10 @@ async def test_a_call_whose_stream_the_server_closes_is_resumed_by_the_client() @mcp.tool() async def interrupt(ctx: Context) -> str: """Emit, close this call's SSE stream, then emit again after the test releases the gate.""" - await ctx.info("before close") # pyright: ignore[reportDeprecated] + await ctx.info("before close") await ctx.close_sse_stream() await gate.wait() - await ctx.info("after close") # pyright: ignore[reportDeprecated] + await ctx.info("after close") done.set() return "resumed" @@ -321,9 +321,9 @@ async def test_a_captured_resumption_token_replays_missed_messages_on_a_new_conn @mcp.tool() async def hold(ctx: Context) -> str: """Emit one notification, wait for the test, emit another, return.""" - await ctx.info("first") # pyright: ignore[reportDeprecated] + await ctx.info("first") await release.wait() - await ctx.info("second") # pyright: ignore[reportDeprecated] + await ctx.info("second") return "done" async def on_token(token: str) -> None: diff --git a/tests/interaction/transports/test_streamable_http.py b/tests/interaction/transports/test_streamable_http.py index bf5a32f5ba..d38e2a0bb3 100644 --- a/tests/interaction/transports/test_streamable_http.py +++ b/tests/interaction/transports/test_streamable_http.py @@ -55,7 +55,7 @@ async def ask(ctx: Context) -> str: @mcp.tool() async def announce(ctx: Context) -> str: """Send one notification related to this request and one that is not.""" - await ctx.info("about to announce") # pyright: ignore[reportDeprecated] + await ctx.info("about to announce") await ctx.session.send_resource_updated("file:///watched.txt") return "announced" diff --git a/tests/server/mcpserver/test_server.py b/tests/server/mcpserver/test_server.py index d1816e6400..98d4a23261 100644 --- a/tests/server/mcpserver/test_server.py +++ b/tests/server/mcpserver/test_server.py @@ -1110,10 +1110,10 @@ async def test_context_logging(self): mcp = MCPServer() async def logging_tool(msg: str, ctx: Context) -> str: - await ctx.debug("Debug message") # pyright: ignore[reportDeprecated] - await ctx.info("Info message") # pyright: ignore[reportDeprecated] - await ctx.warning("Warning message") # pyright: ignore[reportDeprecated] - await ctx.error("Error message") # pyright: ignore[reportDeprecated] + await ctx.debug("Debug message") + await ctx.info("Info message") + await ctx.warning("Warning message") + await ctx.error("Error message") return f"Logged messages for {msg}" mcp.add_tool(logging_tool) From 8cfdd44010e1ff1c888f52cb64b5f55dab8ff2ae Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 20 Jun 2026 17:48:22 +0200 Subject: [PATCH 4/7] Restructure list_roots deprecation test to cover its branches on 3.14 The `with anyio.fail_after(...)` block was the last statement in the test and nested inside `pytest.warns`, so coverage.py flagged its `->exit` arc as a partial branch on 3.14. Move `fail_after` to the outer `with` and add a trailing assertion (the call reached the wire), matching the sibling `list_roots` tests. --- tests/shared/test_peer.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/shared/test_peer.py b/tests/shared/test_peer.py index 7c8009e68e..57e01cc2f7 100644 --- a/tests/shared/test_peer.py +++ b/tests/shared/test_peer.py @@ -150,9 +150,12 @@ async def test_peer_list_roots_is_deprecated_sep_2577(): rec = _Recorder({"roots": []}) async with running_pair(direct_pair, server_on_request=rec.on_request) as (client, *_): peer = ClientPeer(client) - with pytest.warns(MCPDeprecationWarning, match=r"`list_roots` is deprecated as of 2026-07-28 \(SEP-2577\)\."): - with anyio.fail_after(5): + with anyio.fail_after(5): + with pytest.warns( + MCPDeprecationWarning, match=r"`list_roots` is deprecated as of 2026-07-28 \(SEP-2577\)\." + ): await peer.list_roots() # pyright: ignore[reportDeprecated] + assert rec.seen[0][0] == "roots/list" def test_dump_params_merges_meta_over_model_meta(): From 7ee2e0d362b650092ecc6b8f425e54e2f94cfbdd Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 20 Jun 2026 18:11:50 +0200 Subject: [PATCH 5/7] Deprecate Connection.log for consistency with the logging capability Address PR review: `Connection.log()` sends the same `notifications/message` the logging capability covers and `Context.log`'s docstring steers users to it, so it now carries the same SEP-2577 `@deprecated` marker. Its two test call sites get `# pyright: ignore[reportDeprecated]`, and the migration guide lists it among the deprecated logging entry points. --- README.v2.md | 16 ++++++++-------- docs/migration.md | 2 +- .../mcp_everything_server/server.py | 10 +++++----- examples/snippets/servers/notifications.py | 8 ++++---- examples/snippets/servers/tool_progress.py | 4 ++-- src/mcp/server/connection.py | 4 +++- src/mcp/server/mcpserver/context.py | 4 ++++ tests/interaction/mcpserver/test_context.py | 14 +++++++------- tests/interaction/mcpserver/test_tools.py | 2 +- tests/interaction/transports/test_flows.py | 2 +- .../transports/test_hosting_resume.py | 18 +++++++++--------- .../transports/test_streamable_http.py | 2 +- tests/server/mcpserver/test_server.py | 8 ++++---- tests/server/test_connection.py | 4 ++-- 14 files changed, 52 insertions(+), 46 deletions(-) diff --git a/README.v2.md b/README.v2.md index 33c045bff6..e631745d76 100644 --- a/README.v2.md +++ b/README.v2.md @@ -360,7 +360,7 @@ mcp = MCPServer(name="Progress Example") @mcp.tool() async def long_running_task(task_name: str, ctx: Context, steps: int = 5) -> str: """Execute a task with progress updates.""" - await ctx.info(f"Starting: {task_name}") + await ctx.info(f"Starting: {task_name}") # pyright: ignore[reportDeprecated] for i in range(steps): progress = (i + 1) / steps @@ -369,7 +369,7 @@ async def long_running_task(task_name: str, ctx: Context, steps: int = 5) -> str total=1.0, message=f"Step {i + 1}/{steps}", ) - await ctx.debug(f"Completed step {i + 1}") + await ctx.debug(f"Completed step {i + 1}") # pyright: ignore[reportDeprecated] return f"Task '{task_name}' completed" ``` @@ -707,7 +707,7 @@ mcp = MCPServer(name="Progress Example") @mcp.tool() async def long_running_task(task_name: str, ctx: Context, steps: int = 5) -> str: """Execute a task with progress updates.""" - await ctx.info(f"Starting: {task_name}") + await ctx.info(f"Starting: {task_name}") # pyright: ignore[reportDeprecated] for i in range(steps): progress = (i + 1) / steps @@ -716,7 +716,7 @@ async def long_running_task(task_name: str, ctx: Context, steps: int = 5) -> str total=1.0, message=f"Step {i + 1}/{steps}", ) - await ctx.debug(f"Completed step {i + 1}") + await ctx.debug(f"Completed step {i + 1}") # pyright: ignore[reportDeprecated] return f"Task '{task_name}' completed" ``` @@ -982,10 +982,10 @@ mcp = MCPServer(name="Notifications Example") async def process_data(data: str, ctx: Context) -> str: """Process data with logging.""" # Different log levels - await ctx.debug(f"Debug: Processing '{data}'") - await ctx.info("Info: Starting processing") - await ctx.warning("Warning: This is experimental") - await ctx.error("Error: (This is just a demo)") + await ctx.debug(f"Debug: Processing '{data}'") # pyright: ignore[reportDeprecated] + await ctx.info("Info: Starting processing") # pyright: ignore[reportDeprecated] + await ctx.warning("Warning: This is experimental") # pyright: ignore[reportDeprecated] + await ctx.error("Error: (This is just a demo)") # pyright: ignore[reportDeprecated] # Notify about resource changes await ctx.session.send_resource_list_changed() diff --git a/docs/migration.md b/docs/migration.md index 1bcf93928b..de2056e609 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -1224,7 +1224,7 @@ The user-facing methods for these features now carry `typing_extensions.deprecat - Sampling: `ServerSession.create_message()`, `ClientPeer.sample()` - Roots: `ServerSession.list_roots()`, `ClientPeer.list_roots()`, `ClientSession.send_roots_list_changed()`, `Client.send_roots_list_changed()` -- Logging: `ServerSession.send_log_message()`, `ClientSession.set_logging_level()`, `Client.set_logging_level()`, and `MCPServer` `Context.log()`. The `Context.debug()` / `info()` / `warning()` / `error()` helpers delegate to `log()`, so they warn through it rather than carrying their own marker. +- Logging: `ServerSession.send_log_message()`, `Connection.log()`, `ClientSession.set_logging_level()`, `Client.set_logging_level()`, and the `MCPServer` `Context` helpers `log()`, `debug()`, `info()`, `warning()`, `error()` 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: diff --git a/examples/servers/everything-server/mcp_everything_server/server.py b/examples/servers/everything-server/mcp_everything_server/server.py index 147ffbd6ba..01baa56340 100644 --- a/examples/servers/everything-server/mcp_everything_server/server.py +++ b/examples/servers/everything-server/mcp_everything_server/server.py @@ -143,13 +143,13 @@ def test_multiple_content_types() -> list[TextContent | ImageContent | EmbeddedR @mcp.tool() async def test_tool_with_logging(ctx: Context) -> str: """Tests tool that emits log messages during execution""" - await ctx.info("Tool execution started") + await ctx.info("Tool execution started") # pyright: ignore[reportDeprecated] await asyncio.sleep(0.05) - await ctx.info("Tool processing data") + await ctx.info("Tool processing data") # pyright: ignore[reportDeprecated] await asyncio.sleep(0.05) - await ctx.info("Tool execution completed") + await ctx.info("Tool execution completed") # pyright: ignore[reportDeprecated] return "Tool with logging executed successfully" @@ -314,13 +314,13 @@ def test_error_handling() -> str: @mcp.tool() async def test_reconnection(ctx: Context) -> str: """Tests SSE polling by closing stream mid-call (SEP-1699)""" - await ctx.info("Before disconnect") + await ctx.info("Before disconnect") # pyright: ignore[reportDeprecated] await ctx.close_sse_stream() await asyncio.sleep(0.2) # Wait for client to reconnect - await ctx.info("After reconnect") + await ctx.info("After reconnect") # pyright: ignore[reportDeprecated] return "Reconnection test completed" diff --git a/examples/snippets/servers/notifications.py b/examples/snippets/servers/notifications.py index d6d903cc7f..05c0fbf331 100644 --- a/examples/snippets/servers/notifications.py +++ b/examples/snippets/servers/notifications.py @@ -7,10 +7,10 @@ async def process_data(data: str, ctx: Context) -> str: """Process data with logging.""" # Different log levels - await ctx.debug(f"Debug: Processing '{data}'") - await ctx.info("Info: Starting processing") - await ctx.warning("Warning: This is experimental") - await ctx.error("Error: (This is just a demo)") + await ctx.debug(f"Debug: Processing '{data}'") # pyright: ignore[reportDeprecated] + await ctx.info("Info: Starting processing") # pyright: ignore[reportDeprecated] + await ctx.warning("Warning: This is experimental") # pyright: ignore[reportDeprecated] + await ctx.error("Error: (This is just a demo)") # pyright: ignore[reportDeprecated] # Notify about resource changes await ctx.session.send_resource_list_changed() diff --git a/examples/snippets/servers/tool_progress.py b/examples/snippets/servers/tool_progress.py index 376dbc5db8..78703416af 100644 --- a/examples/snippets/servers/tool_progress.py +++ b/examples/snippets/servers/tool_progress.py @@ -6,7 +6,7 @@ @mcp.tool() async def long_running_task(task_name: str, ctx: Context, steps: int = 5) -> str: """Execute a task with progress updates.""" - await ctx.info(f"Starting: {task_name}") + await ctx.info(f"Starting: {task_name}") # pyright: ignore[reportDeprecated] for i in range(steps): progress = (i + 1) / steps @@ -15,6 +15,6 @@ async def long_running_task(task_name: str, ctx: Context, steps: int = 5) -> str total=1.0, message=f"Step {i + 1}/{steps}", ) - await ctx.debug(f"Completed step {i + 1}") + await ctx.debug(f"Completed step {i + 1}") # pyright: ignore[reportDeprecated] return f"Task '{task_name}' completed" diff --git a/src/mcp/server/connection.py b/src/mcp/server/connection.py index e0f406a200..8a8034e37e 100644 --- a/src/mcp/server/connection.py +++ b/src/mcp/server/connection.py @@ -20,9 +20,10 @@ import anyio from pydantic import BaseModel +from typing_extensions import deprecated from mcp.shared.dispatcher import CallOptions, Outbound -from mcp.shared.exceptions import NoBackChannelError +from mcp.shared.exceptions import MCPDeprecationWarning, NoBackChannelError from mcp.shared.peer import Meta, dump_params from mcp.types import ( ClientCapabilities, @@ -209,6 +210,7 @@ async def ping(self, *, meta: Meta | None = None, opts: CallOptions | None = Non """ await self.send_raw_request("ping", dump_params(None, meta), opts) + @deprecated("The logging capability is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def log(self, level: LoggingLevel, data: Any, logger: str | None = None, *, meta: Meta | None = None) -> None: """Send a `notifications/message` log entry on the standalone stream. Best-effort.""" params: dict[str, Any] = {"level": level, "data": data} diff --git a/src/mcp/server/mcpserver/context.py b/src/mcp/server/mcpserver/context.py index 4776197ff4..0bf0b7ebfd 100644 --- a/src/mcp/server/mcpserver/context.py +++ b/src/mcp/server/mcpserver/context.py @@ -268,18 +268,22 @@ async def close_standalone_sse_stream(self) -> None: await self._request_context.close_standalone_sse_stream() # Convenience methods for common log levels + @deprecated("The logging capability is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def debug(self, data: Any, *, logger_name: str | None = None) -> None: """Send a debug log message.""" await self.log("debug", data, logger_name=logger_name) # pyright: ignore[reportDeprecated] + @deprecated("The logging capability is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def info(self, data: Any, *, logger_name: str | None = None) -> None: """Send an info log message.""" await self.log("info", data, logger_name=logger_name) # pyright: ignore[reportDeprecated] + @deprecated("The logging capability is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def warning(self, data: Any, *, logger_name: str | None = None) -> None: """Send a warning log message.""" await self.log("warning", data, logger_name=logger_name) # pyright: ignore[reportDeprecated] + @deprecated("The logging capability is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def error(self, data: Any, *, logger_name: str | None = None) -> None: """Send an error log message.""" await self.log("error", data, logger_name=logger_name) # pyright: ignore[reportDeprecated] diff --git a/tests/interaction/mcpserver/test_context.py b/tests/interaction/mcpserver/test_context.py index fd7ef49d99..f3ee3f52e4 100644 --- a/tests/interaction/mcpserver/test_context.py +++ b/tests/interaction/mcpserver/test_context.py @@ -41,10 +41,10 @@ async def test_context_logging_helpers_send_log_notifications(connect: Connect) @mcp.tool() async def narrate(ctx: Context) -> str: - await ctx.debug("d") - await ctx.info("i") - await ctx.warning("w") - await ctx.error("e") + await ctx.debug("d") # pyright: ignore[reportDeprecated] + await ctx.info("i") # pyright: ignore[reportDeprecated] + await ctx.warning("w") # pyright: ignore[reportDeprecated] + await ctx.error("e") # pyright: ignore[reportDeprecated] return "done" async def collect(params: LoggingMessageNotificationParams) -> None: @@ -136,7 +136,7 @@ async def test_report_progress_without_a_progress_token_sends_nothing(connect: C @mcp.tool() async def mill(ctx: Context) -> str: await ctx.report_progress(1, 3) - await ctx.info("milling done") + await ctx.info("milling done") # pyright: ignore[reportDeprecated] return "milled" async def collect(message: IncomingMessage) -> None: @@ -250,8 +250,8 @@ async def test_set_logging_level_is_rejected_and_messages_are_never_filtered(con @mcp.tool() async def chatter(ctx: Context) -> str: - await ctx.debug("noise") - await ctx.error("signal") + await ctx.debug("noise") # pyright: ignore[reportDeprecated] + await ctx.error("signal") # pyright: ignore[reportDeprecated] return "done" async def collect(params: LoggingMessageNotificationParams) -> None: diff --git a/tests/interaction/mcpserver/test_tools.py b/tests/interaction/mcpserver/test_tools.py index 05135c1286..1314d85587 100644 --- a/tests/interaction/mcpserver/test_tools.py +++ b/tests/interaction/mcpserver/test_tools.py @@ -414,7 +414,7 @@ def doomed() -> str: async def grow(ctx: Context) -> str: mcp.add_tool(extra, name="extra") mcp.remove_tool("doomed") - await ctx.info("tool set changed") + await ctx.info("tool set changed") # pyright: ignore[reportDeprecated] return "mutated" async def collect(message: IncomingMessage) -> None: diff --git a/tests/interaction/transports/test_flows.py b/tests/interaction/transports/test_flows.py index c428fe2d68..e8081a7a1d 100644 --- a/tests/interaction/transports/test_flows.py +++ b/tests/interaction/transports/test_flows.py @@ -32,7 +32,7 @@ async def test_concurrent_clients_on_one_stateful_server_receive_only_their_own_ @mcp.tool() async def announce(label: str, ctx: Context) -> str: """Emit one info-level log carrying the caller's label, then return it.""" - await ctx.info(label) + await ctx.info(label) # pyright: ignore[reportDeprecated] return label received_a: list[object] = [] diff --git a/tests/interaction/transports/test_hosting_resume.py b/tests/interaction/transports/test_hosting_resume.py index c7945d56c3..b22df0ff2b 100644 --- a/tests/interaction/transports/test_hosting_resume.py +++ b/tests/interaction/transports/test_hosting_resume.py @@ -55,7 +55,7 @@ def _counting_server() -> MCPServer: async def count(ctx: Context, n: int) -> str: """Emit n log notifications related to this call, plus one unrelated resource update.""" for i in range(1, n + 1): - await ctx.info(f"tick {i}") + await ctx.info(f"tick {i}") # pyright: ignore[reportDeprecated] await ctx.session.send_resource_updated("file:///elsewhere.txt") return f"counted to {n}" @@ -138,10 +138,10 @@ async def test_get_with_last_event_id_replays_only_that_streams_missed_events() @mcp.tool() async def count(ctx: Context) -> str: """Emit one related notification, wait for the test, then emit two more plus an unrelated one.""" - await ctx.info("tick 1") + await ctx.info("tick 1") # pyright: ignore[reportDeprecated] await release.wait() - await ctx.info("tick 2") - await ctx.info("tick 3") + await ctx.info("tick 2") # pyright: ignore[reportDeprecated] + await ctx.info("tick 3") # pyright: ignore[reportDeprecated] await ctx.session.send_resource_updated("file:///elsewhere.txt") return "counted" @@ -219,7 +219,7 @@ async def hold(ctx: Context) -> str: """Signal start, wait for the test, signal completion.""" started.set() await release.wait() - await ctx.info("released") + await ctx.info("released") # pyright: ignore[reportDeprecated] finished.set() return "held" @@ -263,10 +263,10 @@ async def test_a_call_whose_stream_the_server_closes_is_resumed_by_the_client() @mcp.tool() async def interrupt(ctx: Context) -> str: """Emit, close this call's SSE stream, then emit again after the test releases the gate.""" - await ctx.info("before close") + await ctx.info("before close") # pyright: ignore[reportDeprecated] await ctx.close_sse_stream() await gate.wait() - await ctx.info("after close") + await ctx.info("after close") # pyright: ignore[reportDeprecated] done.set() return "resumed" @@ -321,9 +321,9 @@ async def test_a_captured_resumption_token_replays_missed_messages_on_a_new_conn @mcp.tool() async def hold(ctx: Context) -> str: """Emit one notification, wait for the test, emit another, return.""" - await ctx.info("first") + await ctx.info("first") # pyright: ignore[reportDeprecated] await release.wait() - await ctx.info("second") + await ctx.info("second") # pyright: ignore[reportDeprecated] return "done" async def on_token(token: str) -> None: diff --git a/tests/interaction/transports/test_streamable_http.py b/tests/interaction/transports/test_streamable_http.py index d38e2a0bb3..bf5a32f5ba 100644 --- a/tests/interaction/transports/test_streamable_http.py +++ b/tests/interaction/transports/test_streamable_http.py @@ -55,7 +55,7 @@ async def ask(ctx: Context) -> str: @mcp.tool() async def announce(ctx: Context) -> str: """Send one notification related to this request and one that is not.""" - await ctx.info("about to announce") + await ctx.info("about to announce") # pyright: ignore[reportDeprecated] await ctx.session.send_resource_updated("file:///watched.txt") return "announced" diff --git a/tests/server/mcpserver/test_server.py b/tests/server/mcpserver/test_server.py index 98d4a23261..d1816e6400 100644 --- a/tests/server/mcpserver/test_server.py +++ b/tests/server/mcpserver/test_server.py @@ -1110,10 +1110,10 @@ async def test_context_logging(self): mcp = MCPServer() async def logging_tool(msg: str, ctx: Context) -> str: - await ctx.debug("Debug message") - await ctx.info("Info message") - await ctx.warning("Warning message") - await ctx.error("Error message") + await ctx.debug("Debug message") # pyright: ignore[reportDeprecated] + await ctx.info("Info message") # pyright: ignore[reportDeprecated] + await ctx.warning("Warning message") # pyright: ignore[reportDeprecated] + await ctx.error("Error message") # pyright: ignore[reportDeprecated] return f"Logged messages for {msg}" mcp.add_tool(logging_tool) diff --git a/tests/server/test_connection.py b/tests/server/test_connection.py index e7ec874e32..e5d60994c9 100644 --- a/tests/server/test_connection.py +++ b/tests/server/test_connection.py @@ -206,7 +206,7 @@ async def test_connection_ping_sends_ping_on_standalone(): async def test_connection_log_sends_logging_message_notification(): out = StubOutbound() conn = Connection(out, has_standalone_channel=True) - await conn.log("info", {"k": "v"}, logger="my.logger") + await conn.log("info", {"k": "v"}, logger="my.logger") # pyright: ignore[reportDeprecated] method, params = out.notifications[0] assert method == "notifications/message" assert params is not None @@ -219,7 +219,7 @@ async def test_connection_log_sends_logging_message_notification(): async def test_connection_log_with_meta_includes_meta_in_params(): out = StubOutbound() conn = Connection(out, has_standalone_channel=True) - await conn.log("info", "x", meta={"traceId": "abc"}) + await conn.log("info", "x", meta={"traceId": "abc"}) # pyright: ignore[reportDeprecated] _, params = out.notifications[0] assert params is not None assert params["_meta"] == {"traceId": "abc"} From 24cb7b086e282b13f7cf07e2da547aae9d189f45 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 20 Jun 2026 18:14:20 +0200 Subject: [PATCH 6/7] List the lowlevel Context.log in the SEP-2577 deprecation note The migration guide's Logging bullet named only the `MCPServer` `Context.log()`, but the lowlevel `mcp.server.context.Context.log()` is a distinct public class that this PR also decorates. Add it so users of that Context find their method. --- docs/migration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/migration.md b/docs/migration.md index de2056e609..a6a0066bd6 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -1224,7 +1224,7 @@ The user-facing methods for these features now carry `typing_extensions.deprecat - Sampling: `ServerSession.create_message()`, `ClientPeer.sample()` - Roots: `ServerSession.list_roots()`, `ClientPeer.list_roots()`, `ClientSession.send_roots_list_changed()`, `Client.send_roots_list_changed()` -- Logging: `ServerSession.send_log_message()`, `Connection.log()`, `ClientSession.set_logging_level()`, `Client.set_logging_level()`, and the `MCPServer` `Context` helpers `log()`, `debug()`, `info()`, `warning()`, `error()` +- Logging: `ServerSession.send_log_message()`, `Connection.log()`, `ClientSession.set_logging_level()`, `Client.set_logging_level()`, `mcp.server.context.Context.log()` (the lowlevel `Context`), and the `MCPServer` `Context` helpers `log()`, `debug()`, `info()`, `warning()`, `error()` 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: From b3041311d6c810bbf51cb0c5a4e76a49c3628576 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 20 Jun 2026 18:18:52 +0200 Subject: [PATCH 7/7] Use capability-level deprecation messages for roots and sampling The logging deprecations name the feature ("The logging capability is deprecated as of ..."); roots and sampling still named the individual method. Switch `list_roots`, `send_roots_list_changed`, `sample`, and `create_message` to the same capability-level wording so every SEP-2577 warning is consistent. --- src/mcp/client/client.py | 2 +- src/mcp/client/session.py | 2 +- src/mcp/server/session.py | 8 ++++---- src/mcp/shared/peer.py | 8 ++++---- tests/shared/test_peer.py | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/mcp/client/client.py b/src/mcp/client/client.py index d589d72d57..978c5f5be2 100644 --- a/src/mcp/client/client.py +++ b/src/mcp/client/client.py @@ -316,7 +316,7 @@ async def list_tools(self, *, cursor: str | None = None, meta: RequestParamsMeta """List available tools from the server.""" return await self.session.list_tools(params=PaginatedRequestParams(cursor=cursor, _meta=meta)) - @deprecated("`send_roots_list_changed` is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) + @deprecated("The roots capability is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def send_roots_list_changed(self) -> None: """Send a notification that the roots list has changed.""" # TODO(Marcelo): Currently, there is no way for the server to handle this. We should add support. diff --git a/src/mcp/client/session.py b/src/mcp/client/session.py index 044507ae93..4b24e98b1d 100644 --- a/src/mcp/client/session.py +++ b/src/mcp/client/session.py @@ -551,7 +551,7 @@ async def list_tools(self, *, params: types.PaginatedRequestParams | None = None return result - @deprecated("`send_roots_list_changed` is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) + @deprecated("The roots capability is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def send_roots_list_changed(self) -> None: """Send a roots/list_changed notification.""" await self.send_notification(types.RootsListChangedNotification()) diff --git a/src/mcp/server/session.py b/src/mcp/server/session.py index df69d1ec17..5aad5602ac 100644 --- a/src/mcp/server/session.py +++ b/src/mcp/server/session.py @@ -156,7 +156,7 @@ async def send_resource_updated(self, uri: str | AnyUrl) -> None: ) @overload - @deprecated("`create_message` is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) + @deprecated("The sampling capability is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def create_message( self, messages: list[types.SamplingMessage], @@ -176,7 +176,7 @@ async def create_message( ... @overload - @deprecated("`create_message` is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) + @deprecated("The sampling capability is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def create_message( self, messages: list[types.SamplingMessage], @@ -195,7 +195,7 @@ async def create_message( """Overload: With tools, returns array-capable content.""" ... - @deprecated("`create_message` is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) + @deprecated("The sampling capability is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def create_message( self, messages: list[types.SamplingMessage], @@ -272,7 +272,7 @@ async def create_message( metadata=metadata_obj, ) - @deprecated("`list_roots` is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) + @deprecated("The roots capability is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def list_roots(self) -> types.ListRootsResult: """Send a roots/list request.""" if self._stateless: diff --git a/src/mcp/shared/peer.py b/src/mcp/shared/peer.py index 99e5fb2a01..ddf5c1c8ce 100644 --- a/src/mcp/shared/peer.py +++ b/src/mcp/shared/peer.py @@ -85,7 +85,7 @@ async def notify(self, method: str, params: Mapping[str, Any] | None) -> None: await self._outbound.notify(method, params) @overload - @deprecated("`sample` is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) + @deprecated("The sampling capability is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def sample( self, messages: list[SamplingMessage], @@ -103,7 +103,7 @@ async def sample( opts: CallOptions | None = None, ) -> CreateMessageResult: ... @overload - @deprecated("`sample` is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) + @deprecated("The sampling capability is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def sample( self, messages: list[SamplingMessage], @@ -120,7 +120,7 @@ async def sample( meta: Meta | None = None, opts: CallOptions | None = None, ) -> CreateMessageResultWithTools: ... - @deprecated("`sample` is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) + @deprecated("The sampling capability is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def sample( self, messages: list[SamplingMessage], @@ -200,7 +200,7 @@ async def elicit_url( result = await self.send_raw_request("elicitation/create", dump_params(params, meta), opts) return ElicitResult.model_validate(result, by_name=False) - @deprecated("`list_roots` is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) + @deprecated("The roots capability is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning) async def list_roots(self, *, meta: Meta | None = None, opts: CallOptions | None = None) -> ListRootsResult: """Send a `roots/list` request. diff --git a/tests/shared/test_peer.py b/tests/shared/test_peer.py index 57e01cc2f7..d17af88520 100644 --- a/tests/shared/test_peer.py +++ b/tests/shared/test_peer.py @@ -152,7 +152,7 @@ async def test_peer_list_roots_is_deprecated_sep_2577(): peer = ClientPeer(client) with anyio.fail_after(5): with pytest.warns( - MCPDeprecationWarning, match=r"`list_roots` is deprecated as of 2026-07-28 \(SEP-2577\)\." + MCPDeprecationWarning, match=r"The roots capability is deprecated as of 2026-07-28 \(SEP-2577\)\." ): await peer.list_roots() # pyright: ignore[reportDeprecated] assert rec.seen[0][0] == "roots/list"