From 9f8de1974d7dede344fa5203d859d3941065caab Mon Sep 17 00:00:00 2001 From: Max Isbey <224885523+maxisbey@users.noreply.github.com> Date: Wed, 18 Feb 2026 13:04:54 +0000 Subject: [PATCH] refactor: remove unused mcp.shared.progress module Remove the ProgressContext, Progress, and progress() context manager from mcp.shared.progress. This module had zero real-world adoption - every project that implements progress notifications uses either Context.report_progress() or session.send_progress_notification() directly. The module also had an unfixed related_request_id bug (notifications silently dropped on Streamable HTTP transport), which further confirms it was never used in production. - Delete src/mcp/shared/progress.py - Remove test_progress_context_manager test - Update migration guide to document removal and alternatives Github-Issue: #2021 --- docs/migration.md | 45 +++++--- src/mcp/shared/progress.py | 45 -------- tests/shared/test_progress_notifications.py | 113 -------------------- 3 files changed, 32 insertions(+), 171 deletions(-) delete mode 100644 src/mcp/shared/progress.py diff --git a/docs/migration.md b/docs/migration.md index 17fd92bd0..631683693 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -371,7 +371,7 @@ async def handle_call_tool(ctx: ServerRequestContext, params: CallToolRequestPar server = Server("my-server", on_call_tool=handle_call_tool) ``` -### `RequestContext` and `ProgressContext` type parameters simplified +### `RequestContext` type parameters simplified The `RequestContext` class has been split to separate shared fields from server-specific fields. The shared `RequestContext` now only takes 1 type parameter (the session type) instead of 3. @@ -380,40 +380,59 @@ The `RequestContext` class has been split to separate shared fields from server- - Type parameters reduced from `RequestContext[SessionT, LifespanContextT, RequestT]` to `RequestContext[SessionT]` - Server-specific fields (`lifespan_context`, `experimental`, `request`, `close_sse_stream`, `close_standalone_sse_stream`) moved to new `ServerRequestContext` class in `mcp.server.context` -**`ProgressContext` changes:** - -- Type parameters reduced from `ProgressContext[SendRequestT, SendNotificationT, SendResultT, ReceiveRequestT, ReceiveNotificationT]` to `ProgressContext[SessionT]` - **Before (v1):** ```python from mcp.client.session import ClientSession from mcp.shared.context import RequestContext, LifespanContextT, RequestT -from mcp.shared.progress import ProgressContext # RequestContext with 3 type parameters ctx: RequestContext[ClientSession, LifespanContextT, RequestT] - -# ProgressContext with 5 type parameters -progress_ctx: ProgressContext[SendRequestT, SendNotificationT, SendResultT, ReceiveRequestT, ReceiveNotificationT] ``` **After (v2):** ```python from mcp.client.context import ClientRequestContext -from mcp.client.session import ClientSession from mcp.server.context import ServerRequestContext, LifespanContextT, RequestT -from mcp.shared.progress import ProgressContext # For client-side context (sampling, elicitation, list_roots callbacks) ctx: ClientRequestContext # For server-specific context with lifespan and request types server_ctx: ServerRequestContext[LifespanContextT, RequestT] +``` + +### `ProgressContext` and `progress()` context manager removed + +The `mcp.shared.progress` module (`ProgressContext`, `Progress`, and the `progress()` context manager) has been removed. This module had no real-world adoption — all users send progress notifications via `Context.report_progress()` or `session.send_progress_notification()` directly. + +**Before:** + +```python +from mcp.shared.progress import progress -# ProgressContext with 1 type parameter -progress_ctx: ProgressContext[ClientSession] +with progress(ctx, total=100) as p: + await p.progress(25) +``` + +**After — use `Context.report_progress()` (recommended):** + +```python +@server.tool() +async def my_tool(x: int, ctx: Context) -> str: + await ctx.report_progress(25, 100) + return "done" +``` + +**After — use `session.send_progress_notification()` (low-level):** + +```python +await session.send_progress_notification( + progress_token=progress_token, + progress=25, + total=100, +) ``` ### Resource URI type changed from `AnyUrl` to `str` diff --git a/src/mcp/shared/progress.py b/src/mcp/shared/progress.py deleted file mode 100644 index 510bd8163..000000000 --- a/src/mcp/shared/progress.py +++ /dev/null @@ -1,45 +0,0 @@ -from collections.abc import Generator -from contextlib import contextmanager -from dataclasses import dataclass, field -from typing import Generic - -from pydantic import BaseModel - -from mcp.shared._context import RequestContext, SessionT -from mcp.types import ProgressToken - - -class Progress(BaseModel): - progress: float - total: float | None - - -@dataclass -class ProgressContext(Generic[SessionT]): - session: SessionT - progress_token: ProgressToken - total: float | None - current: float = field(default=0.0, init=False) - - async def progress(self, amount: float, message: str | None = None) -> None: - self.current += amount - - await self.session.send_progress_notification( - self.progress_token, self.current, total=self.total, message=message - ) - - -@contextmanager -def progress( - ctx: RequestContext[SessionT], - total: float | None = None, -) -> Generator[ProgressContext[SessionT], None]: - progress_token = ctx.meta.get("progress_token") if ctx.meta else None - if progress_token is None: # pragma: no cover - raise ValueError("No progress token provided") - - progress_ctx = ProgressContext(ctx.session, progress_token, total) - try: - yield progress_ctx - finally: - pass diff --git a/tests/shared/test_progress_notifications.py b/tests/shared/test_progress_notifications.py index 6b87774c0..aad9e5d43 100644 --- a/tests/shared/test_progress_notifications.py +++ b/tests/shared/test_progress_notifications.py @@ -10,9 +10,7 @@ from mcp.server.lowlevel import NotificationOptions from mcp.server.models import InitializationOptions from mcp.server.session import ServerSession -from mcp.shared._context import RequestContext from mcp.shared.message import SessionMessage -from mcp.shared.progress import progress from mcp.shared.session import RequestResponder @@ -198,117 +196,6 @@ async def handle_client_message( assert server_progress_updates[2]["progress"] == 1.0 -@pytest.mark.anyio -async def test_progress_context_manager(): - """Test client using progress context manager for sending progress notifications.""" - # Create memory streams for client/server - server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](5) - client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](5) - - # Track progress updates - server_progress_updates: list[dict[str, Any]] = [] - - progress_token = None - - # Register progress handler - async def handle_progress(ctx: ServerRequestContext, params: types.ProgressNotificationParams) -> None: - server_progress_updates.append( - { - "token": params.progress_token, - "progress": params.progress, - "total": params.total, - "message": params.message, - } - ) - - server = Server(name="ProgressContextTestServer", on_progress=handle_progress) - - # Run server session to receive progress updates - async def run_server(): - # Create a server session - async with ServerSession( - client_to_server_receive, - server_to_client_send, - InitializationOptions( - server_name="ProgressContextTestServer", - server_version="0.1.0", - capabilities=server.get_capabilities(NotificationOptions(), {}), - ), - ) as server_session: - async for message in server_session.incoming_messages: - try: - await server._handle_message(message, server_session, {}) - except Exception as e: # pragma: no cover - raise e - - # Client message handler - async def handle_client_message( - message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception, - ) -> None: - if isinstance(message, Exception): # pragma: no cover - raise message - - # run client session - async with ( - ClientSession( - server_to_client_receive, - client_to_server_send, - message_handler=handle_client_message, - ) as client_session, - anyio.create_task_group() as tg, - ): - tg.start_soon(run_server) - - await client_session.initialize() - - progress_token = "client_token_456" - - # Create request context - request_context = RequestContext( - request_id="test-request", - session=client_session, - meta={"progress_token": progress_token}, - ) - - # Utilize progress context manager - with progress(request_context, total=100) as p: - await p.progress(10, message="Loading configuration...") - await p.progress(30, message="Connecting to database...") - await p.progress(40, message="Fetching data...") - await p.progress(20, message="Processing results...") - - # Wait for all messages to be processed - await anyio.sleep(0.5) - tg.cancel_scope.cancel() - - # Verify progress updates were received by server - assert len(server_progress_updates) == 4 - - # first update - assert server_progress_updates[0]["token"] == progress_token - assert server_progress_updates[0]["progress"] == 10 - assert server_progress_updates[0]["total"] == 100 - assert server_progress_updates[0]["message"] == "Loading configuration..." - - # second update - assert server_progress_updates[1]["token"] == progress_token - assert server_progress_updates[1]["progress"] == 40 - assert server_progress_updates[1]["total"] == 100 - assert server_progress_updates[1]["message"] == "Connecting to database..." - - # third update - assert server_progress_updates[2]["token"] == progress_token - assert server_progress_updates[2]["progress"] == 80 - assert server_progress_updates[2]["total"] == 100 - assert server_progress_updates[2]["message"] == "Fetching data..." - - # final update - assert server_progress_updates[3]["token"] == progress_token - assert server_progress_updates[3]["progress"] == 100 - assert server_progress_updates[3]["total"] == 100 - assert server_progress_updates[3]["message"] == "Processing results..." - - @pytest.mark.anyio async def test_progress_callback_exception_logging(): """Test that exceptions in progress callbacks are logged and \