> 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/zh/shi-yong-gong-ju-jin-jie/retries-and-idempotency.md).

# 重试与幂等

重试 MCP 调用，避免意外将重试变成一笔新购买。

超时表示客户端没有收到响应，不能证明服务器已停止，也不能证明未发生付款。如果你的代码会重试调用，请为同一个逻辑操作保留一个幂等键，并在使用相同工具和参数时复用它。

幂等键告诉 Assay，两个请求是同一次调用的不同尝试。它不同于 JSON-RPC 请求 ID，后者用于将单个请求与响应配对。

## 本地 MCP 会做什么

对于每个转发的 `tools/call`，代理会保留调用方提供的有效 `_meta["assay/idempotency-key"]`，或添加一个新的 UUID。调用方的键无效时也会被替换为新 UUID，因此请在发送前验证键。

如果需要付款，代理会检查请求、签名一次，然后使用相同幂等键和付款授权重新发送同一次调用。它**每次传入的工具调用最多只为一笔付款签名**。该调用的另一个付款请求会返回，但不会签名。

网络错误或超时会返回客户端；代理不会持续自动重试该失败。它可以更新被拒绝的 OAuth 令牌并重发一次。重发会保留已准备的调用及其幂等键。

请 AI “再试一次”通常会创建新的传入工具调用。如果调用方没有提供键，新调用会获得新 UUID，可能成为另一笔购买。前一次调用的一次签名规则不适用于它。

## 从自己的代码重试

在 MCP `tools/call` 参数中设置键，与 `name`、`arguments` 同级，不要放入工具参数内部。使用 1–128 个可打印 ASCII 字符。UUID 是合适的选择。

以下 Node.js 示例通过 stdio 启动 `assay-mcp`，通过搜索模式的 `call_tool` 调用名为 `get_weather` 的工具，并用相同键重试。请使用发现结果中的工具名称和参数。先按[安装 assay-mcp](/developers/zh/shi-yong-gong-ju-ru-men/install-assay-mcp.md)完成登录。

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

保存为 `retry.mjs`，然后运行 `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();
```

每次重试都发送相同参数，并使用新的 JSON-RPC 请求 ID（由 SDK 分配）。重试时不要创建新键，否则会开始一笔新的购买。如果修改工具或参数，请用新键创建新的操作。

## 重复请求会收到什么

键的作用域是组织和智能体，因此通过同一智能体的其他连接重试，也可以找到原始调用。

| 结果                               | 发生了什么                                 | 代码应如何处理                           |
| -------------------------------- | ------------------------------------- | --------------------------------- |
| 带 `_meta["assay/replayed"]` 的结果  | Assay 返回已保存的结果和收据。此次重试没有再次执行，也没有再次收费。 | 接受结果。                             |
| `IDEMPOTENCY_IN_PROGRESS`        | 之前的尝试仍在运行。此次重试未增加费用。                  | 按返回的 `retryAfter` 等待，再使用相同键。      |
| `IDEMPOTENCY_KEY_REUSED`         | 此键与其他工具、参数或请求冲突。                      | 修正调用方的键跟踪逻辑。只有新操作才使用新键。           |
| `IDEMPOTENCY_RESULT_UNAVAILABLE` | 调用已完成，但无法重放结果。此次重试未再次收费。              | 查看原始收据或调用历史。新键会请求另一次调用，可能再次付款。    |
| `IDEMPOTENCY_ATTEMPT_STALLED`    | 原始付款记录仍在，但尝试在完成前停止了。                  | 核对所指的付款记录。重复此键不会重启调用；有意发起新调用需要新键。 |

## 确认是否成功

将键与操作记录一起保存，检查结果元数据及文本结果。已完成的重复请求应标明重放；仍在运行的重复请求应要求等待。即使网络随后失败，本地支出总额仍可能包含已记录的待签名金额；参阅[支出限额](/developers/zh/shi-yong-gong-ju-ru-men/spending-limits.md)。

<details>

<summary>可选：保留时间和故障边界</summary>

| 设置           | 服务器默认值                  | 影响                                                 |
| ------------ | ----------------------- | -------------------------------------------------- |
| 结果重放窗口       | 15 分钟；可通过部署配置           | 及时重试。保存的结果不是永久存储。                                  |
| 最大存储输出       | 262,144 字节 JSON；可通过部署配置 | 更大的结果会留下标记，重放时返回 `IDEMPOTENCY_RESULT_UNAVAILABLE`。 |
| 进行中请求的占用租期   | 120 秒                   | 超过该时长仍未完成的付款记录会报告为停滞。                              |
| 进行中请求的建议等待时间 | 1 秒                     | 优先使用返回的 `retryAfter` 值。                            |

付费调用还会对组织、智能体和幂等键执行持久化唯一性检查。对于仍有效的带键付款记录，临时重放存储丢失本身不会允许再次收费。条件允许时，服务器会从原始付款的验证记录重建近期结果。

这不是无限制的“恰好执行一次”保证。免费调用没有持久化付款记录兜底。已作废、过期或退款的付款记录会释放其键；后续调用可以再次执行。有意发起新操作时使用新键；在发起替代操作前，先核对结果不明的调用。

如果取消请求在签名前到达，取消调用可阻止代理签名，但不能撤回已发送的授权。首次签名后的付款请求未获签名，也不能证明首次付款失败。

</details>

## 错误

| 代码 / 消息                                    | 含义             | 解决方法                           |
| ------------------------------------------ | -------------- | ------------------------------ |
| `-32002`、**Assay did not answer in time.** | 转发的请求超时。       | 核对结果后，用已保存的键重试。                |
| `-32003`、**Can't reach Assay (…)**         | 网络交换失败。        | 恢复连接并复用已保存的键。                  |
| `MCP_IDEMPOTENCY_KEY_INVALID`              | 到达服务器的请求包含无效键。 | 使用上述可打印 ASCII 格式；本地代理通常会替换无效键。 |

## 延伸阅读

* [付费流程](/developers/zh/shi-yong-gong-ju-ru-men/how-paying-works.md)
* [支出限额](/developers/zh/shi-yong-gong-ju-ru-men/spending-limits.md)

## 下一步

* [assay-mcp CLI 参考](/developers/zh/shi-yong-gong-ju-jin-jie/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/zh/shi-yong-gong-ju-jin-jie/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.
