Skip to main content
Glama
Tom-Chencao

Tavily MCP Key Pool

by Tom-Chencao

Tavily MCP Key Pool

简体中文版 README: README.zh.md

Why? If you have multiple Tavily API keys (multiple accounts, a team budget, batch-purchased credits, …) and use them through an AI coding agent, you'll hit three problems fast:

  1. Single-key bottlenecks — one key's rate limit throttles everything.

  2. Silent failures — a key expires, hits a quota, or gets revoked, and your searches just... stop working.

  3. No visibility — you don't know which keys are being used or how much.

This project solves all three: a tiny MCP server that round-robins across your key pool, auto-deactivates dead keys, and exposes usage stats — so you can drop it into Claude Desktop, Cursor, DeepSeek Harness, or any MCP client without changing your workflow.

A Tavily MCP server with a SQLite-backed round-robin API key pool, built-in usage tracking, automatic health-based failover, and a standalone FastAPI dashboard. Standard MCP protocol — works with any MCP-compatible client (Claude Desktop, Cursor, DeepSeek Harness, etc.).

Highlights

  • 🔄 Round-robin key rotation across N Tavily API keys (SQLite, zero startup cost).

  • 📊 Usage tracking: per-key request count, error count, credits consumed.

  • 🩺 Automatic health check: probe all keys with a lightweight search, auto-deactivate dead ones; expose results via tavily_pool_status.

  • 🛠️ Six core MCP tools (Tavily parity: search, extract, crawl, map, research) plus tavily_pool_status and tavily_research_status (async fetch).

  • 🌐 Standalone FastAPI dashboard (CORS-enabled, loopback-only) with stats, per-key view, add/remove/deactivate/activate, and one-click health probe.

  • 🔌 Drop-in for any MCP client via stdio; the DSH integration is a one-page patch + an example client plugin (see examples/dsh-integration/).

How it differs from the official tavily-mcp

Feature

Official tavily-mcp

This repo

Single API key env var

Multiple keys, round-robin

✅ SQLite pool

Per-key usage stats

✅ request count + credits + errors

Health probe + auto-deactivation

Standalone dashboard

✅ FastAPI on 127.0.0.1:8000

MCP tools parity (search/extract/crawl/map/research)

✅ (plus the pool-status / research-status extras)

Async research polling

(manual)

✅ built-in tavily_research + tavily_research_status

Architecture

+--------------------------------------------------+
|  MCP clients (Claude Desktop / Cursor / DSH …)   |
+--------+---------------------+-------------------+
         | stdio (JSON-RPC)     | HTTPS / CORS
+--------▼--------------+     +▼-----------------------+
|  mcp_server.py (FastMCP)|     |  dashboard.py (FastAPI) |
|  + key_pool.py (SQLite) |     |  uvicorn 127.0.0.1:8000 |
+----------------------+--+     +-----+----------------+
                       |              |
                       v              v
                tavily_keys.db  <— SQLite-backed pool
                       |
                       v
              Tavily REST API (round-robin over N keys)

Quick Start

1. Install dependencies

python -m venv .venv
. .venv/bin/activate        # Linux/macOS
# or:  .venv\Scripts\Activate.ps1   (Windows PowerShell)
pip install -r requirements.txt

The pinned mcp constraint in requirements.txt is <2.0: see DSH integration / Pitfall #1 — the FastMCP import path moved in mcp 2.x.

2. Add API keys

Create a keys.txt with one key per line:

tvly-xxxxxxxxxxxxxxxx
tvly-yyyyyyyyyyyyyyyy

Then import them:

python cli.py add --from-file keys.txt

Or start the dashboard (next step) and paste them into the Add API Keys form. Keys are stored plaintext in tavily_keys.db (SQLite) so the pool can round-robin with zero startup cost — see Security.

3. Start the MCP server

For a direct stdio MCP server (any MCP client):

./run_mcp.sh                                # Linux/macOS
# or:  .venv\Scripts\python.exe mcp_server.py   (Windows)

The server announces seven tools; the public names in MCP-aware clients look like tavily_search, tavily_extract, etc.

4. Start the dashboard (optional, independent process)

./run_dashboard.sh                          # default port 8000
# or:  .venv\Scripts\python.exe -m uvicorn dashboard:app --host 127.0.0.1 --port 8000

Open http://127.0.0.1:8000 in your browser. The dashboard is CORS-enabled for loopback origins so an embedded settings panel in another UI can call it.

MCP Tools

Tool

Purpose

tavily_search

Web search (basic/advanced, topic, time range, include/exclude domains, country, etc.)

tavily_extract

Extract clean content from URLs

tavily_crawl

Crawl a website and extract content from multiple pages

tavily_map

Discover URLs on a site (faster than crawl)

tavily_research

AI deep research (30–120s+; uses background polling internally — see Pitfall #2)

tavily_pool_status

Pool stats: active keys, total requests/errors/credits, recent 24h breakdown

tavily_research_status(request_id)

Fetch the result of an async research task that timed out

CLI

python cli.py list                 # all keys
python cli.py list --active        # only active
python cli.py stats                # JSON dump of pool state
python cli.py health               # probe every active key; deactivate dead ones
python cli.py recent -n 20         # recent request log
python cli.py add tvly-... [...]   # add one or more keys
python cli.py add --from-file keys.txt
python cli.py activate tvly-xx****yy     # masked id, see `list`
python cli.py deactivate tvly-xx****yy --reason "manually disabled"
python cli.py remove tvly-xx****yy

Using with Claude Desktop / Cursor / other generic MCP clients

For any client that accepts an MCP stdio command:

{
  "mcpServers": {
    "tavily": {
      "command": "/absolute/path/to/.venv/bin/python3",
      "args": ["mcp_server.py"],
      "cwd": "/absolute/path/to/this/repo"
    }
  }
}

Or streamable HTTP if your client supports it and you've wrapped the server in an HTTP transport yourself — out of scope for this repo.


DeepSeek Harness (DSH) Integration

Tested with @deepseek-ai/dsh 0.1.0-rc.6 (web profile).

The DeepSeek Harness (dsh) uses the Cordis plugin framework and ships with an official MCP client bridge (@deepseek-ai/dsh-mcp-client). The integration is therefore very thin: one user-patch layer + an example browser-side plugin (this repo's examples/dsh-integration/client-tavily-panel/).

A. Register the Tavily MCP server in DSH

Edit ~/.dsh/profiles/web/cordis.patch.yml (the user-patch layer applied after every bundle). Add a new insert block — the values below assume the repo lives at C:\Users\ASUS\.dsh\tavily-pool\:

- insert:
    - id: mcp-tavily
      name: '@deepseek-ai/dsh-mcp-client'
      config:
        transport: stdio
        serverName: tavily
        command: 'C:\Users\ASUS\.dsh\tavily-pool\.venv\Scripts\python.exe'
        args: ['mcp_server.py']
        cwd: 'C:\Users\ASUS\.dsh\tavily-pool'
        # research can take >2 minutes on big topics; the default 30s is too tight
        toolCallTimeoutMs: 600000
        failOnStartupError: false

Verify the merge with dsh --profile web --dump-config before restarting. The MCP server appears then as mcp__tavily__tavily_search (etc.) in the agent's tool list.

B. (Optional) Embed the dashboard in DSH settings

Copy examples/dsh-integration/client-tavily-panel/ anywhere on disk. The example uses @deepseek-ai/dsh-client-ui-slots' settings.section slot — the plugin registers a Tavily 号池 panel that calls the dashboard over fetch. To install:

  1. Place the package (e.g. ~/.dsh/plugins/client-tavily-panel/).

  2. Link it into the profile's node_modules so require.resolve can find it (DSH loads client plugins through its package-name resolution chain):

    New-Item -ItemType Junction `
      -Path "$env:DSH_HOME\profiles\node_modules\dsh-client-tavily-panel" `
      -Target "C:\Users\ASUS\.dsh\plugins\client-tavily-panel"

    Junction (not symlink) avoids needing admin rights. If you skip this and pnpm add the package locally, fine — but watch out: pnpm may stall on any other unrelated file: / GitHub-source dependencies in your profile.

  3. Add a roster entry to cordis.patch.yml:

    - insert:
        - id: client-tavily-panel
          name: 'dsh-client-tavily-panel'
  4. Restart dsh web. (See Pitfall #6 — HMR is intentionally disabled for the web profile; patch changes only load on full restart.)

After restart, open ⚙️ Settings — the Tavily 号池 entry appears in the left navigation.

Pitfalls hit during the DeepSeek integration

These are real errors I (the original integrator) hit. Read these before you start, in the order below — each one wasted time.

Pitfall #1: mcp SDK versioning

mcp_server.py does from mcp.server.fastmcp import FastMCP. That module was removed in mcp 2.0 (the FastMCP implementation moved to a separate fastmcp package with a different API). If you run pip install mcp and grab the latest, the MCP server refuses to start:

ModuleNotFoundError: No module named 'mcp.server.fastmcp'

Pin it:

# requirements.txt
mcp>=1.0.0,<2.0.0

Tested with mcp 1.29.0.

Pitfall #2: tavily_research is async and bound to the creating key

Three sub-bugs in one:

  • The tavily-python SDK renamed research()'s first positional arg from query to input. Calling client.research(query=…) fails with missing 1 required positional argument: 'input'.

  • The SDK enforces model ∈ {"mini", "pro", "auto"} at runtime, but the Tavily REST API itself accepts model=standard|pro. Passing standard raises model must be one of: mini, pro or auto.

  • research() returns a status: pending envelope immediately — the actual result arrives 30–120+ seconds later. You must poll get_research(request_id) until status == "completed". Otherwise the tool always returns "pending" and your model thinks the call failed.

  • The research task is bound to the API key that created it. Other keys in the pool cannot fetch the result (returns 404). Always poll with the same TavilyClient instance — do not re-call pool.next_key() on each poll iteration, or you'll keep hitting the wrong keys.

This repo's tavily_research already wraps the full lifecycle: poll for up to ~570s, then return a status: timeout envelope with the request_id so the caller can fetch later. A second tool, tavily_research_status(request_id), walks the active-key list to find the right key for an ad-hoc fetch — needed because the tool call may have timed out on a different process.

Pitfall #3: dashboard.py UTF-8 read bug on Windows

dashboard.py does:

DASHBOARD_HTML = TPL.read_text()

Path.read_text() defaults to locale.getpreferredencoding(), which is GBK on Windows (zh-CN). The bundled templates/dashboard.html is UTF-8 and contains CJK characters, so the dashboard raises:

UnicodeDecodeError: 'gbk' codec can't decode byte 0xb6 in position 4308

Fix:

DASHBOARD_HTML = TPL.read_text(encoding="utf-8")

Pitfall #4: Cross-platform paths in run_*.sh

run_mcp.sh and run_dashboard.sh hard-code .venv/bin/python3 (Linux conventions) and were never tested on Windows. The script authors also shipped a systemd unit using /home/user/code/Tavily — clearly Linux-only.

You do not need these scripts at all on Windows; just invoke the .venv\Scripts\python.exe directly (see the YAML above). They're kept in the repo for the original Linux use case.

Pitfall #5: DSH patch config is loaded only at startup

cordis.patch.yml is read when the web profile boots. Changes do not hot-reload — the hmr row in the web-app bundle patch is intentionally disabled:

- id: hmr
  disabled: true
# TODO: Re-enable shared HMR for Web after its reload lifecycle is tested.

So after every edit to cordis.patch.yml, restart dsh web (see Pitfall #6 for how to do this safely).

Use dsh --profile web --dump-config to verify your patch merges correctly without actually booting the GUI. It's much faster than starting, checking the GUI, killing, fixing, repeating.

Pitfall #6: How to restart dsh web without killing yourself

dsh web is the host process that runs this conversation, including your tool process. If you naively run

Stop-Process -Id <dsh-web-pid> -Force
Start-Process dsh.cmd web

from a pwsh that the same dsh web spawned, you will kill yourself mid-command before the new instance ever starts. The first time I tried, the PowerShell session aborted with exit code 4294967295 and nothing happened.

The fix: hand the restart to Windows Task Scheduler, which runs the script under svchost (not under dsh web):

$script = "$env:TEMP\dsh_restart.ps1"
@"
Start-Sleep -Seconds 8
Stop-Process -Id <dsh-web-pid> -Force
Get-CimInstance Win32_Process |
  Where-Object { `$_.CommandLine -match 'dsh web' } |
  ForEach-Object { Stop-Process -Id `$_.ProcessId -Force }
Start-Sleep -Seconds 3
Start-Process 'C:\…\dsh.cmd' web -WorkingDirectory 'H:\…' -WindowStyle Hidden
"@ | Out-File $script -Encoding utf8

schtasks /create /tn dsh-restart /tr "powershell -NoProfile -File $script" /sc once /st 23:59 /f
schtasks /run /tn dsh-restart
schtasks /delete /tn dsh-restart /f

Then you have ~8 seconds to return your final answer before the old instance dies. Tell the user to refresh http://127.0.0.1:3080 after 20–30 seconds.

Pitfall #7: Migrating the tool directory while the MCP server is running

DSH's mcp-client reconnects on connection loss with exponential backoff (initialDelayMs 500, maxAttempts 10). Killing the Python child process triggers a reconnect — which spawns a new child immediately. If you then try to Move-Item the directory, the new .venv\Scripts\python.exe has the file locked and robocopy fails with [Result: 32] / "being used by another process".

Two viable strategies:

  • Copy first, then delete source. Copy-Item reads locked files via Windows file-sharing; it does not need exclusive access. After the copy succeeds, kill the old MCP server + remove the source. .venv is fully relocatable as long as pyvenv.cfg's home = line still points to the same base Python install.

  • Loop kill + robocopy /MOVE until it succeeds within the backoff window. Ugly but works.

The original migration used:

Copy-Item -Path D:\Downloads\Tavily -Destination C:\Users\ASUS\.dsh\tavily-pool -Recurse -Force
# verify copy
Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match 'python.exe' -and $_.CommandLine -match 'mcp_server' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
# loop until deletion succeeds
for ($i=0; $i -lt 8; $i++) {
  Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match 'mcp_server' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
  Start-Sleep -Milliseconds 200
  Remove-Item D:\Downloads\Tavily -Recurse -Force -ErrorAction SilentlyContinue
  if (-not (Test-Path D:\Downloads\Tavily)) { break }
  Start-Sleep -Seconds 2
}

Pitfall #8: pnpm add may stall on unrelated dependencies

When you run dsh plugin --profile web add <dir> to install a local plugin, pnpm resolves the whole profile workspace — including any GitHub-sourced or HTTP-sourced bundles your package.json lists. If your profile already includes something like dsh-files: https://codeload.github.com/...tar.gz/... and that download stalls (firewall, DNS, cold cache, registry quota), your local plugin never installs and pnpm hangs for the full timeout.

Workaround: skip pnpm and create the resolution yourself:

New-Item -ItemType Junction `
  -Path "$env:DSH_HOME\profiles\node_modules\dsh-client-tavily-panel" `
  -Target "<absolute path to your plugin package>"

Junctions (not symlinks) work without admin rights and behave identically for require.resolve. The patch layer then references the package by its name field, exactly as if pnpm had installed it.

Pitfall #9: Settings-panel client plugin format

If you write your own DSH client plugin (browser side), the runtime format is not ESM, not Cordis-from-source. The dsh-client-modules plugin hosts a small in-memory module loader and fetches each client bundle from /plugins/<id>/client.js. The bundle must call:

window.__ModuleLoader__.load({
  id: "your-package-name",   // matches package.json "name"
  factory: (require) => {
    var module = { exports: {} };
    var exports = module.exports;
    var react = require("react");           // available
    var jsx = require("react/jsx-runtime"); // available
    // ... define components ...
    function apply(ctx) {
      ctx.slots.inject("settings.section", () => ctx.slots.register({
        name: "settings.section",
        id: "your-id",
        order: 100,
        label: "Your Label"
      }, YourComponent));
    }
    exports.apply = apply;
    exports.inject = ["slots"];             // services you depend on
    return module.exports;
  }
});

And your package.json must include:

{
  "main": "lib/index.js",
  "exports": { "./client": { "default": "./lib/client.js" } },
  "dsh": { "client": { "inject": ["@deepseek-ai/dsh-client-ui-slots"], "platform": "web" } }
}

lib/index.js is the host entry — it runs server-side; it can be a no-op (function apply() {}; export { apply };).


Security

  • Plaintext keys at rest. tavily_keys.db stores your Tavily API keys in cleartext because the SQLite-backed pool is queried on every request. Protect the file with filesystem permissions (Linux: chmod 600). Never commit tavily_keys.db (see .gitignore).

  • Loopback-only dashboard by default. dashboard.py binds 127.0.0.1:8000. If you expose it on a LAN, add authentication immediately.

  • CORS is wide-open on purpose — the dashboard is meant to be called by embedded UIs on the same host. This is safe because of the loopback bind, but if you change the bind address, narrow CORSMiddleware.allow_origins to match.

  • Rotating a leaked key: python cli.py remove tvly-xxxxxxxx****yyyy, revoke it in the Tavily dashboard, repeat for each row in the pool.

Troubleshooting

Symptom

Cause / fix

ModuleNotFoundError: No module named 'mcp.server.fastmcp'

mcp is ≥ 2.0; pin to <2.0 (Pitfall #1)

TavilyClient.research() missing 1 required positional argument: 'input'

Old-style call — mcp_server.py already uses input= (Pitfall #2)

model must be one of: mini, pro or auto

SDK-level restriction, mapped to auto in this repo (Pitfall #2)

Research always returns pending

Did you call get_research after research? This repo does it for you

UnicodeDecodeError: 'gbk' codec can't decode…

Dashboard HTML read bug (Pitfall #3); fixed in this repo

node.exe and python.exe files locked during move

Kill MCP server, copy first, delete after (Pitfall #7)

Tools registered but DSH session doesn't see them

Did you restart dsh web? Patches only load on startup (Pitfall #5)

__DSH_BOOT__ doesn't list your plugin

Junction/require-resolve issue (Pitfall #8); verify with dsh --profile web --dump-config

Credits

The pool management code (key_pool.py, dashboard.py, the FastMCP mcp_server.py skeleton, cli.py) was originally written by an unattributed author and shared publicly. This repository adds:

  • mcp 1.x compatibility (queryinput, model mapping, research polling).

  • A new tavily_research_status tool for async fetch.

  • Windows cross-platform fixes (UTF-8 read in dashboard.py).

  • A drop-in settings-panel client plugin for DSH, and the integration pitfall log above.

If you know the original author, please open an issue so I can add a credit.

License

MIT. See LICENSE.

-
license - not tested
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

  • One API key for 6 AI models. Pay-per-use. MCP protocol support with web search.

  • Web search for AI agents — one tool across 6 engines, routed to the cheapest + cached.

  • Zenrows MCP server — Fetch, Extract, Batch, and Browser Sessions for AI coding assistants

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Tom-Chencao/a-beginner-s-warehouse'

If you have feedback or need assistance with the MCP directory API, please join our Discord server