Skip to content

Commit a30cdc3

Browse files
committed
Cover the remaining client-session branches and document the migration
Adds tests for ServerMessageMetadata routing, related-request-id notifications, and params-absent inbound requests over direct dispatch, plus the migration-guide entry for the ClientSession dispatcher swap.
1 parent d985c55 commit a30cdc3

2 files changed

Lines changed: 58 additions & 1 deletion

File tree

docs/migration.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1164,7 +1164,23 @@ In practice, replace direct `ServerSession` use with `Server.run(read_stream, wr
11641164

11651165
`BaseSession._in_flight` and the `RequestResponder` members that supported it (`cancel()`, the `cancelled` and `in_flight` properties, the `on_complete` constructor argument, and the internal `CancelScope`) have been removed. These existed to let `ServerSession` cancel a handler when a `CancelledNotification` arrived; `ServerSession` no longer drives a receive loop, so they were dead code. Inbound-cancellation handling for the server now lives in `JSONRPCDispatcher`.
11661166

1167-
`BaseSession` is still used by `ClientSession`, which never relied on these members. `RequestResponder.respond()` is unchanged.
1167+
`BaseSession` itself has since been removed entirely; see the next section.
1168+
1169+
### `ClientSession` now runs on `JSONRPCDispatcher`; `BaseSession` removed
1170+
1171+
`ClientSession` keeps its public surface — the `(read_stream, write_stream, ...)` constructor, every typed method, manual `initialize()`, and the async context-manager lifecycle — but the v1 receive loop (`BaseSession`) underneath it is gone. A new `ClientSession.from_dispatcher(dispatcher, ...)` constructor accepts a pre-built dispatcher (for example a `DirectDispatcher` for in-process embedding).
1172+
1173+
Behavior changes:
1174+
1175+
- **Request ids count from 1** (previously 0). Progress tokens, which reuse the request id, shift the same way. Ids are opaque per JSON-RPC; do not assign meaning to them.
1176+
- **Timeouts**: the error message is now `Request 'tools/call' timed out` (previously `Timed out while waiting for response to CallToolRequest. Waited N seconds.`), and a timed-out or abandoned request is followed by `notifications/cancelled` on the wire, so the server stops the handler instead of leaving it running. The `initialize` request is never cancelled this way, and requests sent with resumption metadata are also exempt so they stay resumable.
1177+
- **Server-initiated requests run concurrently.** Sampling/elicitation/roots callbacks no longer serialize the receive loop: a slow callback does not block other traffic, a callback may itself send requests without deadlocking, and a server's `notifications/cancelled` now actually interrupts the callback (the request is then answered with an error response).
1178+
- **Notification callbacks are concurrent.** `logging_callback` and `message_handler` start in arrival order, but there is no completion-before-response guarantee (matching the TypeScript, C#, and Go SDKs). Callbacks that need strict sequencing must coordinate themselves.
1179+
- **Unknown-id responses are ignored**, as the spec asks. v1 surfaced them to `message_handler` as a `RuntimeError`; nothing is surfaced now.
1180+
- **A raising request callback** is answered with `code=0` and the exception text. v1 flattened every callback exception to `INVALID_PARAMS`. Callbacks that want a specific error response should return `ErrorData` (unchanged) or raise `MCPError`.
1181+
- **`send_request` before entering the context manager** raises `RuntimeError` immediately; v1 wrote to the transport and hung until the timeout. `send_notification` before entry still works.
1182+
1183+
`mcp.shared.session` is now a compatibility module: `ProgressFnT` is re-exported (its home is `mcp.shared.dispatcher`), and `RequestResponder` remains as a typing-only stub so `MessageHandlerFnT` annotations keep importing — it has been unreachable at runtime since the server-side swap. `RequestResponder.respond()` no longer exists.
11681184

11691185
### Experimental Tasks support removed
11701186

tests/client/test_session.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -900,9 +900,50 @@ async def server_on_notify(
900900
await tg.start(server_side.run, server_on_request, server_on_notify)
901901
async with session:
902902
results.append(await session.send_ping(meta=None))
903+
# Server-to-client direction: direct dispatch delivers ping with no
904+
# params member at all (no _meta injection outside JSON-RPC).
905+
assert await server_side.send_raw_request("ping", None) == {}
903906
# related_request_id routing is JSON-RPC plumbing; on other
904907
# dispatchers the notification is sent without it.
905908
await session.send_notification(types.RootsListChangedNotification(), related_request_id=7)
906909
server_side.close()
907910
assert results == [types.EmptyResult()]
908911
assert notified == ["notifications/roots/list_changed"]
912+
913+
914+
@pytest.mark.anyio
915+
async def test_send_request_with_server_metadata_routes_related_request_id():
916+
"""ServerMessageMetadata.related_request_id is threaded onto the outgoing message."""
917+
from mcp.shared.message import ServerMessageMetadata
918+
919+
async with raw_client_session() as (session, to_client, from_client):
920+
async with anyio.create_task_group() as tg:
921+
922+
async def call() -> None:
923+
await session.send_request(
924+
types.PingRequest(), types.EmptyResult, metadata=ServerMessageMetadata(related_request_id=3)
925+
)
926+
927+
tg.start_soon(call)
928+
out = await from_client.receive()
929+
assert isinstance(out.metadata, ServerMessageMetadata)
930+
assert out.metadata.related_request_id == 3
931+
assert isinstance(out.message, JSONRPCRequest)
932+
await to_client.send(SessionMessage(JSONRPCResponse(jsonrpc="2.0", id=out.message.id, result={})))
933+
934+
935+
@pytest.mark.anyio
936+
async def test_send_notification_with_related_request_id_attaches_metadata():
937+
"""A related_request_id on a notification rides the originating request's stream."""
938+
from mcp.shared.message import ServerMessageMetadata
939+
940+
async with raw_client_session() as (session, _to_client, from_client):
941+
await session.send_notification(
942+
types.ProgressNotification(
943+
params=types.ProgressNotificationParams(progress_token=1, progress=0.5),
944+
),
945+
related_request_id=4,
946+
)
947+
out = await from_client.receive()
948+
assert isinstance(out.metadata, ServerMessageMetadata)
949+
assert out.metadata.related_request_id == 4

0 commit comments

Comments
 (0)