Skip to content

Commit ceabb7d

Browse files
committed
refactor(client): rename message_handler to message_callback for naming consistency
Closes #2588. The five user-facing callback fields on ClientConfig / ClientSession / ServerConnectionParams were inconsistently named (four used *_callback, one used *_handler). Rename message_handler -> message_callback across: src/mcp/client/client.py ClientConfig field + forwarding call src/mcp/client/session.py ClientSession.__init__ param + internal _message_handler -> _message_callback; related docstring and log messages updated src/mcp/client/session_group.py ServerConnectionParams field + forwarding call src/mcp/client/__main__.py CLI entry point src/mcp/client/subscriptions.py docstring src/mcp/shared/jsonrpc_dispatcher.py docstring Type alias MessageHandlerFnT is intentionally not renamed (separate naming layer). Test usages updated across the codebase. Since this lands on the v2 rewrite on main, a breaking rename is appropriate without a deprecation shim. 4383 tests pass; ruff lint and format clean.
1 parent 6f114fc commit ceabb7d

32 files changed

Lines changed: 138 additions & 134 deletions

examples/stories/standalone_get/client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99

1010
async def main(target: Target, *, mode: str = "auto") -> None:
11-
# `message_handler` is constructor-only on `Client`, so the event it sets
11+
# `message_callback` is constructor-only on `Client`, so the event it sets
1212
# has to exist before the connection does.
1313
received: list[types.ResourceListChangedNotification] = []
1414
seen = anyio.Event()
@@ -18,7 +18,7 @@ async def on_message(message: object) -> None:
1818
received.append(message)
1919
seen.set()
2020

21-
async with Client(target, mode=mode, message_handler=on_message) as client:
21+
async with Client(target, mode=mode, message_callback=on_message) as client:
2222
before = await client.list_resources()
2323
assert len(before.resources) >= 1, before
2424

examples/stories/stickynotes/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ async def on_message(message: object) -> None:
2222
if isinstance(message, types.ResourceListChangedNotification):
2323
list_changed.set()
2424

25-
async with Client(target, mode=mode, elicitation_callback=on_elicit, message_handler=on_message) as client:
25+
async with Client(target, mode=mode, elicitation_callback=on_elicit, message_callback=on_message) as client:
2626
legacy = client.protocol_version in HANDSHAKE_PROTOCOL_VERSIONS
2727

2828
# Add two notes.

src/mcp/client/__main__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
logger = logging.getLogger("client")
2323

2424

25-
async def message_handler(
25+
async def message_callback(
2626
message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception,
2727
) -> None:
2828
if isinstance(message, Exception):
@@ -40,7 +40,7 @@ async def run_session(
4040
async with ClientSession(
4141
read_stream,
4242
write_stream,
43-
message_handler=message_handler,
43+
message_callback=message_callback,
4444
client_info=client_info,
4545
) as session:
4646
logger.info("Initializing session")

src/mcp/client/client.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ def _strip_userinfo(url: str) -> str:
152152
return url[:start] + authority.rpartition("@")[2] + url[end:]
153153

154154

155-
def _evicting_message_handler(cache: ClientResponseCache, user_handler: MessageHandlerFnT | None) -> MessageHandlerFnT:
155+
def _evicting_message_callback(cache: ClientResponseCache, user_handler: MessageHandlerFnT | None) -> MessageHandlerFnT:
156156
"""Wrap the session message handler with cache eviction on server notifications."""
157157

158158
async def handler(
@@ -166,7 +166,7 @@ async def handler(
166166
if user_handler is not None:
167167
await user_handler(message)
168168
else:
169-
# Mirrors ClientSession's default handler (session._default_message_handler).
169+
# Mirrors ClientSession's default handler (session._default_message_callback).
170170
await anyio.lowlevel.checkpoint()
171171

172172
return handler
@@ -316,7 +316,7 @@ async def main():
316316
"""Callback for handling logging notifications."""
317317

318318
# TODO(Marcelo): Why do we have both "callback" and "handler"?
319-
message_handler: MessageHandlerFnT | None = None
319+
message_callback: MessageHandlerFnT | None = None
320320
"""Callback for handling raw messages."""
321321

322322
client_info: Implementation | None = None
@@ -417,17 +417,17 @@ def __post_init__(self) -> None:
417417
async def _build_session(self, exit_stack: AsyncExitStack) -> ClientSession:
418418
"""Enter the resolved connector and return an un-entered ClientSession."""
419419
dispatcher = await self._connect(exit_stack, self.mode, self.raise_exceptions)
420-
message_handler = self.message_handler
420+
message_callback = self.message_callback
421421
if self._response_cache is not None:
422-
message_handler = _evicting_message_handler(self._response_cache, self.message_handler)
422+
message_callback = _evicting_message_callback(self._response_cache, self.message_callback)
423423
return ClientSession(
424424
dispatcher=dispatcher,
425425
read_timeout_seconds=self.read_timeout_seconds,
426426
sampling_callback=self.sampling_callback,
427427
sampling_capabilities=self.sampling_capabilities,
428428
list_roots_callback=self.list_roots_callback,
429429
logging_callback=self.logging_callback,
430-
message_handler=message_handler,
430+
message_callback=message_callback,
431431
client_info=self.client_info,
432432
elicitation_callback=self.elicitation_callback,
433433
extensions=self._folded_extensions.ad,

src/mcp/client/session.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ async def __call__(
163163
) -> None: ... # pragma: no branch
164164

165165

166-
async def _default_message_handler(
166+
async def _default_message_callback(
167167
message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception,
168168
) -> None:
169169
await anyio.lowlevel.checkpoint()
@@ -315,7 +315,7 @@ class ClientSession:
315315
`dispatcher=`), enter as an async context manager, then call
316316
`initialize()`. The dispatcher owns the receive loop and request
317317
correlation; this class owns the typed MCP layer and the constructor
318-
callbacks. Transport `Exception` items reach `message_handler` only when
318+
callbacks. Transport `Exception` items reach `message_callback` only when
319319
the session builds its own dispatcher from a stream pair.
320320
321321
Extension `result_claims` fold into tools/call parsing at `adopt()`;
@@ -331,7 +331,7 @@ def __init__(
331331
elicitation_callback: ElicitationFnT | None = None,
332332
list_roots_callback: ListRootsFnT | None = None,
333333
logging_callback: LoggingFnT | None = None,
334-
message_handler: MessageHandlerFnT | None = None,
334+
message_callback: MessageHandlerFnT | None = None,
335335
client_info: types.Implementation | None = None,
336336
*,
337337
sampling_capabilities: types.SamplingCapability | None = None,
@@ -355,7 +355,7 @@ def __init__(
355355
self._elicitation_callback = elicitation_callback or _default_elicitation_callback
356356
self._list_roots_callback = list_roots_callback or _default_list_roots_callback
357357
self._logging_callback = logging_callback or _default_logging_callback
358-
self._message_handler = message_handler or _default_message_handler
358+
self._message_callback = message_callback or _default_message_callback
359359
self._tool_output_schemas: dict[str, dict[str, Any] | None] = {}
360360
self._x_mcp_header_maps: dict[str, dict[tuple[str, ...], str]] = {}
361361
self._initialize_result: types.InitializeResult | None = None
@@ -370,7 +370,7 @@ def __init__(
370370
raise ValueError("pass read_stream/write_stream or dispatcher, not both")
371371
self._dispatcher: Dispatcher[Any] = dispatcher
372372
if isinstance(dispatcher, JSONRPCDispatcher) and dispatcher.on_stream_exception is None:
373-
# Route transport-level Exception items into message_handler — only
373+
# Route transport-level Exception items into message_callback — only
374374
# stream-backed dispatchers carry these; DirectDispatcher has none.
375375
# Don't clobber a caller-supplied hook.
376376
# TODO(L78): this leaves a bound-method ref on the dispatcher after the
@@ -1367,7 +1367,7 @@ def _intercept_notification(self, method: str, params: Mapping[str, Any] | None)
13671367
async def _on_notify(
13681368
self, dctx: DispatchContext[TransportContext], method: str, params: Mapping[str, Any] | None
13691369
) -> None:
1370-
"""Route a server notification: validate, run the typed callback, tee to message_handler."""
1370+
"""Route a server notification: validate, run the typed callback, tee to message_callback."""
13711371
# Same fallback as `_on_request`: covers pre-handshake and stateless.
13721372
version = self._negotiated_version or "2025-11-25"
13731373
try:
@@ -1403,16 +1403,16 @@ async def _on_notify(
14031403
try:
14041404
if isinstance(notification, types.LoggingMessageNotification):
14051405
await self._logging_callback(notification.params)
1406-
await self._message_handler(notification)
1406+
await self._message_callback(notification)
14071407
except Exception:
14081408
# Contain here, not in the dispatcher: DirectDispatcher awaits this
14091409
# handler inline in the peer's notify() call, so a raising callback
14101410
# would otherwise fail the peer's send. A raising logging_callback
1411-
# skips the message_handler tee for that notification (v1 parity).
1411+
# skips the message_callback tee for that notification (v1 parity).
14121412
logger.exception("notification callback for %r raised", method)
14131413

14141414
async def _on_stream_exception(self, exc: Exception) -> None:
1415-
"""Deliver a transport-level fault to message_handler via a spawned task.
1415+
"""Deliver a transport-level fault to message_callback via a spawned task.
14161416
14171417
Running the handler inline would park the dispatcher's read loop and
14181418
deadlock handlers that await session I/O.
@@ -1422,6 +1422,6 @@ async def _on_stream_exception(self, exc: Exception) -> None:
14221422

14231423
async def _deliver_stream_exception(self, exc: Exception) -> None:
14241424
try:
1425-
await self._message_handler(exc)
1425+
await self._message_callback(exc)
14261426
except Exception:
1427-
logger.exception("message_handler raised on transport exception")
1427+
logger.exception("message_callback raised on transport exception")

src/mcp/client/session_group.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ class ClientSessionParameters:
7878
elicitation_callback: ElicitationFnT | None = None
7979
list_roots_callback: ListRootsFnT | None = None
8080
logging_callback: LoggingFnT | None = None
81-
message_handler: MessageHandlerFnT | None = None
81+
message_callback: MessageHandlerFnT | None = None
8282
client_info: types.Implementation | None = None
8383

8484

@@ -347,7 +347,7 @@ async def _establish_session(
347347
elicitation_callback=session_params.elicitation_callback,
348348
list_roots_callback=session_params.list_roots_callback,
349349
logging_callback=session_params.logging_callback,
350-
message_handler=session_params.message_handler,
350+
message_callback=session_params.message_callback,
351351
client_info=session_params.client_info,
352352
)
353353
)

src/mcp/client/subscriptions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def __init__(self, negotiated_version: str | None) -> None:
6464
super().__init__(
6565
f"subscriptions/listen is not available at protocol version {negotiated_version!r}; it requires "
6666
"2026-07-28. On earlier versions use subscribe_resource() and the change notifications delivered "
67-
"through message_handler."
67+
"through message_callback."
6868
)
6969

7070

src/mcp/shared/jsonrpc_dispatcher.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ def __init__(
291291
self.on_stream_exception = on_stream_exception
292292
"""Observer for ``Exception`` items on the read stream. Mutable so a session can
293293
bind it after the dispatcher is built (e.g. ``ClientSession`` routing into
294-
``message_handler``); only consulted inside ``run()`` so pre-enter assignment is safe."""
294+
``message_callback``); only consulted inside ``run()`` so pre-enter assignment is safe."""
295295

296296
self._next_id = 0
297297
self._pending: dict[RequestId, _Pending] = {}

tests/client/test_client_caching.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -217,11 +217,11 @@ async def test_cache_false_disables_the_cache_and_the_handler_wrap() -> None:
217217
async def handler(message: IncomingMessage) -> None:
218218
raise NotImplementedError
219219

220-
client = Client(_list_changed_server(), cache=False, message_handler=handler)
220+
client = Client(_list_changed_server(), cache=False, message_callback=handler)
221221
assert client._response_cache is None
222222

223223
async with client:
224-
assert client.session._message_handler is handler
224+
assert client.session._message_callback is handler
225225

226226

227227
def test_the_default_cache_uses_a_per_client_in_memory_store() -> None:
@@ -284,7 +284,7 @@ async def collect(message: IncomingMessage) -> None:
284284
received.append(message)
285285
seen.set()
286286

287-
client = Client(_list_changed_server(), mode="legacy", message_handler=collect)
287+
client = Client(_list_changed_server(), mode="legacy", message_callback=collect)
288288

289289
async with client:
290290
key = await _warm_tools_list_entry(client)
@@ -304,10 +304,10 @@ async def test_non_notification_items_pass_through_to_the_user_handler_untouched
304304
async def collect(message: IncomingMessage) -> None:
305305
received.append(message)
306306

307-
client = Client(_list_changed_server(), message_handler=collect)
307+
client = Client(_list_changed_server(), message_callback=collect)
308308

309309
async with client:
310-
installed = client.session._message_handler
310+
installed = client.session._message_callback
311311
assert installed is not collect # the wrap, not the bare user handler
312312
key = await _warm_tools_list_entry(client)
313313
fault = RuntimeError("stream broke")
@@ -328,7 +328,7 @@ async def collect(message: IncomingMessage) -> None:
328328
received.append(message)
329329
seen.set()
330330

331-
client = Client(_list_changed_server(), mode="legacy", message_handler=collect)
331+
client = Client(_list_changed_server(), mode="legacy", message_callback=collect)
332332
# The wrap reads `_response_cache` at session build, so the swap must happen pre-enter.
333333
client._response_cache = _ExplodingCache(
334334
store=InMemoryResponseCacheStore(),
@@ -923,7 +923,7 @@ async def on_message(message: IncomingMessage) -> None:
923923
assert isinstance(message, ToolListChangedNotification) # the only message this server emits
924924
delivered.set()
925925

926-
client = Client(server, mode="legacy", cache=CacheConfig(default_ttl_ms=60_000), message_handler=on_message)
926+
client = Client(server, mode="legacy", cache=CacheConfig(default_ttl_ms=60_000), message_callback=on_message)
927927
async with client:
928928
await client.list_tools()
929929
await client.list_tools()
@@ -964,7 +964,7 @@ async def on_message(message: IncomingMessage) -> None:
964964
delivered.append(message.params.uri)
965965
seen.set()
966966

967-
client = Client(server, mode="legacy", cache=CacheConfig(default_ttl_ms=60_000), message_handler=on_message)
967+
client = Client(server, mode="legacy", cache=CacheConfig(default_ttl_ms=60_000), message_callback=on_message)
968968
async with client:
969969
await client.read_resource(uri)
970970
await client.read_resource(uri)
@@ -1201,7 +1201,7 @@ async def on_message(message: IncomingMessage) -> None:
12011201
server,
12021202
mode="legacy",
12031203
cache=CacheConfig(default_ttl_ms=60_000, clock=_ManualClock()),
1204-
message_handler=on_message,
1204+
message_callback=on_message,
12051205
)
12061206

12071207
async with client:
@@ -1469,7 +1469,7 @@ async def on_message(message: IncomingMessage) -> None:
14691469
server,
14701470
mode="legacy",
14711471
cache=CacheConfig(default_ttl_ms=60_000, clock=_ManualClock()),
1472-
message_handler=on_message,
1472+
message_callback=on_message,
14731473
)
14741474

14751475
async with client:

tests/client/test_logging_callback.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ async def test_tool_with_log_dict(
5555
return True
5656

5757
# Create a message handler to catch exceptions
58-
async def message_handler(
58+
async def message_callback(
5959
message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception,
6060
) -> None:
6161
if isinstance(message, Exception): # pragma: no cover
@@ -64,7 +64,7 @@ async def message_handler(
6464
async with Client(
6565
server,
6666
logging_callback=logging_collector,
67-
message_handler=message_handler,
67+
message_callback=message_callback,
6868
mode="legacy",
6969
) as client:
7070
# First verify our test tool works

0 commit comments

Comments
 (0)