Skip to content

Commit 88b76c6

Browse files
committed
Narrow message_handler's parameter to notifications and exceptions
The client's message_handler is only ever called with a validated ServerNotification or a transport-level Exception; server-initiated requests are answered by the typed callbacks. The RequestResponder arm of the MessageHandlerFnT union was a v1 leftover with a typing-only stub behind it, so every handler had to annotate a case the session never delivers. Type the parameter as an exported IncomingMessage alias (ServerNotification | Exception) in mcp.client, delete the dead RequestResponder stub, and drop the now-empty mcp.shared.session compatibility module (ProgressFnT already lives in mcp.shared.dispatcher).
1 parent 0cb920f commit 88b76c6

26 files changed

Lines changed: 63 additions & 143 deletions

docs/client/callbacks.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ Two more. Neither declares anything.
135135

136136
`logging_callback` receives every `notifications/message` a server sends, as `LoggingMessageNotificationParams` (`level`, `logger`, `data`). Protocol logging is itself deprecated by the 2026-07-28 spec (**[Logging](../handlers/logging.md)** has what to do instead), so this callback exists for the servers that still emit it.
137137

138-
`message_handler` is the catch-all: every server notification reaches it (as well as its specific callback), and on a stream-backed transport so does every transport-level `Exception`. The one pattern worth knowing is `if isinstance(message, Exception): raise message`, so a broken connection fails loudly instead of vanishing.
138+
`message_handler` is the catch-all: every server notification reaches it (as well as its specific callback), and on a stream-backed transport so does every transport-level `Exception`. Annotate its parameter with `IncomingMessage` (`ServerNotification | Exception`, exported from `mcp.client`). The one pattern worth knowing is `if isinstance(message, Exception): raise message`, so a broken connection fails loudly instead of vanishing.
139139

140140
## Recap
141141

docs/migration.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1651,7 +1651,9 @@ Behavior changes:
16511651
- **`send_notification` no longer takes `related_request_id`, and `send_request` no longer accepts `ServerMessageMetadata`.** No client transport ever serialized these hints; progress and response correlation via `progressToken` and the request id is unaffected.
16521652
- **Client callbacks now receive `mcp.client.ClientRequestContext`** (its `request_id` is always populated); the `mcp.shared.context.RequestContext` generic is deleted. Annotations spelled `RequestContext[ClientSession, Any]` become `ClientRequestContext` (details in [`RequestContext` type parameters simplified](#requestcontext-type-parameters-simplified)).
16531653

1654-
`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. `RequestResponder.respond()` no longer exists, and neither do the cancellation-tracking members (`cancel()`, the `cancelled` and `in_flight` properties, the `on_complete` constructor argument) or `BaseSession._in_flight`; inbound cancellation is handled by `JSONRPCDispatcher`.
1654+
- **`message_handler` no longer receives requests.** Server-initiated requests are answered by the typed callbacks (`sampling_callback`, `elicitation_callback`, `list_roots_callback`), so the handler's parameter is now `IncomingMessage = ServerNotification | Exception`, exported from `mcp.client`. Replace the hand-written v1 union `RequestResponder[ServerRequest, ClientResult] | ServerNotification | Exception` with `IncomingMessage`; `RequestResponder` is gone (below), so the old annotation no longer imports.
1655+
1656+
The `mcp.shared.session` module is gone. `RequestResponder` is removed — `respond()`, the cancellation-tracking members (`cancel()`, the `cancelled` and `in_flight` properties, the `on_complete` constructor argument) and `BaseSession._in_flight` have no replacement; inbound cancellation is handled by `JSONRPCDispatcher`. `ProgressFnT` now lives only in `mcp.shared.dispatcher`, and `RequestId` in `mcp_types`.
16551657

16561658
### Experimental Tasks support removed
16571659

examples/stories/standalone_get/client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import anyio
44
import mcp_types as types
55

6-
from mcp.client import Client
6+
from mcp.client import Client, IncomingMessage
77
from stories._harness import Target, run_client
88

99

@@ -13,7 +13,7 @@ async def main(target: Target, *, mode: str = "auto") -> None:
1313
received: list[types.ResourceListChangedNotification] = []
1414
seen = anyio.Event()
1515

16-
async def on_message(message: object) -> None:
16+
async def on_message(message: IncomingMessage) -> None:
1717
if isinstance(message, types.ResourceListChangedNotification):
1818
received.append(message)
1919
seen.set()

examples/stories/stickynotes/client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import mcp_types as types
55
from mcp_types.version import HANDSHAKE_PROTOCOL_VERSIONS
66

7-
from mcp.client import Client, ClientRequestContext
7+
from mcp.client import Client, ClientRequestContext, IncomingMessage
88
from stories._harness import Target, run_client
99

1010

@@ -18,7 +18,7 @@ async def on_elicit(context: ClientRequestContext, params: types.ElicitRequestPa
1818
return types.ElicitResult(action="cancel")
1919
return types.ElicitResult(action="accept", content={"confirm": answer == "confirm"})
2020

21-
async def on_message(message: object) -> None:
21+
async def on_message(message: IncomingMessage) -> None:
2222
if isinstance(message, types.ResourceListChangedNotification):
2323
list_changed.set()
2424

src/mcp/client/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
UnexpectedClaimedResult,
2121
advertise,
2222
)
23-
from mcp.client.session import ClientSession
23+
from mcp.client.session import ClientSession, IncomingMessage
2424

2525
__all__ = [
2626
"CacheConfig",
@@ -32,6 +32,7 @@
3232
"ClientExtension",
3333
"ClientRequestContext",
3434
"ClientSession",
35+
"IncomingMessage",
3536
"InMemoryResponseCacheStore",
3637
"InputRequiredRoundsExceededError",
3738
"NotificationBinding",

src/mcp/client/__main__.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,10 @@
99
import mcp_types as types
1010

1111
from mcp.client._transport import ReadStream, WriteStream
12-
from mcp.client.session import ClientSession
12+
from mcp.client.session import ClientSession, IncomingMessage
1313
from mcp.client.sse import sse_client
1414
from mcp.client.stdio import StdioServerParameters, stdio_client
1515
from mcp.shared.message import SessionMessage
16-
from mcp.shared.session import RequestResponder
1716

1817
if not sys.warnoptions:
1918
warnings.simplefilter("ignore")
@@ -22,9 +21,7 @@
2221
logger = logging.getLogger("client")
2322

2423

25-
async def message_handler(
26-
message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception,
27-
) -> None:
24+
async def message_handler(message: IncomingMessage) -> None:
2825
if isinstance(message, Exception):
2926
logger.error("Error: %s", message)
3027
return

src/mcp/client/client.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
ClientRequestContext,
5353
ClientSession,
5454
ElicitationFnT,
55+
IncomingMessage,
5556
ListRootsFnT,
5657
LoggingFnT,
5758
MessageHandlerFnT,
@@ -68,7 +69,6 @@
6869
from mcp.shared.exceptions import MCPDeprecationWarning, MCPError
6970
from mcp.shared.extension import validate_extension_identifier
7071
from mcp.shared.jsonrpc_dispatcher import JSONRPCDispatcher
71-
from mcp.shared.session import RequestResponder
7272
from mcp.shared.subscriptions import event_to_notification
7373

7474
logger = logging.getLogger(__name__)
@@ -155,9 +155,7 @@ def _strip_userinfo(url: str) -> str:
155155
def _evicting_message_handler(cache: ClientResponseCache, user_handler: MessageHandlerFnT | None) -> MessageHandlerFnT:
156156
"""Wrap the session message handler with cache eviction on server notifications."""
157157

158-
async def handler(
159-
message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception,
160-
) -> None:
158+
async def handler(message: IncomingMessage) -> None:
161159
if isinstance(message, types.ServerNotification):
162160
try:
163161
await cache.evict_for_notification(message)

src/mcp/client/session.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from functools import reduce
77
from operator import or_
88
from types import TracebackType
9-
from typing import Annotated, Any, Final, Literal, Protocol, cast, overload
9+
from typing import Annotated, Any, Final, Literal, Protocol, TypeAlias, cast, overload
1010

1111
import anyio
1212
import anyio.abc
@@ -53,7 +53,6 @@
5353
)
5454
from mcp.shared.jsonrpc_dispatcher import JSONRPCDispatcher, cancelled_request_id_from_params
5555
from mcp.shared.message import ClientMessageMetadata, SessionMessage
56-
from mcp.shared.session import RequestResponder
5756
from mcp.shared.subscriptions import SUBSCRIPTION_ID_META_KEY, event_from_wire
5857
from mcp.shared.transport_context import TransportContext
5958

@@ -172,16 +171,15 @@ class LoggingFnT(Protocol):
172171
async def __call__(self, params: types.LoggingMessageNotificationParams) -> None: ... # pragma: no branch
173172

174173

174+
IncomingMessage: TypeAlias = types.ServerNotification | Exception
175+
"""What `message_handler` receives: every server notification, plus transport-level exceptions."""
176+
177+
175178
class MessageHandlerFnT(Protocol):
176-
async def __call__(
177-
self,
178-
message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception,
179-
) -> None: ... # pragma: no branch
179+
async def __call__(self, message: IncomingMessage) -> None: ... # pragma: no branch
180180

181181

182-
async def _default_message_handler(
183-
message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception,
184-
) -> None:
182+
async def _default_message_handler(message: IncomingMessage) -> None:
185183
await anyio.lowlevel.checkpoint()
186184

187185

src/mcp/client/session_group.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525
from mcp.client.stdio import StdioServerParameters
2626
from mcp.client.streamable_http import streamable_http_client
2727
from mcp.shared._httpx_utils import create_mcp_http_client
28+
from mcp.shared.dispatcher import ProgressFnT
2829
from mcp.shared.exceptions import MCPError
29-
from mcp.shared.session import ProgressFnT
3030

3131

3232
class SseServerParameters(BaseModel):

src/mcp/shared/session.py

Lines changed: 0 additions & 22 deletions
This file was deleted.

0 commit comments

Comments
 (0)