Skip to main content
Glama
Sylvan-Cheng

mcp-key-rotator

by Sylvan-Cheng
README.md
# mcp-key-rotator

本机代理。官方 Tavily / Firecrawl MCP 仍由 `npx` 启动,密钥轮换发生在它们发出的 HTTP 请求之前。

额度按账号计算。同一账号里的多把 key 共用一个额度池,轮换没有效果。每把 key 应来自各自独立的账号。

## 行为

| 状态 | 含义 | 处理 |
| --- | --- | --- |
| 402、432、433 | 额度用尽 | 这把 key 停用到账单重置,换下一把重试 |
| 429 | 频率限制 | 按 `retry-after` 冷却,最长 1 小时,然后换下一把 |
| 401 | key 无效 | 停用,直到进程重启 |
| 400、403、404、5xx | 参数、权限或上游故障 | 原样返回,不换 key |

数组里的第一把 key 是主 key,后面的是副 key。也可以写 `"role": "primary"`,它会被排到最前,一个池里只能有一把。主 key 可用时每次都用它。

主 key 失败后记一个恢复时间,写到 key 文件旁边的 `state.json`。在此之前新请求直接走副 key,并发请求只让一路去探测失败的主 key。Tavily 的额度在每个自然月 1 日(UTC)恢复,接口不返回这个日期。Firecrawl 返回 402 时再读一次 `GET /v2/team/credit-usage`,用 `billingPeriodEnd`。429 只冷却 `Retry-After` 那段时间,连续触发会把冷却加倍,最长 1 小时。

搜索、抓取这类被拒绝的请求会换 key 重发。已经返回 200 的请求不会重发。响应里的 `id` / `request_id` 会钉在创建它的那把 key 上,之后的轮询一直用它。Tavily research、Firecrawl crawl 都是这样。响应里指向 `api.firecrawl.dev` 或 `api.tavily.com` 的链接会改写回本机代理。

代理只监听 `127.0.0.1`。日志里的 key 只保留末四位。

## 准备

```powershell
cd D:\Github\mcp-key-rotator
npm install
npm test
npm run build
```

把 `keys.example.json` 复制到 `%USERPROFILE%\.grok\key-rotator\keys.json`:

```json
{
  "tavily": [
    { "label": "primary", "key": "tvly-...", "role": "primary" },
    { "label": "secondary", "key": "tvly-...", "role": "secondary" }
  ],
  "firecrawl": [
    { "label": "primary", "key": "fc-...", "role": "primary" },
    { "label": "secondary", "key": "fc-...", "role": "secondary" }
  ]
}
```

字符串和带 `label` 的对象都可以。不写 `role` 时,数组顺序就是优先级。

环境变量:

| 变量 | 默认 | 作用 |
| --- | --- | --- |
| `KEYS_FILE` | 当前目录的 `keys.json`,否则 `%USERPROFILE%\.grok\key-rotator\keys.json` | key 文件 |
| `ROTATOR_PORT` | `8788` | 监听端口 |
| `CREDIT_RESET_DAY` | `1` | Firecrawl 读不到 `billingPeriodEnd` 时的回退日 |

直接跑代理:

```powershell
$env:KEYS_FILE = "$env:USERPROFILE\.grok\key-rotator\keys.json"
npm start
```

`GET http://127.0.0.1:8788/healthz` 返回各池剩余可用 key 数量,不返回密钥。改完 `keys.json` 要重启代理。

## 接到 Grok

`~/.grok/config.toml`:

```toml
[mcp_servers.firecrawl]
command = "node"
args = ["D:\\Github\\mcp-key-rotator\\dist\\launch.js", "firecrawl"]
enabled = true
startup_timeout_sec = 90

[mcp_servers.firecrawl.env]
KEYS_FILE = "C:\\Users\\Ruixi\\.grok\\key-rotator\\keys.json"

[mcp_servers.tavily]
command = "node"
args = ["D:\\Github\\mcp-key-rotator\\dist\\launch.js", "tavily"]
enabled = true
startup_timeout_sec = 90

[mcp_servers.tavily.env]
KEYS_FILE = "C:\\Users\\Ruixi\\.grok\\key-rotator\\keys.json"
```

`launch.js firecrawl` 把官方 `firecrawl-mcp` 的 `FIRECRAWL_API_URL` 指到本机。`launch.js tavily` 给官方 `tavily-mcp` 加上 `dist/redirect.js`:那个包把地址写死成 `https://api.tavily.com`,垫片把这些请求改到本机,代理再同时替换 `Authorization` 和 JSON 里的 `api_key`。

两个进程会共用一个已经在听的代理。代理是独立进程,MCP 退出后它还在。停掉它:

```powershell
Get-NetTCPConnection -LocalPort 8788 -State Listen |
  ForEach-Object { Stop-Process -Id $_.OwningProcess }
```

改完配置后新开一个 Grok 会话。