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

{peer.name}: protocol era not selected

Error message

{peer.name}: protocol era not selected

What it means

A request was attempted against a peer whose era field is neither 'modern' nor 'legacy', meaning connect_all never completed handshake/activation for that peer. The client refuses to guess a wire format and aborts before serializing anything.

Source

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

    def connect_all(self) -> None:
        for peer_name in sorted(self.peers):
            self._connect_peer(self.peers[peer_name])

    def _request(self, peer: Peer, method: str, params: dict[str, Any]) -> dict[str, Any]:
        request_id = self._new_id()
        if peer.era == "modern":
            message = modern_request(
                request_id,
                method,
                params,
                peer.protocol_version or PROTOCOL_VERSION,
                self.client_capabilities,
            )
        elif peer.era == "legacy":
            message = legacy_request(request_id, method, params)
        else:
            raise RuntimeError(f"{peer.name}: protocol era not selected")
        response = self._send(peer, message)
        if not isinstance(response, dict):
            raise RuntimeError(f"{peer.name}: missing response")
        kind, payload = decode_rpc_response(response, request_id)
        if kind != "result":
            raise RuntimeError(f"{peer.name}: RPC error {payload}")
        result = dict(payload)
        if peer.era == "modern" and "resultType" not in result:
            raise RuntimeError(f"{peer.name}: modern result omitted resultType")
        if peer.era == "legacy":
            result.setdefault("resultType", "complete")
        return result

    def discover_tools(self) -> None:
        for peer_name in sorted(self.peers):
            peer = self.peers[peer_name]
            if peer.available:
                result = self._request(peer, "tools/list", {})

View on GitHub (pinned to 39ea8a1c6d)

Solutions

  1. Ensure connect_all() runs to completion before discover_tools()/call()
  2. Don't swallow per-peer connect errors — check peer.available first
  3. Re-run the handshake for the failed peer
  4. Guard calls with a check on peer.era/peer.available

Example fix

// before
try:
    client.connect_all()
except RuntimeError:
    pass
client.discover_tools()  # era never selected

// after
client.connect_all()
if not all(p.available for p in client.peers.values()):
    raise SystemExit("some peers failed to connect")
client.discover_tools()
Defensive patterns

Strategy: validation

Validate before calling

assert all(p.available for p in client.peers.values()), 'run connect_all first'
client.discover_tools()

Type guard

def peer_ready(peer) -> bool:
    return peer.era in {'modern', 'legacy'} and peer.available

Try / catch

null

Prevention

When it happens

Trigger: discover_tools or call is invoked on a peer where peer.era is unset because _activate_modern/_probe_legacy never ran or failed (e.g. connect_all skipped, or an earlier connect error was swallowed).

Common situations: Calling discover_tools/call before connect_all; swallowing a connect_all exception per-peer and then continuing; resetting peer state manually.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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