Skip to content

[v2] Boolean progress total is delivered to callbacks as 1.0 #3217

Description

@chavalasantosh

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:

[(0.5, 1.0)]

Expected output:

[(0.5, None)]

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions