Initial Checks
Release line
2.x (current stable)
Description
JSONRPCDispatcher incorrectly treats a boolean total in an inbound notifications/progress message as a number.
A raw notification containing "total": true invokes the progress callback with total=1.0.
The dispatcher already rejects boolean values for progressToken and progress, but its optional total validation only uses:
isinstance(total, int | float)
Because bool is a subclass of int in Python, True passes this check and float(True) becomes 1.0.
This makes an invalid boolean value indistinguishable from a genuine numeric total.
Expected behavior: the progress notification is delivered, but the invalid optional total value is treated as absent and passed to the callback as None.
Actual output:
Expected output:
I reproduced this on the current v2 source and searched the existing issues and pull requests without finding this specific case.
I have a focused fix and regression test prepared locally. I will wait for maintainer confirmation before opening a pull request.
Disclosure: I used AI assistance during the investigation. I reproduced the behavior, reviewed the implementation, and take responsibility for this report and the proposed change.
Example Code
from collections.abc import Mapping
from typing import Any
import anyio
from mcp_types import JSONRPCNotification, JSONRPCRequest, JSONRPCResponse
from mcp.shared.dispatcher import DispatchContext
from mcp.shared.jsonrpc_dispatcher import JSONRPCDispatcher
from mcp.shared.message import SessionMessage
from mcp.shared.transport_context import TransportContext
async def ignore_request(
context: DispatchContext[TransportContext],
method: str,
params: Mapping[str, Any] | None,
) -> dict[str, Any]:
return {}
async def ignore_notification(
context: DispatchContext[TransportContext],
method: str,
params: Mapping[str, Any] | None,
) -> None:
return None
async def main() -> None:
outbound_send, outbound_receive = anyio.create_memory_object_stream[SessionMessage](8)
inbound_send, inbound_receive = anyio.create_memory_object_stream[SessionMessage | Exception](8)
dispatcher: JSONRPCDispatcher[TransportContext] = JSONRPCDispatcher(
inbound_receive,
outbound_send,
)
seen: list[tuple[float, float | None]] = []
async def peer() -> None:
request = await outbound_receive.receive()
assert isinstance(request.message, JSONRPCRequest)
request_id = request.message.id
await inbound_send.send(
SessionMessage(
message=JSONRPCNotification(
jsonrpc="2.0",
method="notifications/progress",
params={
"progressToken": request_id,
"progress": 0.5,
"total": True,
},
)
)
)
await inbound_send.send(
SessionMessage(
message=JSONRPCResponse(
jsonrpc="2.0",
id=request_id,
result={"ok": True},
)
)
)
async def on_progress(
progress: float,
total: float | None,
message: str | None,
) -> None:
seen.append((progress, total))
async with anyio.create_task_group() as task_group:
await task_group.start(
dispatcher.run,
ignore_request,
ignore_notification,
)
task_group.start_soon(peer)
await dispatcher.send_raw_request(
"ping",
None,
{"on_progress": on_progress},
)
task_group.cancel_scope.cancel()
print(seen)
anyio.run(main)
Python & MCP Python SDK
Python: 3.12.13
MCP Python SDK: v2 current main source (a4f4ccd091138771535e17191123f20b30fda68e)
OS: Linux
Initial Checks
Release line
2.x (current stable)
Description
JSONRPCDispatcherincorrectly treats a booleantotalin an inboundnotifications/progressmessage as a number.A raw notification containing
"total": trueinvokes the progress callback withtotal=1.0.The dispatcher already rejects boolean values for
progressTokenandprogress, but its optionaltotalvalidation only uses:Because
boolis a subclass ofintin Python,Truepasses this check andfloat(True)becomes1.0.This makes an invalid boolean value indistinguishable from a genuine numeric total.
Expected behavior: the progress notification is delivered, but the invalid optional
totalvalue is treated as absent and passed to the callback asNone.Actual output:
Expected output:
I reproduced this on the current v2 source and searched the existing issues and pull requests without finding this specific case.
I have a focused fix and regression test prepared locally. I will wait for maintainer confirmation before opening a pull request.
Disclosure: I used AI assistance during the investigation. I reproduced the behavior, reviewed the implementation, and take responsibility for this report and the proposed change.
Example Code
Python & MCP Python SDK