Skip to content

Differentiate retry guidance for MCP and REST clients in 'Transient errors' section #21

Description

@akolotov

Context

The blockscout-analysis skill's SKILL.md § "Transient errors" (currently lines 29–36 on main) gives one unified retry rule for both access methods:

  • Retry on 5xx (especially 500 Internal Server Error): retry the same call up to 3 times before giving up …
  • Applies to both access methods. The same rule governs native MCP tool calls and scripts hitting the MCP REST API …

This was discovered to be inaccurate during review of blockscout/mcp-server#361, which fixes an integration-test retry cascade (3 internal × 3 test-wrapper = 9 attempts). The same cascade exists in production via REST clients that follow this rule literally.

Empirical findings

Verified by running an in-process experiment that forces httpx.ConnectError / httpx.ConnectTimeout / honest upstream 502 against make_blockscout_request (production default bs_request_max_retries = 3):

Failure mode Internal attempts REST surface MCP surface
Transport error (ConnectError) — retries exhausted 3 500 + JSON {\"error\":...} 200 + isError:true, bare exception text
Transport timeout (ConnectTimeout) — retries exhausted 3 504 + JSON 200 + isError:true, bare exception text
Honest upstream 502 (HTTPStatusError, not retried) 1 502 pass-through 200 + isError:true, \"502 Bad Gateway - Details: ...\"

Two key consequences for the skill:

  1. Native MCP clients never see a 5xx HTTP status. Tool errors are always wrapped as {\"isError\": true, \"content\": [{\"type\": \"text\", \"text\": \"Error executing tool <name>: <str(exception)>\"}]} at HTTP 200. The current rule "retry on 5xx" is structurally inapplicable in MCP mode — an agent has to reason about retryability from the error text, not from a status code.

  2. REST scripts following the rule literally produce a 3×3=9 cascade for transport failures. A ConnectError exhausts the server's 3 internal retries → surfaces as 500; a ConnectTimeout surfaces as 504. Both are 5xx → the script's "retry 3 times" rule fires on top → 9 total attempts for a single transport-level event. The script has no reliable way to distinguish "already-retried transport failure" from "honest upstream 5xx" via status code alone.

Proposed change

Split § "Transient errors" into two access-method-specific subsections rather than one unified rule.

Native MCP (agents)

  • Tool errors arrive as isError: true with text content; no HTTP status indicator.
  • Retryability is decided from the error text (natural-language understanding) — agents should retry when the text suggests a transient/network issue (e.g., "connection reset", "timed out", "All connection attempts failed", "upstream 5xx") and not retry on validation/4xx-style messages.
  • Add a soft note: the server already retries transport-level failures internally up to 3 times before surfacing them, so additional aggressive retries from the agent rarely help — 1 follow-up attempt is usually sufficient.

REST API (scripts)

  • 500 and 504 responses from the MCP server itself typically indicate the server already exhausted its 3 internal retries on a transport-level failure (incomplete reads, connection errors, downstream timeouts). For these, 1 additional client-side retry is sufficient — do not retry up to 3 times.
  • For honest 5xx passed through from upstream Blockscout (e.g., a genuine 502 from the indexer), 3 retries are still reasonable. Scripts cannot reliably distinguish these two cases by status code alone, so the conservative default should be 1 retry.
  • 4xx semantics unchanged (no retry, including 413 for direct_api_call oversize).

Related work

  • mcp-server PR introducing the two-tier timeout and integration-test retry fix: Introduce a two-tier timeout system for blockscout requests mcp-server#361
  • SPEC.md § 7 "HTTP Request Robustness" documents the internal retry policy on the server side; a parallel update there is being planned.
  • The corresponding REST-only note will land in mcp-server/API.md § "Error Handling".

Versioning

This is a skill content change; will need a bump-skill-version once content is finalized.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions