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

{peer.name}: bounded legacy probe failed closed

Error message

{peer.name}: bounded legacy probe failed closed

What it means

The legacy initialize probe was sent with a bounded timeout (legacy_probe_timeout_ms) and the transport raised TimeoutError or ConnectionError. The client deliberately fails closed rather than retrying indefinitely, wrapping the transport error to identify which peer and which stage failed.

Source

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

    def _probe_legacy(self, peer: Peer, trigger: str) -> None:
        if not peer.allow_legacy:
            raise RuntimeError(
                f"{peer.name}: {trigger}; legacy compatibility is not allowlisted"
            )
        request_id = self._new_id()
        initialize = legacy_request(
            request_id,
            "initialize",
            {
                "protocolVersion": self.supported_legacy[0],
                "capabilities": self.client_capabilities.copy(),
                "clientInfo": CLIENT_INFO.copy(),
            },
        )
        try:
            response = self._send(peer, initialize, self.legacy_probe_timeout_ms)
        except (TimeoutError, ConnectionError) as exc:
            raise RuntimeError(f"{peer.name}: bounded legacy probe failed closed") from exc
        if not isinstance(response, dict):
            raise RuntimeError(f"{peer.name}: bounded legacy probe returned no result")
        kind, payload = decode_rpc_response(response, request_id)
        if kind != "result":
            raise RuntimeError(f"{peer.name}: legacy initialize returned an error")
        result = payload
        version = result.get("protocolVersion")
        capabilities = result.get("capabilities")
        server_info = result.get("serverInfo")
        valid_server_info = (
            isinstance(server_info, dict)
            and isinstance(server_info.get("name"), str)
            and bool(server_info["name"])
            and isinstance(server_info.get("version"), str)
            and bool(server_info["version"])
        )
        if version not in self.supported_legacy:
            raise RuntimeError(f"{peer.name}: unsupported legacy protocol revision")

View on GitHub (pinned to 39ea8a1c6d)

Solutions

  1. Raise legacy_probe_timeout_ms on the client to cover the server's startup latency
  2. Verify the peer process is alive and listening before connect_all
  3. Check network path/firewall between client and peer
  4. Confirm the server actually speaks the legacy initialize method at all

Example fix

// before
client = McpClient(..., legacy_probe_timeout_ms=500)

// after
client = McpClient(..., legacy_probe_timeout_ms=5000)
Defensive patterns

Strategy: retry

Validate before calling

if not peer_reachable(peer):
    raise SystemExit(f"{peer.name} unreachable before connect")

Type guard

null

Try / catch

try:
    client.connect_all()
except RuntimeError as e:
    if "legacy probe failed closed" in str(e) and transient_network():
        client.connect_all()  # one bounded retry after fixing the network

Prevention

When it happens

Trigger: peer.allow_legacy is True, _probe_legacy sends legacy_request('initialize') via self._send(peer, ..., self.legacy_probe_timeout_ms), and the send times out or the connection drops before a response arrives.

Common situations: Legacy server that is slow to answer initialize; dead peer process behind a still-open socket; firewall dropping responses; legacy_probe_timeout_ms configured too low for a cold-starting server.

Related errors


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