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

{peer.name}: malformed discovery response

Error message

{peer.name}: malformed discovery response

What it means

During modern discovery the peer returned a non-null response that is not a dict, so it cannot be a JSON-RPC message. The client distinguishes this from the empty case (which triggers a legacy probe) and treats a non-dict payload as a protocol violation.

Source

Thrown at phases/13-tools-and-protocols/08-building-an-mcp-client/code/main.py:423

        request_id = self._new_id()
        probe = modern_request(
            request_id,
            "server/discover",
            {},
            self.probe_version,
            self.client_capabilities,
        )
        try:
            response = self._send(peer, probe, self.discovery_timeout_ms)
        except (TimeoutError, ConnectionError) as exc:
            self._probe_legacy(peer, type(exc).__name__)
            return

        if response is None:
            self._probe_legacy(peer, "empty discovery response")
            return
        if not isinstance(response, dict):
            raise RuntimeError(f"{peer.name}: malformed discovery response")
        kind, payload = decode_rpc_response(response, request_id)
        if kind == "result":
            advertised = payload.get("supportedVersions", [])
            if not isinstance(advertised, list) or not all(
                isinstance(version, str) for version in advertised
            ):
                raise RuntimeError(f"{peer.name}: malformed modern discovery result")
            selected = self._mutual_version(advertised)
            if selected is None:
                raise RuntimeError(f"{peer.name}: no mutually supported modern version")
            self._activate_modern(peer, payload, selected)
            return

        code = payload["code"]
        if code in RECOGNIZED_MODERN_ERRORS:
            if code != -32022:
                raise RuntimeError(f"{peer.name}: correct modern request error {code} before retrying")
            data = payload.get("data")

View on GitHub (pinned to 39ea8a1c6d)

Solutions

  1. Inspect the raw discovery response the transport produced
  2. Fix the transport to return parsed dict messages
  3. Remove any intermediary that mangles the message shape
  4. Add a regression test asserting discovery responses are dicts

Example fix

// before
transport.receive = lambda: raw_json_text  # str leaks through

// after
transport.receive = lambda: json.loads(raw_json_text)  # dict returned
Defensive patterns

Strategy: type-guard

Validate before calling

resp = transport.peek_discovery(peer)
if resp is not None and not isinstance(resp, dict):
    raise ValueError('transport returns non-dict frames; fix before connect')

Type guard

def is_rpc_message(value) -> bool:
    return isinstance(value, dict) and "jsonrpc" in value

Try / catch

null

Prevention

When it happens

Trigger: _connect_peer's discovery _send returns a truthy value where isinstance(response, dict) is False — e.g. a string, list, or number.

Common situations: Transport returning unparsed JSON text; a proxy or mock injecting a non-message payload; partial deserialization bugs in the framing layer.

Understand the failure class

Related errors


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