> For the complete documentation index, see [llms.txt](https://docs.goassay.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.goassay.ai/developers/use-tools-advanced/retries-and-idempotency.md).

# Retries and idempotency

Retry MCP calls without accidentally treating a retry as a new purchase.

A timeout means that your client did not receive an answer. It does not prove that the server stopped or that no payment happened. If your own code retries a call, keep one idempotency key for that logical operation and reuse it with the same tool and arguments.

An idempotency key tells Assay that two requests are attempts at the same call. It is different from the JSON-RPC request ID, which pairs one request with its response.

## What the local MCP does

For every forwarded `tools/call`, the proxy keeps a valid caller-supplied `_meta["assay/idempotency-key"]` or adds a fresh UUID. It replaces an invalid caller key with a fresh UUID too, so validate your keys before sending them.

If payment is needed, the proxy checks the request, signs once, then resends the same call with the same idempotency key and its payment authorization. It signs **at most one payment per incoming tool call**. Another payment request for that call comes back unsigned.

A network error or timeout is returned to your client; the proxy does not keep retrying that failure automatically. It can renew a rejected OAuth token and resend once. That resend keeps the prepared call and its idempotency key.

Asking the AI to “try again” usually creates a new incoming tool call. Without a caller-supplied key, that new call gets a new UUID and can become a separate purchase. The one-signature rule for the previous call does not cover it.

## Retry from your own code

Set the key in the MCP `tools/call` parameters, beside `name` and `arguments`, not inside the tool's arguments. Use 1–128 printable ASCII characters. A UUID is a suitable choice.

This Node.js example starts `assay-mcp` over stdio, calls a tool named `get_weather` through the search-mode `call_tool`, and retries with the same key. Use the tool name and arguments from your discovery result. Sign in first, as in [Install assay-mcp](/developers/use-tools-get-started/install-assay-mcp.md).

```sh
npm install @modelcontextprotocol/sdk
```

Save as `retry.mjs`, then run `node retry.mjs`:

```js
import { randomUUID } from "node:crypto";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

const client = new Client({ name: "my-app", version: "1.0.0" });
await client.connect(new StdioClientTransport({
  command: "npx",
  args: ["-y", "@goassay-ai/mcp", "--name", "assay"],
}));

// One key per logical operation. Save it with your operation record before the first attempt.
const params = {
  name: "call_tool",
  arguments: { name: "get_weather", arguments: { city: "London" } },
  _meta: { "assay/idempotency-key": randomUUID() },
};
const sleep = (s) => new Promise((r) => setTimeout(r, s * 1000));

let result;
for (let attempt = 1; attempt <= 3; attempt++) {
  try {
    // Wait longer than the local MCP's own 120-second timeout.
    result = await client.callTool(params, undefined, { timeout: 130_000 });
  } catch (err) {
    // A timeout or network error does not prove that nothing was charged.
    // Retry with the same params, so the same key.
    if (attempt === 3) throw err;
    await sleep(2 * attempt);
    continue;
  }
  const error = result._meta?.["assay/error"];
  if (error?.code !== "IDEMPOTENCY_IN_PROGRESS" || attempt === 3) break;
  await sleep(error.retryAfter ?? 1); // The first attempt is still running.
}

if (result._meta?.["assay/replayed"]) console.log("Saved result returned; not charged again.");
console.log(JSON.stringify(result.content, null, 2));
await client.close();
```

Each retry sends the same parameters with a new JSON-RPC request ID, which the SDK assigns. Do not create a new key to retry: that starts a new purchase. If you change the tool or arguments, create a new operation with a new key.

## What a duplicate receives

Keys are scoped to the organization and agent, so a retry through another connection for that same agent can find the original call.

| Result                                | What happened                                                                            | What your code should do                                                                                                 |
| ------------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| Result with `_meta["assay/replayed"]` | Assay returned the saved result and receipt. It did not run or charge this retry again.  | Accept the result.                                                                                                       |
| `IDEMPOTENCY_IN_PROGRESS`             | The earlier attempt is still running. This retry added no charge.                        | Wait for the returned `retryAfter`, then use the same key.                                                               |
| `IDEMPOTENCY_KEY_REUSED`              | The key conflicts with a different tool, arguments or request.                           | Correct the caller's key tracking. Use a new key only for a new operation.                                               |
| `IDEMPOTENCY_RESULT_UNAVAILABLE`      | The call completed, but its result cannot be replayed. This retry was not charged again. | Inspect the original receipt or call history. A new key requests another call and may pay again.                         |
| `IDEMPOTENCY_ATTEMPT_STALLED`         | The original payment record remains, but the attempt stopped before finishing.           | Reconcile the named payment record. Repeating this key will not restart the call; a deliberate new call needs a new key. |

## Check it worked

Keep the key with your operation record and inspect result metadata as well as the text result. A completed duplicate should identify a replay; a running duplicate should ask you to wait. Local spending totals can still include an amount recorded for signing when the network later fails; see [Spending limits](/developers/use-tools-get-started/spending-limits.md).

<details>

<summary>Optional: retention and failure boundaries</summary>

| Setting                     | Server default                                 | Consequence                                                                          |
| --------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------ |
| Result replay window        | 15 minutes; deployment-configurable            | Retry promptly. A saved result is not permanent storage.                             |
| Largest stored output       | 262,144 bytes of JSON; deployment-configurable | Larger results leave a marker and return `IDEMPOTENCY_RESULT_UNAVAILABLE` on replay. |
| In-flight claim lease       | 120 seconds                                    | An unfinished payment record older than this is reported as stalled.                 |
| Suggested in-progress delay | 1 second                                       | Prefer the returned `retryAfter` value.                                              |

Paid calls also have a durable uniqueness check on the organization, agent and idempotency key. Losing the temporary replay store does not by itself permit a second charge for a live keyed payment record. When possible, the server rebuilds a recent result from the original payment's verification record.

This is not an unlimited exactly-once execution promise. Free calls have no durable payment backstop. Payment records that are voided, expired or refunded release their keys; a later call can run again. Use fresh keys for deliberate new operations and reconcile uncertain outcomes before starting a replacement.

Cancelling a call prevents the proxy from signing it if cancellation arrives before signing. It does not undo an authorization already sent. A payment request left unsigned after the first signature is also not evidence that the first payment failed.

</details>

## Errors

| Code / message                              | Meaning                                           | Fix                                                                                   |
| ------------------------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `-32002`, **Assay did not answer in time.** | A forwarded request reached its timeout.          | Retry with your saved key after checking the outcome.                                 |
| `-32003`, **Can't reach Assay (…)**         | A network exchange failed.                        | Restore connectivity and reuse the saved key.                                         |
| `MCP_IDEMPOTENCY_KEY_INVALID`               | A request reaching the server has an invalid key. | Use the printable ASCII format above; the local proxy normally replaces invalid keys. |

## Go further

* [How paying works](/developers/use-tools-get-started/how-paying-works.md)
* [Spending limits](/developers/use-tools-get-started/spending-limits.md)

## Next

* [assay-mcp CLI reference](/developers/use-tools-advanced/cli-reference.md)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.goassay.ai/developers/use-tools-advanced/retries-and-idempotency.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
