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

: RPC error

Error message

{peer.name}: RPC error {payload}

What it means

Relay point in MultiServerClient._request: the peer returned a well-formed JSON-RPC error object (already validated by decode_rpc_response); this raise re-surfaces the peer's error payload to the local caller, prefixing it with the peer name so failures across multiple servers stay attributable.

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.

Appendix: 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)