---
title: "MCP Tool Call Error Handling: When an Agent Should Retry"
description: "Each call_endpoint error code mapped to one agent action (fix, wait, stop or ask the user), with the exact error text the model sees and a system prompt."
url: https://perceptlayer.com/blog/mcp-tool-call-errors-agent-retries
---

[Blog](https://perceptlayer.com/blog) / [AI agents](https://perceptlayer.com/blog/category/ai-agents)

# MCP Tool Call Error Handling: When an Agent Should Retry

16 Sept 2026 Perceptlayer 7 min read

Good MCP tool call error handling gives the model an error it can act on, then tells it which action fits which error: fix the call, wait and retry, stop, or ask the user. Perceptlayer's `call_endpoint` returns every failure as a tool result marked `isError`, with text that starts `Error <code>:`, so the model reads the code directly. Only two codes are worth retrying unchanged, and no failed call costs credits.

## Key takeaways

-   Every failed `call_endpoint` returns a tool result with `isError: true` and text of the form `Error <code>: <message> (request_id: req_…)`. The model sees the code, not a stack trace.
-   Fix and retry once: `invalid_parameters` and `unknown_endpoint`. Wait and retry: `rate_limited` and `temporarily_unavailable`, whose text ends `Retry after N seconds.`
-   Don't retry: `not_found` (no data for those parameters), and `insufficient_credits`, `restricted_endpoint` and `invalid_api_key`, which only a person can fix.
-   Failed calls cost 0 credits, so retries are free in credits. What a retry loop costs is turns, time and context, which is why the agent needs a cap.
-   Put the mapping in the system prompt and enforce a retry limit in code. The prompt shapes behaviour; the code guarantees it.

## Why should tool errors reach the model at all?

Because a model can only correct what it can see. The MCP schema says errors that originate from a tool should be reported inside the tool result with `isError` set to true, since otherwise the model wouldn't know an error occurred and couldn't self-correct ( [Model Context Protocol schema](https://modelcontextprotocol.io/specification/2025-06-18/schema), 2025). The tools specification draws the line the same way: unknown tools, invalid arguments and server errors are JSON-RPC protocol errors, while API failures and business-logic errors are tool execution errors reported with `isError: true` ( [MCP tools specification](https://modelcontextprotocol.io/specification/2025-06-18/server/tools), 2025).

Perceptlayer's MCP server follows that split. An endpoint's own parameters travel inside `call_endpoint`'s `params` object, so a bad value there is a failure of the endpoint call, not of the tool's arguments. A bad endpoint ID, bad parameters, an empty result or a busy source all come back as normal tool results with `isError: true`, so the model reads them like any other output and decides what to do next. That puts the decision with the model, which is useful for `invalid_parameters`, where it can repair the call itself, and risky for codes it can't fix, where an unguided model may try again anyway. The rest of this article is about guiding that decision.

## What does the model actually see when a call fails?

One line of text: `Error`, the code, a colon, a message, and the request ID in brackets. When the source asks you to wait, the line also ends with `Retry after N seconds.`, taken from the `Retry-After` header. These are the messages the server sends today, from its source code, with the request ID shortened:

| Code | HTTP status behind it | What the model reads |
| --- | --- | --- |
| `invalid_parameters` | 422 | `Error invalid_parameters: Invalid parameters: 'safe' must be one of: active, off. (request_id: req_…)` |
| `unknown_endpoint` | none (MCP-level) | `Error unknown_endpoint: No endpoint 'google.search'. Use search_endpoints to find endpoint IDs.` |
| `not_found` | 404 | `Error not_found: No data was found for these parameters. You were not charged. (request_id: req_…)` |
| `rate_limited` | 429 | `Error rate_limited: Too many requests. Retry after the number of seconds in the Retry-After header. Retry after N seconds. (request_id: req_…)` |
| `temporarily_unavailable` | 503 | `Error temporarily_unavailable: The data source is busy. … You were not charged. Retry after N seconds. (request_id: req_…)` |
| `insufficient_credits` | 402 | `Error insufficient_credits: Your credit balance is used up. Buy more credits to continue. (request_id: req_…)` |
| `restricted_endpoint` | 403 | `Error restricted_endpoint: This endpoint requires accepting the restricted-endpoint terms in your dashboard. (request_id: req_…)` |
| `invalid_api_key` | 401 | `Error invalid_api_key: Missing, unknown or revoked API key. (request_id: req_…)` |

Two more codes can appear: `source_error` (502, "The data source returned an error") and `timeout` (504, "did not answer within 60 seconds"), both ending "You were not charged." When the source itself rejects parameters that passed validation, `invalid_parameters` carries "The data source rejected these parameters. Check them against the API reference." The [errors docs](https://docs.perceptlayer.com/errors) list every code with its HTTP status.

## Which action fits each error code?

Sort the codes by who can fix the problem. If the model can fix it, let it fix the call and try once more. If time fixes it, wait the stated seconds and retry. If nothing in this conversation will change the answer, stop. If only the user can fix it, say so and end the task. Every one of these failures costs 0 credits, so credit cost never decides the action; wasted turns do.

| Code | Who fixes it | Agent action | Retry? |
| --- | --- | --- | --- |
| `invalid_parameters` | The model | Read the message, call `get_endpoint` if needed, correct the parameter | Once, with the fix |
| `unknown_endpoint` | The model | Call `search_endpoints`, use a listed ID | Once, with the new ID |
| `rate_limited` | Time | Wait the `Retry after N seconds`, then repeat the same call | Yes, after the wait |
| `temporarily_unavailable` | Time | Same as `rate_limited` | Yes, after the wait, at most twice |
| `source_error`, `timeout` | Maybe time | Retry once later; if it fails again, report it | Once |
| `not_found` | Nobody | Accept that there's no data for these parameters; change the question or report it | No, not unchanged |
| `insufficient_credits` | The user | Stop and tell the user the balance is used up | No |
| `restricted_endpoint` | The user | Stop and tell the user to accept the restricted-endpoint terms | No |
| `invalid_api_key` | The user | Stop and tell the user to fix the server's API key | No |

`not_found` deserves care. It means the source had nothing for those exact parameters, which is an answer, not a fault. Retrying the same call wastes a turn. A different query, a broader location or another source may succeed, and that's a new call, not a retry.

## What does a recovered call look like?

Here's the most common recovery: a parameter outside its allowed values, fixed on the next turn. The model asks for a Google search with `safe` set to a value the endpoint doesn't accept:

```json
{ "endpoint": "google-search.search", "params": { "query": "best restaurants in Berlin", "safe": "maybe" } }
```

The tool result comes back with `isError: true` and this text:

```text
Error invalid_parameters: Invalid parameters: 'safe' must be one of: active, off. (request_id: req_…)
```

The message names the parameter and its allowed values, so the model needs no extra lookup. It sends the call again with `"safe": "off"`, and this time the result is the search JSON followed by a second text block, `Credits used: 1. Credits remaining: …` and the request ID. The failed attempt cost nothing; the whole recovery cost 1 credit. The [Google Search API page](https://perceptlayer.com/apis/google-search) lists that endpoint's other parameters.

A rate limit plays out differently. With the free account's 1 request a second, an agent that fires several calls at once gets `rate_limited` on the extras, each ending `Retry after N seconds.` The right move is to wait that long and repeat the same calls, not to change them. The [rate limits docs](https://docs.perceptlayer.com/rate-limits) explain the per-account limits, and the article on [MCP server rate limiting](https://perceptlayer.com/blog/mcp-server-rate-limiting-agents) covers pacing calls so the limit rarely trips.

## What should the system prompt say?

State the mapping in a few plain rules, keyed on the code at the start of the error text, rather than general advice to "handle errors gracefully" that leaves the model to guess what each code means. This block fits any agent that uses the server:

```text
When a call_endpoint result starts with "Error <code>:", act on the code:
- invalid_parameters or unknown_endpoint: fix the call using the message
  (call get_endpoint or search_endpoints if you need to), then try once more.
- rate_limited or temporarily_unavailable: wait the seconds given after
  "Retry after", then repeat the same call. Give up after two waits.
- source_error or timeout: try once more; if it fails again, say the source
  is not answering right now.
- not_found: there is no data for those parameters. Don't repeat the call.
  Change the query if that still answers the user's question; otherwise say so.
- insufficient_credits, restricted_endpoint, invalid_api_key: stop. Tell the
  user exactly what the message says they need to do. Don't call other tools
  to work around it.
Never repeat an identical failed call more than once.
```

The last line matters most. It stops the loop where a model answers each failure by resending the same request, which costs no credits but burns turns and fills the context with error lines.

## How do you enforce the rules in code?

Count failures per call in the client and refuse to forward a repeat past the limit. A prompt shapes what the model tries; your code decides what actually reaches the server. If you run the agent loop yourself, wrap the tool call: parse the code from the result text, track identical calls, and sleep for `Retry after` before passing a repeat through. The parsing is two regular expressions:

```python
import json
import re
import time

ERROR = re.compile(r"^Error (\w+):")
RETRY = re.compile(r"Retry after (\d+) seconds")
failures = {}

def guarded_call(call_tool, endpoint, params, max_repeats=1):
    key = endpoint + json.dumps(params, sort_keys=True)
    if failures.get(key, 0) > max_repeats:
        return "Error retry_limit: this exact call already failed. Change it or stop."
    text = call_tool("call_endpoint", {"endpoint": endpoint, "params": params})
    m = ERROR.match(text)
    if not m:
        failures.pop(key, None)
        return text
    failures[key] = failures.get(key, 0) + 1
    wait = RETRY.search(text)
    if m.group(1) in ("rate_limited", "temporarily_unavailable") and wait:
        time.sleep(int(wait.group(1)))
    return text
```

The error texts above come from the server's source code; we haven't run this guard against a live agent for this article. `call_tool` is whatever function your MCP client uses to call a tool and return its text. The guard sleeps through rate limits so the model's next attempt lands after the window, and it turns a third identical failure into a message the model can't misread. For a hard cap on credits per run as well, the guide to [AI agent cost control](https://perceptlayer.com/blog/ai-agent-web-data-costs) adds a budget check to the same wrapper.

To see the error texts for yourself, add the server to your client using the [AI agents (MCP) docs](https://docs.perceptlayer.com/ai-agents) and send a call with a deliberately wrong parameter.

## APIs in this post

-   [Google Search **1 credit per call** · 7 endpoints](https://perceptlayer.com/apis/google-search)

## Frequently asked questions

### Do failed MCP tool calls cost credits?

No. Every error response from the API carries `X-Credits-Used: 0`, and nothing is debited from the balance. Only a successful `call_endpoint` spends the endpoint's credits, 1 for most endpoints. Retries after an error are free in credits, though each one still costs a model turn and context.

### Should an agent retry not\_found?

Not with the same parameters. `not_found` means the source had no data for exactly what was asked, so the identical call will return the same answer. The agent should change the query, such as a broader location or different wording, if that still serves the user, or report that nothing was found.

### How long should the agent wait after rate\_limited?

The number of seconds in the error text after "Retry after", which comes from the response's `Retry-After` header. Waiting less just produces another `rate_limited`. Free accounts get 1 request a second and paying accounts at least 5, counted per minute across all of an account's keys.

### Why do some errors have no request ID?

`unknown_endpoint` and bad `params` shapes are caught by the MCP server before any request reaches the API, so there's no request to identify. Errors from the API itself, such as `invalid_parameters` from validation or `rate_limited`, include a `request_id` you can quote to support.

### Can the agent fix insufficient\_credits itself?

No, and it shouldn't try. The balance is prepaid and there's no automatic top-up, so the call fails until someone buys credits. The agent should stop and tell the user. Calling other endpoints won't help, because every paid call fails the same way until the balance is topped up on the [pricing page](https://perceptlayer.com/pricing).

mcp error handling ai agents retries tool calling

## More from the blog

1.  [AI agents](https://perceptlayer.com/blog/category/ai-agents) 12 Oct 2026 Perceptlayer 7 min read
    
    ## [MCP Server Too Many Tools: Keeping Agent Context Small](https://perceptlayer.com/blog/mcp-too-many-tools-context)
    
    One tool per endpoint for 1,659 endpoints is about 567,000 characters of schema. A 10-step checklist to keep an MCP server's tool list small.
    
2.  [AI agents](https://perceptlayer.com/blog/category/ai-agents) 2 Sept 2026 Perceptlayer 8 min read
    
    ## [MCP Server for Web Scraping: Tools Agents Can Call](https://perceptlayer.com/blog/mcp-server-for-web-scraping)
    
    An MCP server gives an agent three tools to find, inspect and call web endpoints. Use the scraper for uncatalogued pages, structured endpoints for the rest.
    
3.  [AI agents](https://perceptlayer.com/blog/category/ai-agents) 24 Jul 2026 Perceptlayer 7 min read
    
    ## [LLM Tool Schema Design: A Checklist for Web APIs](https://perceptlayer.com/blog/agent-tool-schema-design-web-apis)
    
    LLM tool schema design checklist, applied to the Google Maps simple-search endpoint: a before and after schema that cuts 28 result fields to 7.
