Skip to content

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

Closed as not planned

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

Activity

  1. added
    P3Nice to haves, rare edge cases
    on Aug 12, 2026
  2. maxisbey commented on Aug 18, 2026

    @maxisbey
    Contributor

    Thanks for the report — the behaviour reproduces as described.

    I'm going to close this as not planned, though. The spec types total as a number, and I couldn't find anything that actually puts a boolean there: in every statically typed SDK it's a compile error, and in the dynamically typed ones (ours included) it only happens when the calling code passes a bool into an API annotated as numeric — a bug in that code, not something the receiving side should paper over. There are also no reports of anyone hitting this with a real client or server.

    Agreed that the check on total is looser than the ones on progressToken and progress, but with no real sender that isn't worth a behaviour change on its own. If a peer turns up that genuinely emits this, happy to reopen.

    AI Disclaimer

  3. added
    v2Affects the v2 line (2.x on main)
    v1Affects the v1.x maintenance line
    on Sep 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3Nice to haves, rare edge casesv1Affects the v1.x maintenance linev2Affects the v2 line (2.x on main)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions