跳到主要内容

Tavily Pool MCP Gateway —— 多 Key 池化调度的 MCP 网关

项目地址:github.com/yjkz/tavily-mcp

项目解决什么问题?

手上有多个 Tavily API key(比如几十个免费账号,每个 1000 credits/月),但每次只能用一个,用完了就得手动换。Tavily Pool MCP Gateway 就是为了解决这个问题:

  1. 对外只暴露一个 MCP 服务地址,任何 MCP 客户端(Claude Code / Cursor 等)直接接入
  2. 多个 key 自动轮流使用,单 key 限流或耗尽时无感切换,容量叠加
  3. 部署在公网时有鉴权、限流、管理控制台

用一句话概括:把一堆零散的 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://你的域名/ 登录控制台:

  1. Key 池 → 批量添加:粘贴你的 Tavily key,点「测试连接」验证
  2. 访问 Token → 创建 Token:选等级、限额,复制 tpm_...
  3. 在 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)

暂无评论,来抢沙发吧!

发表评论