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
- Inspect the raw discovery response the transport produced
- Fix the transport to return parsed dict messages
- Remove any intermediary that mangles the message shape
- 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
- Keep transports responsible for JSON parsing
- Assert frame shape in transport unit tests
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
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- {peer.name}: proven-modern discovery retry returned no resul
- {peer.name}: bounded legacy probe returned no result
- {peer.name}: missing response
- server/discover accepts no params beyond _meta
- {peer.name}: malformed modern discovery result
AI-assisted analysis of rohitg00/ai-engineering-from-scratch@39ea8a1c6d (2026-08-26).
Data as JSON: /api/errors/5d710b433f124710.
Report an issue: GitHub.