From f38c6230709f049dfe23a8769bc94641373520be Mon Sep 17 00:00:00 2001 From: Dhruv Maniya Date: Thu, 30 Jul 2026 10:11:24 +0530 Subject: [PATCH] fix: reject boolean progress totals Signed-off-by: Dhruv Maniya --- src/mcp/shared/jsonrpc_dispatcher.py | 2 +- tests/shared/test_jsonrpc_dispatcher.py | 18 +++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/mcp/shared/jsonrpc_dispatcher.py b/src/mcp/shared/jsonrpc_dispatcher.py index 87bdf31ceb..58d7773089 100644 --- a/src/mcp/shared/jsonrpc_dispatcher.py +++ b/src/mcp/shared/jsonrpc_dispatcher.py @@ -639,7 +639,7 @@ def _dispatch_notification( self._spawn( _shielded_progress(pending.on_progress), float(progress), - float(total) if isinstance(total, int | float) else None, + float(total) if isinstance(total, int | float) and not isinstance(total, bool) else None, message if isinstance(message, str) else None, sender_ctx=sender_ctx, ) diff --git a/tests/shared/test_jsonrpc_dispatcher.py b/tests/shared/test_jsonrpc_dispatcher.py index 9bee8b2c3b..d744c69724 100644 --- a/tests/shared/test_jsonrpc_dispatcher.py +++ b/tests/shared/test_jsonrpc_dispatcher.py @@ -2173,14 +2173,17 @@ async def call() -> None: @pytest.mark.anyio -async def test_progress_with_bool_token_or_bool_progress_does_not_fire_callback(): - """Bool `progressToken`/`progress` values are malformed; the callback must - not fire for the unrelated request keyed by id 1 (`True == 1`).""" +async def test_progress_boolean_fields_are_not_coerced_to_numbers(): + """Raw wire input is required because the typed API rejects boolean progress fields. + + Boolean tokens and progress values are ignored, while a boolean optional + total is treated as absent on an otherwise valid notification. + """ c2s_send, c2s_recv = anyio.create_memory_object_stream[SessionMessage | Exception](32) s2c_send, s2c_recv = anyio.create_memory_object_stream[SessionMessage | Exception](32) client: JSONRPCDispatcher[TransportContext] = JSONRPCDispatcher(s2c_recv, c2s_send) on_request, on_notify = echo_handlers(Recorder()) - seen: list[float] = [] + seen: list[tuple[float, float | None]] = [] try: async with anyio.create_task_group() as tg: await tg.start(client.run, on_request, on_notify) @@ -2194,7 +2197,8 @@ async def respond_with_malformed_then_valid_progress() -> None: for params in ( {"progressToken": True, "progress": 0.1}, # bool token {"progressToken": rid, "progress": True}, # bool progress - {"progressToken": rid, "progress": 0.5}, # valid + {"progressToken": rid, "progress": 0.5, "total": True}, # bool total + {"progressToken": rid, "progress": 0.75, "total": 1}, # valid ): await s2c_send.send( SessionMessage( @@ -2208,7 +2212,7 @@ async def respond_with_malformed_then_valid_progress() -> None: ) async def on_progress(progress: float, total: float | None, message: str | None) -> None: - seen.append(progress) + seen.append((progress, total)) tg.start_soon(respond_with_malformed_then_valid_progress) result = await client.send_raw_request("ping", None, {"on_progress": on_progress}) @@ -2217,7 +2221,7 @@ async def on_progress(progress: float, total: float | None, message: str | None) finally: for s in (c2s_send, c2s_recv, s2c_send, s2c_recv): s.close() - assert seen == [0.5] # only the well-formed progress fired the callback + assert seen == [(0.5, None), (0.75, 1.0)] @pytest.mark.anyio