Tavily Pool MCP Gateway —— 多 Key 池化调度的 MCP 网关
项目解决什么问题?
手上有多个 Tavily API key(比如几十个免费账号,每个 1000 credits/月),但每次只能用一个,用完了就得手动换。Tavily Pool MCP Gateway 就是为了解决这个问题:
- 对外只暴露一个 MCP 服务地址,任何 MCP 客户端(Claude Code / Cursor 等)直接接入
- 多个 key 自动轮流使用,单 key 限流或耗尽时无感切换,容量叠加
- 部署在公网时有鉴权、限流、管理控制台
用一句话概括:把一堆零散的 Tavily 免费 key 变成一个高可用的统一搜索服务。
架构总览
MCP 客户端 (Claude Code / Cursor / …)
│ Authorization: Bearer tpm_xxx
▼
┌──────────────────────────────────────────────┐
│ Nginx (你的域名, HTTPS) │
│ └─ 单进程网关 (仅监听 127.0.0.1:8000) │
│ ├─ /mcp MCP Streamable HTTP 端点 │
│ ├─ / Dashboard 控制台 (密码登录) │
│ ├─ /api/* 管理 API (会话鉴权) │
│ └─ /health 健康检查 │
│ │
│ Key 池调度器: │
│ 轮询 → 429 冷却 → 432/433 标记耗尽 → │
│ 401 禁用 → 自动换 key 重试 │
│ 每 6h 调 GET /usage 校准真实配额 │
└──────────────────────────────────────────────┘
│ 轮流使用, 均摊消耗
▼
Tavily API (key1 / key2 / key3 / …)
单进程架构:MCP 服务、管理 API、静态前端共用一个 8000 端口,由 Starlette(FastMCP http_app() 提供)+ uvicorn 驱动。部署面最小——一个容器搞定一切。
核心功能
Key 池调度——项目心脏
Key 池是整个项目最核心的模块(app/pool.py),本质上是一个状态机。
每个 Key 有四种状态:
[*] → active 添加 key
active → cooling 429 触发 (冷却 60s)
cooling → active 冷却到期 (懒判定,无需后台任务)
active → exhausted 432/433 上游标记 或 本地额度用尽
exhausted → active usage 校准有余额 或 月初滚动恢复
active → disabled 401 (无效 key)
cooling → disabled 401
exhausted → disabled 401
disabled → active 控制台手动启用
调度策略:
- 轮询均摊:
acquire()加锁轮询,跳过非 active 的 key - 429 冷却:设置
cooldown_until = now + 60s,到期由effective_status懒恢复(不需要后台任务) - 432/433 耗尽:标记 exhausted,月初自动滚动 + usage 校准恢复
- 401 无效:直接禁用,控制台告警
- 5xx 网络错误:只记录
last_error,不惩罚 key 状态,换一个重试 - 4xx 参数错误:直接透传给客户端,不浪费重试
关键设计:
effective_status是计算属性,冷却到期无需任何后台任务即时生效。acquire()只认effective_status == "active"。
付费 key 兼容
免费 key 默认 1000 credits/月,但 Tavily 的 /usage 接口有个坑:只有 key 配了独立限额时才返回 key 级数字,否则 key.usage 恒 0、key.limit 为 null,真实配额在 account.plan_usage / plan_limit。校准时会自动回退到账户级,付费 key 完全兼容。
MCP 工具
提供 6 个工具:
| 工具 | 说明 | 权限要求 |
|---|---|---|
tavily_search |
网页搜索 | standard |
tavily_extract |
网页内容提取 | standard |
tavily_crawl |
网站爬取 | full |
tavily_map |
站点地图生成 | full |
tavily_research |
异步深度研究(耗时最长数分钟) | full |
get_my_usage |
客户端自查用量 | 所有 Token |
其中 tavily_research 是异步的:提交后返回 request_id,然后轮询同一 key 直到完成。
鉴权与 Token 治理
双种鉴权方式:
# 方式一:URL 带 Token(推荐,和 Tavily 官方 MCP 同款)
https://域名/mcp?token=tpm_xxx
# 方式二:Authorization 请求头
Authorization: Bearer tpm_xxx
Token 安全设计:
- 服务器只存 SHA-256 哈希,明文签发后永不直接落库
- 支持 Fernet 加密导出(
token_enc字段),用SESSION_SECRET派生密钥 - 吊销即时生效,全部客户端立即失效
- 每个 Token 独立配置:RPM 限流 / 日请求配额 / 月 Credits 上限 / 工具白名单(比 standard/full 等级更细粒度的按工具开关)
告警通知
告警采用队列化设计:请求热路径只做「配置检查(60s 缓存)+ 去重判断 + 入队」,实际发送由后台 alerts-sender 任务完成,发送失败不阻塞工具调用。
| 事件 | 触发点 | 默认开关 | 去重间隔 |
|---|---|---|---|
key_disabled |
401 无效 | 开 | 同 key 30 分钟 |
key_exhausted |
配额耗尽 | 关 | 同 key 30 分钟 |
pool_exhausted |
全池不可用 | 开 | 10 分钟 |
pool_recovered |
死池恢复 | 开(翻转即发) | — |
pool_low_active |
可用 key ≤ 阈值 | 关 | 10 分钟 |
pool_low_remaining |
全池剩余 ≤ 阈值 | 关 | 10 分钟 |
支持 5 个推送渠道:飞书 / 企业微信 / 钉钉(可选 HMAC 签名)/ 通用 Webhook / 邮件(SMTP,支持 163/QQ 授权码)。
Dashboard 控制台
中文界面,默认深色主题。6 个页面:
- 概览:今日请求 / 本月 Credits / Key 池健康 / 14 天趋势图 / 池配额总览
- Key 池:卡片管理、零成本「测试连接」(验证 key 有效性并校准真实剩余配额)、「全量校准」(免等 6h 批量校准)、批量粘贴添加
- 访问 Token:签发(明文仅显示一次)、吊销、限额调整、一键导出 MCP 地址
- 请求日志:按 Token/状态/工具筛选,含查询内容和来源 IP,保留 30 天
- 设置:公告 / 站名 / 图标 / 改密码 / 告警配置
- 登录:失败 5 次锁定 10 分钟,会话签名 Cookie(HttpOnly)
数据模型
SQLite(WAL 模式)四张表:
tavily_keys Key 池状态
├── id, key (明文), label
├── status: active | cooling | exhausted | disabled
├── cooldown_until, credits_used_month, plan_limit
├── monthly_reset_at, total_requests
└── last_usage_json (原始 /usage 响应)
access_tokens 下发给客户端的凭证
├── id, name
├── token_hash (SHA-256, 唯一)
├── token_enc (Fernet 密文, 可导出)
├── prefix (明文前 12 位, 列表识别)
├── tier: standard | full
├── allowed_tools (逗号分隔白名单, NULL=不限)
└── rpm_limit, daily_quota, monthly_credits_limit
request_logs 审计与统计(限额数据源)
├── ts, token_id, tool, query (截 200 字)
├── tavily_key_id, status, http_status
├── credits, latency_ms, client_ip
└── 3 个索引: ts / (token_id, ts) / (tavily_key_id, ts)
settings 键值表 (UPSERT)
└── admin_password_hash, site_name, announcement,
alert_channel, alert_webhook_url, ...
外键是逻辑上的(未声明约束),key/token 删除后历史日志保留。
技术栈
| 层 | 选型 |
|---|---|
| MCP 服务 | Python 3.12+ / FastMCP 3.4(锁定 >=3.4,<4) |
| Web 容器 | Starlette(FastMCP http_app())+ uvicorn |
| HTTP 客户端 | httpx(异步,上游 Tavily) |
| 存储 | SQLite(aiosqlite,WAL 模式,单连接) |
| 加密 | cryptography(Fernet)/ hashlib(PBKDF2 + SHA-256) |
| 前端 | React 19 + TypeScript + Vite + Tailwind v4 + shadcn/ui + recharts |
| 部署 | Docker 多阶段构建 + Nginx 反代 |
| CI/CD | GitHub Actions → 腾讯云 TCR → 服务器 compose |
| 监控 | prometheus-client(/metrics 端点) |
Docker 构建解析
# Stage 1: 构建前端
FROM node:22-slim AS frontend
# npm ci → npm run build → 产物在 /build/dist/
# Stage 2: 运行时
FROM python:3.12-slim AS runtime
# uv sync --frozen --no-dev (依赖缓存层)
# COPY app/ + 前端产物
# 非 root 用户 appuser 运行
# /data 是 SQLite 卷挂载点
# HEALTHCHECK /health 端点
两阶段构建:Node 构建前端 → Python 运行时只包含 app/ + 静态产物。最终镜像体积极小。
配置项
全部通过环境变量配置,有合理默认值:
| 变量 | 默认值 | 说明 |
|---|---|---|
ADMIN_PASSWORD |
必填(dev 模式默认 admin) |
管理员密码 |
HOST / PORT |
0.0.0.0 / 8000 |
监听地址 |
KEYPOOL_COOLDOWN_SECONDS |
60 |
429 冷却时长 |
USAGE_SYNC_INTERVAL_HOURS |
6 |
配额校准频率 |
LOG_RETENTION_DAYS |
30 |
日志保留天数 |
DEFAULT_TOKEN_RPM |
30 |
默认 Token RPM |
KEYPOOL_MAX_RETRIES |
4 |
最大重试次数 |
CHARACTER_LIMIT |
25000 |
输出截断长度 |
TAVILY_POOL_DEV |
— | 设为 1 开启开发模式 |
后台任务
| 任务 | 周期 | 逻辑 |
|---|---|---|
usage_sync_loop |
6h(首次启动立即跑) | 遍历非 disabled key 调 GET /usage,校准/恢复。单个 key 失败不影响其他 |
maintenance_loop |
6h | 月度滚动 + 清理过期日志 |
两个循环都整体 try/except:单轮异常只记日志,下一周期重试。
快速开始
# 1. 克隆项目
git clone https://github.com/yjkz/tavily-mcp.git && cd tavily-mcp
# 2. 配置
cp .env.example .env
# 编辑 .env,设置 ADMIN_PASSWORD
# 3. 启动
docker compose up -d --build
Compose 会把服务发布为 127.0.0.1:8000。Nginx 反代配置:
location / {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off; # MCP Streamable HTTP 长连接所需
proxy_read_timeout 3600s;
}
然后打开 https://你的域名/ 登录控制台:
- Key 池 → 批量添加:粘贴你的 Tavily key,点「测试连接」验证
- 访问 Token → 创建 Token:选等级、限额,复制
tpm_... - 在 MCP 客户端里配置使用
客户端接入示例
Claude Code:
claude mcp add --transport http tavily-pool \
"https://你的域名/mcp?token=tpm_xxx"
Cursor / 通用 MCP JSON:
{
"mcpServers": {
"tavily-pool": {
"url": "https://你的域名/mcp?token=tpm_xxx"
}
}
}
控制台创建 Token 成功时会直接给出可复制的 MCP 地址和 Claude Code 命令。
代码规模
- 后端:约 3,000 行 Python(11 个业务模块),单文件最大
mcp_server.py(919 行)和admin_api.py(808 行) - 前端:6 个页面 + Layout,构建产物约 837 KB(gzip 250 KB)
- 测试:7 个文件、56 个用例,无网络依赖(上游用可脚本化的 FakeTavily)
安全须知
- Tavily key 明文存于服务器 SQLite(需原样转发上游),靠服务器权限保护。确保
.env、data/目录权限,不要提交进仓库 - 面向公网务必:强
ADMIN_PASSWORD+ HTTPS(由 Nginx 终止 TLS) SESSION_SECRET首次启动生成并写文件持久化到data/session_secret,重启后会话与 Token 加密不失效- Token 泄露时在控制台吊销即可,全部客户端立即失效
- RPM / 日配额 / 月 Credits 三层限额是防滥用的止损开关
本地开发
# 后端
uv sync
TAVILY_POOL_DEV=1 uv run uvicorn app.main:app --port 8000
# 前端(另开终端)
cd dashboard && npm install && npm run dev
# Vite 开发服务器 http://localhost:5173
# /api 与 /mcp 已代理到 8000
# 测试
uv run python -m pytest
评论 (0)
暂无评论,来抢沙发吧!
