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

{peer.name}: RPC error {payload}

Error message

{peer.name}: RPC error {payload}

What it means

The peer answered a tools/list or tools/call request with a JSON-RPC error object. The client surfaces the error payload verbatim in the message so the caller can see the server's code and message.

Source

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

        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", {})
                peer.tools = sorted(result.get("tools", []), key=lambda tool: tool["name"])

    def merge(self, policy: str = "prefix-on-collision") -> None:
        if policy not in {"prefix-on-collision", "reject"}:
            raise ValueError("policy must be prefix-on-collision or reject")
        self.registry.clear()

View on GitHub (pinned to 39ea8a1c6d)

Solutions

  1. Read the embedded payload for the server's error code and message
  2. Verify the tool name against the peer's discovered tools/list result
  3. Check argument shape before calling
  4. Fix the server-side cause if the error is a genuine execution failure

Example fix

// before
client.call("search", {"q": 123})  # q must be a string

// after
client.call("search", {"q": "hello"})
Defensive patterns

Strategy: try-catch

Validate before calling

names = {t['name'] for t in known_tools(peer)}
if tool_name not in names:
    raise ValueError(f'unknown tool {tool_name} for {peer.name}')

Type guard

null

Try / catch

try:
    client.call(tool_name, args)
except RuntimeError as e:
    if "RPC error" in str(e):
        handle_server_error(parse_payload(str(e)), tool_name)

Prevention

When it happens

Trigger: decode_rpc_response(response, request_id) in _request returns kind='error', i.e. the matched response carries an error, not a result.

Common situations: Calling a tool that does not exist on the peer; invalid tool arguments; server-side execution failure or permission denial.

Related errors


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