Skip to content

Commit d367b17

Browse files
committed
Remove the client-side related_request_id surface
ServerMessageMetadata.related_request_id exists for the server's streamable-HTTP transport to route outbound messages onto the originating request's SSE stream. No client transport has ever serialized it, so ClientSession's related_request_id parameter and ServerMessageMetadata acceptance were dead inheritance from the shared v1 BaseSession. - send_notification loses its related_request_id parameter - send_request's metadata narrows to ClientMessageMetadata | None (resumption hints, the live part) - the isinstance(dispatcher) downcasts those parameters forced are gone Progress and response correlation (progressToken in params, JSON-RPC id) are payload-level mechanisms and are unaffected.
1 parent 8e6dfb3 commit d367b17

3 files changed

Lines changed: 10 additions & 79 deletions

File tree

docs/migration.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1187,6 +1187,7 @@ Behavior changes:
11871187
- **Error responses with a null `id`** — the JSON-RPC shape for a peer reporting a parse error — are now dropped with a debug log. v1 surfaced them to `message_handler` as an `MCPError`.
11881188
- **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`. One carve-out: a callback that raises pydantic's `ValidationError` is still answered with `INVALID_PARAMS` (`"Invalid request parameters"`, empty `data`) because the dispatcher cannot distinguish it from inbound-params validation — this conflation is pre-existing v1 behavior, and a revisit is pending.
11891189
- **`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.
1190+
- **`send_notification` no longer takes `related_request_id`, and `send_request` no longer accepts `ServerMessageMetadata`.** The hint was never serialized by any client transport in v1 or v2 — it exists for the server's streamable-HTTP stream routing. Progress and response correlation via `progressToken` and the request id is unaffected.
11901191

11911192
`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.
11921193

src/mcp/client/session.py

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from mcp.shared.dispatcher import CallOptions, DispatchContext, Dispatcher
1919
from mcp.shared.exceptions import MCPError
2020
from mcp.shared.jsonrpc_dispatcher import JSONRPCDispatcher
21-
from mcp.shared.message import ClientMessageMetadata, MessageMetadata, ServerMessageMetadata, SessionMessage
21+
from mcp.shared.message import ClientMessageMetadata, SessionMessage
2222
from mcp.shared.session import ProgressFnT, RequestResponder
2323
from mcp.shared.transport_context import TransportContext
2424
from mcp.shared.version import SUPPORTED_PROTOCOL_VERSIONS
@@ -202,15 +202,13 @@ async def send_request(
202202
request: types.ClientRequest,
203203
result_type: type[ReceiveResultT],
204204
request_read_timeout_seconds: float | None = None,
205-
metadata: MessageMetadata = None,
205+
metadata: ClientMessageMetadata | None = None,
206206
progress_callback: ProgressFnT | None = None,
207207
) -> ReceiveResultT:
208208
"""Send a request and wait for its typed result.
209209
210210
Args:
211-
metadata: Transport hints: `ClientMessageMetadata` resumption fields
212-
(streamable HTTP), or a `ServerMessageMetadata.related_request_id`
213-
routing the message onto the originating request's stream.
211+
metadata: Streamable HTTP resumption hints.
214212
215213
Raises:
216214
MCPError: Error response, read timeout, or connection closed.
@@ -224,38 +222,21 @@ async def send_request(
224222
opts["timeout"] = timeout
225223
if progress_callback is not None:
226224
opts["on_progress"] = progress_callback
227-
related_request_id: types.RequestId | None = None
228-
if isinstance(metadata, ClientMessageMetadata):
225+
if metadata is not None:
229226
if metadata.resumption_token is not None:
230227
opts["resumption_token"] = metadata.resumption_token
231228
if metadata.on_resumption_token_update is not None:
232229
opts["on_resumption_token"] = metadata.on_resumption_token_update
233-
elif isinstance(metadata, ServerMessageMetadata):
234-
related_request_id = metadata.related_request_id
235230
if method == "initialize":
236231
# The spec forbids cancelling initialize.
237232
opts["cancel_on_abandon"] = False
238-
if related_request_id is not None and isinstance(self._dispatcher, JSONRPCDispatcher):
239-
# Only JSON-RPC dispatchers have per-request streams to route onto.
240-
raw = await self._dispatcher.send_raw_request(
241-
method, data.get("params"), opts, _related_request_id=related_request_id
242-
)
243-
else:
244-
raw = await self._dispatcher.send_raw_request(method, data.get("params"), opts)
233+
raw = await self._dispatcher.send_raw_request(method, data.get("params"), opts)
245234
return result_type.model_validate(raw, by_name=False)
246235

247-
async def send_notification(
248-
self,
249-
notification: types.ClientNotification,
250-
related_request_id: types.RequestId | None = None,
251-
) -> None:
236+
async def send_notification(self, notification: types.ClientNotification) -> None:
252237
"""Send a one-way notification. Usable before entering the context manager."""
253238
data = notification.model_dump(by_alias=True, mode="json", exclude_none=True)
254-
# `is not None`: request ids are opaque and 0 is valid.
255-
if related_request_id is not None and isinstance(self._dispatcher, JSONRPCDispatcher):
256-
await self._dispatcher.notify(data["method"], data.get("params"), _related_request_id=related_request_id)
257-
else:
258-
await self._dispatcher.notify(data["method"], data.get("params"))
239+
await self._dispatcher.notify(data["method"], data.get("params"))
259240

260241
async def initialize(self) -> types.InitializeResult:
261242
sampling = (

tests/client/test_session.py

Lines changed: 2 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from mcp.shared._context import RequestContext
1515
from mcp.shared.direct_dispatcher import create_direct_dispatcher_pair
1616
from mcp.shared.dispatcher import CallOptions, DispatchContext, OnNotify, OnRequest
17-
from mcp.shared.message import ServerMessageMetadata, SessionMessage
17+
from mcp.shared.message import SessionMessage
1818
from mcp.shared.session import RequestResponder
1919
from mcp.shared.transport_context import TransportContext
2020
from mcp.shared.version import SUPPORTED_PROTOCOL_VERSIONS
@@ -978,8 +978,7 @@ async def server_on_notify(
978978
results.append(await session.send_ping(meta=None))
979979
# Server-to-client: direct dispatch delivers ping with no params member (no _meta injection).
980980
assert await server_side.send_raw_request("ping", None) == {}
981-
# related_request_id is JSON-RPC plumbing; other dispatchers send the notification without it.
982-
await session.send_notification(types.RootsListChangedNotification(), related_request_id=7)
981+
await session.send_notification(types.RootsListChangedNotification())
983982
server_side.close()
984983
assert results == [types.EmptyResult()]
985984
assert notified == ["notifications/roots/list_changed"]
@@ -1082,53 +1081,3 @@ async def notify(self, method: str, params: Mapping[str, Any] | None) -> None:
10821081
assert scope.cancelled_caught
10831082
# The failed enter must not leave the session half-entered.
10841083
assert session._task_group is None
1085-
1086-
1087-
@pytest.mark.anyio
1088-
async def test_send_request_with_server_metadata_routes_related_request_id():
1089-
"""ServerMessageMetadata.related_request_id is threaded onto the outgoing message."""
1090-
async with raw_client_session() as (session, to_client, from_client):
1091-
async with anyio.create_task_group() as tg:
1092-
1093-
async def call() -> None:
1094-
await session.send_request(
1095-
types.PingRequest(), types.EmptyResult, metadata=ServerMessageMetadata(related_request_id=3)
1096-
)
1097-
1098-
tg.start_soon(call)
1099-
out = await from_client.receive()
1100-
assert isinstance(out.metadata, ServerMessageMetadata)
1101-
assert out.metadata.related_request_id == 3
1102-
assert isinstance(out.message, JSONRPCRequest)
1103-
await to_client.send(SessionMessage(JSONRPCResponse(jsonrpc="2.0", id=out.message.id, result={})))
1104-
1105-
1106-
@pytest.mark.anyio
1107-
async def test_send_notification_with_related_request_id_attaches_metadata():
1108-
"""A related_request_id on a notification rides the originating request's stream."""
1109-
async with raw_client_session() as (session, _to_client, from_client):
1110-
await session.send_notification(
1111-
types.ProgressNotification(
1112-
params=types.ProgressNotificationParams(progress_token=1, progress=0.5),
1113-
),
1114-
related_request_id=4,
1115-
)
1116-
out = await from_client.receive()
1117-
assert isinstance(out.metadata, ServerMessageMetadata)
1118-
assert out.metadata.related_request_id == 4
1119-
1120-
1121-
@pytest.mark.anyio
1122-
async def test_send_notification_with_related_request_id_zero_attaches_metadata():
1123-
"""`related_request_id=0` still attaches metadata: 0 is a valid request id, so the session checks
1124-
`is not None`, not truthiness (regression pin). Wire-level: only the sent `SessionMessage` shows it."""
1125-
async with raw_client_session() as (session, _to_client, from_client):
1126-
await session.send_notification(
1127-
types.ProgressNotification(
1128-
params=types.ProgressNotificationParams(progress_token=1, progress=0.5),
1129-
),
1130-
related_request_id=0,
1131-
)
1132-
out = await from_client.receive()
1133-
assert isinstance(out.metadata, ServerMessageMetadata)
1134-
assert out.metadata.related_request_id == 0

0 commit comments

Comments
 (0)