Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 32 additions & 13 deletions docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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`
Expand Down
45 changes: 0 additions & 45 deletions src/mcp/shared/progress.py

This file was deleted.

113 changes: 0 additions & 113 deletions tests/shared/test_progress_notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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 \
Expand Down