rohitg00/ai-engineering-from-scratch · error · ValueError

progressToken must be a string or integer

Error message

progressToken must be a string or integer

What it means

Before running the tool handler the server validates metadata['progressToken']: when present it must be str or int. bool is explicitly rejected because in Python bool is a subclass of int, so True would otherwise slip through the isinstance check — the guard exists to make that trap visible.

Source

Thrown at certifications/claude/lessons/11-mcp-server-design-and-integration/code/main.py:323

    def _call_tool(
        self, params: dict[str, Any], metadata: dict[str, Any]
    ) -> tuple[dict[str, Any], list[dict[str, Any]]]:
        name = params["name"]
        if not isinstance(name, str) or not name:
            raise ValueError("name must be a non-empty string")
        tool = self.tools.get(name)
        if tool is None:
            raise ValueError("unknown tool")
        arguments = tool.validate_arguments(params.get("arguments", {}))
        if name == "prepare_review":
            return self._prepare_review(params, metadata, arguments), []

        token = metadata.get("progressToken")
        notifications: list[dict[str, Any]] = []
        if token is not None:
            if not isinstance(token, (str, int)) or isinstance(token, bool):
                raise ValueError("progressToken must be a string or integer")
            notifications = [
                self._progress(token, 0, 1, "starting"),
                self._progress(token, 1, 1, "complete"),
            ]
        value = tool.handler(arguments)
        return self._complete(
            content=[{"type": "text", "text": json.dumps(value)}], isError=False
        ), notifications

    def _prepare_review(
        self,
        params: dict[str, Any],
        metadata: dict[str, Any],
        arguments: dict[str, Any],
    ) -> dict[str, Any]:
        input_requests = {
            "workspace_scope": {"method": "roots/list", "params": {}},
            "review_sample": {

View on GitHub (pinned to 39ea8a1c6d)

Solutions

  1. Send progressToken as a string or integer, e.g. "tok-1" or 42
  2. Omit progressToken entirely when progress notifications are not wanted
  3. Never send booleans — they pass isinstance(token, int) checks in looser code but are rejected here by design

Example fix

# before
meta = {"progressToken": True}

# after
meta = {"progressToken": "tok-1"}
Defensive patterns

Strategy: type-guard

Validate before calling

token = meta.get("progressToken")
if token is not None and (not isinstance(token, (str, int)) or isinstance(token, bool)):
    raise ValueError("progressToken must be a string or integer")
server.exchange("tools/call", params, metadata=meta)

Type guard

def is_valid_progress_token(token: object) -> bool:
    return token is None or (isinstance(token, (str, int)) and not isinstance(token, bool))

Prevention

When it happens

Trigger: tools/call with a progressToken set to true/false, a float, a list, or a dict in the request metadata (_meta).

Common situations: A JSON config or UI checkbox serialized as boolean into progressToken; client library defaulting the token to an options object; copying a whole request payload as the token.

Related errors


AI-assisted analysis of rohitg00/ai-engineering-from-scratch@39ea8a1c6d (2026-08-26). Data as JSON: /api/errors/aacae5953dc23611. Report an issue: GitHub.