Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/mcp/shared/jsonrpc_dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down
18 changes: 11 additions & 7 deletions tests/shared/test_jsonrpc_dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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(
Expand All @@ -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})
Expand All @@ -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
Expand Down
Loading