DeepSeek Search Everywhere MCP
# DeepSeek Search Everywhere MCP
[](https://github.com/radoxtech/deepseek-search-everywhere-mcp/actions/workflows/ci.yml)
[](LICENSE)

An [MCP](https://modelcontextprotocol.io) server that gives AI agents web search, page fetching and
multi-source research through the official DeepSeek Search & Fetch API.
It is a thin, careful layer over that API: results are deduplicated, content is capped to a budget you set,
repeated calls are served from an in-memory cache, and outgoing calls are paced by a local rate limiter.
Six tools, stdio by default, optional bearer-protected streamable HTTP.
It is a standard MCP server, so it works with **any MCP-compatible AI client or agent**: Claude Code,
Claude Desktop, Google Antigravity, Cursor, Windsurf, VS Code (GitHub Copilot), Gemini CLI, Codex CLI, Zed,
opencode and others. Local clients launch it over stdio; remote or shared setups can connect over HTTP.
<!-- mcp-name: io.github.radoxtech/deepseek-search-everywhere-mcp -->
> Not affiliated with DeepSeek. This is an independent, community-maintained project.
## Features
- **Search with content in one request**: `need_content` returns page text inline, so there is usually no
need for a second fetch per result.
- **Batch fetch and research**: `deepseek_fetch_many` fetches up to 10 URLs concurrently;
`deepseek_research` runs a search and fills the gaps by fetching pages whose search content is missing.
- **Result hygiene**: canonical-URL and title/site deduplication, per-result and total character budgets,
Markdown unescaping.
- **Optional query rewrite** to English through `deepseek-flash` (thinking disabled), plus an explicit `lang`
operator for non-English queries.
- **Cache and limiter**: TTL + LRU cache with single-flight, per-endpoint token buckets, `refresh` to bypass.
- **Safe by default**: static SSRF checks, secret redaction, no telemetry. See [Security](#security).
## Requirements
- Python 3.11 or newer
- A DeepSeek API key (`DEEPSEEK_API_KEY`) from the [DeepSeek platform](https://platform.deepseek.com)
## Install
The package is installed from GitHub; no registry release is required.
```bash
# Run without installing (uv)
uvx --from git+https://github.com/radoxtech/deepseek-search-everywhere-mcp deepseek-search-everywhere-mcp
# Or install with pip
pip install git+https://github.com/radoxtech/deepseek-search-everywhere-mcp.git
# Or from a source checkout
git clone https://github.com/radoxtech/deepseek-search-everywhere-mcp.git
cd deepseek-search-everywhere-mcp
pip install -e .
```
The HTTP transport needs the `http` extra (it pulls in `uvicorn`):
```bash
pip install "deepseek-search-everywhere-mcp[http] @ git+https://github.com/radoxtech/deepseek-search-everywhere-mcp.git"
# from a checkout: pip install -e ".[http]"
```
Check that the key and connectivity work (prints your balance, then exits):
```bash
export DEEPSEEK_API_KEY=sk-...
deepseek-search-everywhere-mcp --check
```
## Quick start
All examples use stdio. Replace `sk-...` with your key.
The server is client-agnostic: the examples below cover common clients, and any other MCP client
(Antigravity, Cursor, Windsurf, VS Code, Gemini CLI, Codex CLI, Zed, ...) works with the same
command, arguments and environment variable in its own MCP configuration format.
### Claude Code
```bash
claude mcp add deepseek-search-everywhere -e DEEPSEEK_API_KEY=sk-... -- \
uvx --from git+https://github.com/radoxtech/deepseek-search-everywhere-mcp deepseek-search-everywhere-mcp
```
### Claude Desktop
Edit `claude_desktop_config.json` (Settings, Developer, Edit Config):
```json
{
"mcpServers": {
"deepseek-search-everywhere": {
"command": "uvx",
"args": [
"--from",
"git+https://github.com/radoxtech/deepseek-search-everywhere-mcp",
"deepseek-search-everywhere-mcp"
],
"env": { "DEEPSEEK_API_KEY": "sk-..." }
}
}
}
```
### opencode
In `opencode.json`:
```json
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"deepseek-search-everywhere": {
"type": "local",
"command": [
"uvx",
"--from",
"git+https://github.com/radoxtech/deepseek-search-everywhere-mcp",
"deepseek-search-everywhere-mcp"
],
"environment": { "DEEPSEEK_API_KEY": "{env:DEEPSEEK_API_KEY}" },
"enabled": true
}
}
}
```
### Any stdio client
Most clients (including Antigravity, Cursor and Windsurf) use an `mcpServers` JSON block. Use the `uvx`
form from the Claude Desktop example, or, if the package is already installed, point the client at the
console script:
```json
{
"mcpServers": {
"deepseek-search-everywhere": {
"command": "deepseek-search-everywhere-mcp",
"env": { "DEEPSEEK_API_KEY": "sk-..." }
}
}
}
```
`python -m deepseek_search_everywhere_mcp` is equivalent.
### Streamable HTTP
For clients that connect over HTTP. A bearer token is **always** required; the server refuses to start without one.
```bash
pip install -e ".[http]" # or the pip/GitHub form shown above
export DEEPSEEK_API_KEY=sk-...
export MCP_BEARER_TOKEN="$(python -c 'import secrets; print(secrets.token_urlsafe(32))')"
deepseek-search-everywhere-mcp --transport streamable-http # http://127.0.0.1:8000/mcp
```
Options: `--host` (default `127.0.0.1`), `--port` (default `8000`), `--path` (default `/mcp`). The token is read
from the environment only, never from the command line. The server runs stateless and answers with JSON
(no SSE streams). Client entry:
```json
{
"mcpServers": {
"deepseek-search-everywhere": {
"url": "http://127.0.0.1:8000/mcp",
"headers": { "Authorization": "Bearer <MCP_BEARER_TOKEN>" }
}
}
}
```
Claude Code: `claude mcp add --transport http deepseek-search-everywhere http://127.0.0.1:8000/mcp --header "Authorization: Bearer $MCP_BEARER_TOKEN"`.
The server speaks plain HTTP; when binding beyond loopback, put TLS in front of it (see [SECURITY.md](SECURITY.md)).
## Tools
| Tool | Purpose | Parameters (defaults) |
|---|---|---|
| `deepseek_web_search` | Search; titles, URLs, dates, authority, rerank scores, optional page content | `query`, `count=10` (1-50), `ori_question`, `need_content=False`, `lang`, `max_chars_per_result=4000`, `max_total_chars=40000`, `dedupe=True`, `rewrite=False`, `refresh=False` |
| `deepseek_search_and_read` | Search with page content in one request; the recommended retrieval path | `query`, `count=5` (1-50), `ori_question`, `lang`, `max_chars_per_result=4000`, `max_total_chars=30000`, `dedupe=True`, `rewrite=False`, `refresh=False` |
| `deepseek_fetch` | Fetch one known URL as Markdown | `url`, `clean=True`, `max_chars=20000`, `refresh=False` |
| `deepseek_fetch_many` | Fetch 1-10 URLs concurrently (up to 5 at a time) | `urls`, `max_chars_per_url=4000`, `max_total_chars=20000`, `clean=True`, `refresh=False` |
| `deepseek_research` | One search with content, then fetch the sources whose content is missing or shorter than 200 characters (at most 5 fetches) | `question`, `max_pages=5` (1-50), `per_page_chars=6000`, `total_chars=30000`, `refresh=False` |
| `deepseek_balance` | Account balance | none |
Notes on the parameters:
- `query` and `question` are limited to 128 characters and support `site:`, `-site:`, `"phrase"`, `lang:`,
`after:` and `before:`. `ori_question` (max 128 characters) is the natural-language question and improves ranking.
- `lang` is an explicit language operator such as `es`, `de`, `ja` or `pt-BR`. Without it, results for
non-English queries may be mixed-language; for named entities, compare with and without it.
- With `dedupe=True`, searches ask the upstream for a few extra results (`max(2, ceil(count * 0.5))`, capped at 50)
and trim to `count` after removing duplicates, so you still get `count` results when the index returns repeats.
With `dedupe=False` exactly `count` are requested.
- `rewrite=True` translates the query to English with `deepseek-flash` before searching. It is skipped when `lang`
is set and falls back silently to the original query if the rewrite fails.
- `deepseek_fetch_many` skips empty entries, fetches duplicate URLs (same canonical URL) once, keeps the input
order, and reports a failing URL as an item with `ok: false` instead of failing the batch. All fetches share
a 120 s deadline; URLs still pending then are reported as `deadline_exceeded`. Each page gets at most
`min(max_chars_per_url, max_total_chars // number of URLs)` characters.
- `deepseek_research` does no summarising and makes no model call: it returns sources for the caller to synthesise.
Example calls:
```text
deepseek_search_and_read(query="rust async runtime comparison", count=5)
deepseek_web_search(query="gobierno de España presupuestos 2026", lang="es", need_content=False)
deepseek_fetch_many(urls=["https://example.com/a", "https://example.com/b"], max_total_chars=12000)
deepseek_research(question="how does HTTP/3 connection migration work", max_pages=4)
```
### Response envelope
Every tool returns a JSON object with `ok`.
| Field | Where | Meaning |
|---|---|---|
| `ok` | all | `true` on success, `false` on failure |
| `cached` | search, search-and-read, fetch; and per item in `deepseek_fetch_many` and per source in `deepseek_research` | `true` when the response came from the in-memory cache |
| `cached_count` | `deepseek_fetch_many`, `deepseek_research` | how many responses in this call were served from the cache |
| `requests_used` | `deepseek_fetch_many`, `deepseek_research` | upstream requests actually made (cache hits are free) |
| `notes` | all success envelopes | human-readable remarks: dedupe counts, rewrite applied or failed, `lang` applied, truncation ("Some page content was truncated to respect the character budget.") |
| `error` | failures | `{ "code", "message", "status"? }`; `status` is the upstream HTTP status when there was one |
| `partial` | `deepseek_fetch_many`, `deepseek_research` | `true` when some items or sources failed; each failed one carries its own `error`. The top-level `ok` stays `true`, so check `partial` |
| `failed_count` | `deepseek_fetch_many`, `deepseek_research` | number of failed items or sources |
Search results carry `title` (the URL host without `www.` when the upstream title is blank), `url`, `date`, `authority` (0-4, -1 unknown), `rerank_score`, `site`, `snippet`,
and `content`/`content_chars` or `summary` where available. `deepseek_fetch` returns `title`, `content`,
`content_chars`, `truncated`.
### Error codes
| Code | Meaning |
|---|---|
| `missing_api_key` | no key in `DEEPSEEK_API_KEY` or the fallback file |
| `ssrf_blocked` | URL rejected locally: internal host, literal private/loopback address, or embedded credentials |
| `bad_url`, `invalid_url`, `empty_url`, `url_too_long` | malformed or oversized URL |
| `empty_query`, `query_too_long`, `bad_count`, `bad_lang`, `bad_request` | invalid arguments |
| `rate_limited_local` | no local rate-limit token became available within the maximum wait |
| `response_too_large` | upstream body exceeded the cap (8 MiB search, 32 MiB fetch) |
| `deadline_exceeded` | batch item not finished within the shared batch deadline |
| `timeout`, `network_error`, `request_failed`, `invalid_json` | transport-level failures |
| `internal_error` | unexpected exception while fetching one batch item |
| other / `http_error` | upstream API errors keep the code from DeepSeek (or `http_error`) and set `error.status` |
## Cache and rate limiting
The cache and the limiter live in the server process and are not shared between processes.
| | Search | Fetch | Query rewrite |
|---|---|---|---|
| Cache TTL | 5 min | 30 min | 24 h |
| Entry limit | 128 | 128 | 1024 |
| Size limit | 16 MiB | 32 MiB | 1 MiB |
| Rate limit (sustained / burst) | 10 / s, 20 | 5 / s, 10 | not limited |
- Only successful, non-empty responses are cached. Errors are never cached.
- Identical concurrent calls share one upstream request (single-flight); only the one that made the request pays a limiter token, and none of them reports `cached: true`.
- `refresh=true` skips the cache read for that call and replaces the stored entry.
- `dedupe` changes the upstream request (the search over-fetches), so `dedupe=true` and `dedupe=false` calls do not share a cache entry.
- Cache hits and rewrite calls cost no limiter token. One logical call takes one token, retries included.
- If a token would take longer than 5 s to become available, the call fails immediately with `rate_limited_local`
rather than queueing indefinitely.
- Transient failures (HTTP 429 and gateway 5xx) are retried once.
- `DEEPSEEK_MCP_CACHE=0` disables all caches.
## Configuration
| Variable | Required | Purpose |
|---|---|---|
| `DEEPSEEK_API_KEY` | yes | DeepSeek platform key |
| `DEEPSEEK_BASE_URL` | no | API base URL (default `https://api.deepseek.com`) |
| `MCP_BEARER_TOKEN` | HTTP only | token clients must send as `Authorization: Bearer ...` |
| `DEEPSEEK_MCP_CACHE` | no | `1` (default) or `0` to disable caching |
| `DEEPSEEK_MCP_LOG_LEVEL` | no | stderr log level for the server, the MCP SDK and uvicorn: `DEBUG`, `INFO`, `WARNING` (default), `ERROR`, `CRITICAL` |
| `DEEPSEEK_AUTH_JSON` | no | path to a JSON file used as the key fallback (see [Security](#security)) |
`.env.example` lists the same variables. The server does not read `.env` files itself; export the variables or
set them in your MCP client's `env`.
## Security
Full policy and reporting instructions: [SECURITY.md](SECURITY.md).
- **Static SSRF checks only.** URLs are validated before the cache, the limiter or the network are touched:
non-http(s) schemes, internal hostnames, literal private/loopback/link-local addresses and URLs with embedded
credentials return `ssrf_blocked`. The server **does not resolve DNS**, and DeepSeek performs the actual fetch,
so this is not network isolation.
- **Key handling.** The key is sent only in the `Authorization` header to the configured API base URL. Keys,
bearer tokens and similar secrets are redacted from error messages and notes.
- **HTTP transport** requires the bearer token on every request (constant-time comparison, never logged).
- **No telemetry.** The only outbound destination is the DeepSeek API.
- **Legacy opencode fallback.** If `DEEPSEEK_API_KEY` is unset, the server reads the key from
`~/.local/share/opencode/auth.json` (`{"deepseek": {"key": "..."}}`), or from the file named by
`DEEPSEEK_AUTH_JSON`. This exists only for opencode users; prefer the environment variable.
- Tool inputs (queries, URLs) are sent to DeepSeek to do the work. Do not pass secrets in them.
## Limitations
- `deepseek_fetch` returns HTTP 404 for PDFs and Reddit and times out on some sites. Use
`deepseek_search_and_read` with the page title or a `site:`-scoped query instead.
- Fetch returns only title and content: no final URL, status code or publication date.
- Search and fetch only. No site mapping or crawling, no asynchronous jobs, a single provider (DeepSeek).
- The cache and rate limiter are per process; running several server processes multiplies the effective rate.
- Local rate limits are conservative defaults and do not replace DeepSeek's own quotas.
- The HTTP transport is plain HTTP, stateless and JSON-only; there are no OAuth and no SSE streams.
## Development
```bash
git clone https://github.com/radoxtech/deepseek-search-everywhere-mcp.git
cd deepseek-search-everywhere-mcp
python -m venv .venv && . .venv/bin/activate
pip install -e ".[test,lint,http]" build twine
pytest -q # offline suite; no network, no key needed
ruff check . # lint
python -m build # sdist + wheel in dist/
python -m twine check dist/* # validate package metadata
```
The offline tests clear `DEEPSEEK_*` variables and point the key fallback at a nonexistent file, so they cannot
reach the network. CI runs lint, tests and build on Python 3.11, 3.12 and 3.13.
### Live tests
The live suite calls the real DeepSeek API end to end and uses `deepseek-flash` as an LLM judge to grade whether
results are relevant and answer the question. It is excluded from the default run, skips itself without a key,
and a full run costs well under $0.01.
```bash
cp .env.example .env # then put your key in DEEPSEEK_API_KEY=
set -a && . ./.env && set +a # export the variables; tests do not read .env themselves
pytest -m live
```
Judge scores and reasons are printed after the run. `.env` is git-ignored; never commit it.
### Releasing
Releases are published by `.github/workflows/release.yml` when a `v*` tag is pushed. The workflow builds once,
checks that the tag equals the version in `pyproject.toml` and `server.json`, runs `twine check` and a clean-wheel
smoke test, publishes to PyPI through [Trusted Publishing](https://docs.pypi.org/trusted-publishers/) (OIDC, no
tokens), creates a GitHub Release with the artifacts and then updates the MCP Registry. Running the workflow
manually publishes to TestPyPI only.
```bash
# bump the version in pyproject.toml and server.json (tests/test_version_sync.py checks they agree), commit, then:
git tag v0.2.1 && git push origin v0.2.1
bash scripts/smoke_package.sh # optional local check: build, install into a clean venv, run the CLI
```
### Publishing to the MCP Registry manually
`server.json` describes the PyPI package for the [MCP Registry](https://modelcontextprotocol.io/registry). The
README carries an `mcp-name` comment that the registry uses to verify package ownership. The release workflow
does this automatically; to publish by hand:
```bash
python scripts/validate_server_json.py # structural checks; also runs `mcp-publisher validate` if installed
mcp-publisher login github # the io.github.radoxtech namespace requires GitHub authentication
mcp-publisher publish
```
Bump the version in `pyproject.toml` and `server.json` together and publish to PyPI first; `tests/test_version_sync.py`
fails when the versions drift apart.
## Changelog
See [CHANGELOG.md](CHANGELOG.md) for release notes.
## License
[MIT](LICENSE). Not affiliated with DeepSeek.
TDQS
Scored across 6 tools
Three tools (deepseek_web_search with need_content=True, deepseek_search_and_read, and deepseek_research) all perform search-plus-content retrieval and differ mainly by preset flags and internal fallback behavior rather than by distinct purpose. The descriptions do attempt to steer selection ('recommended retrieval path', 'search with need_content=True'), which mitigates but does not eliminate the overlap. Fetch vs fetch_many is clearly distinct.
All names use a uniform deepseek_ prefix with snake_case and are mostly verb- or verb_noun-shaped (web_search, search_and_read, fetch, fetch_many, research). deepseek_balance is noun-only, a minor deviation from the otherwise predictable pattern.
Six tools is well-scoped for a search-and-retrieval service, covering discovery, single fetch, batch fetch, a composite research call, and account balance. No bloat; the only questionable inclusion is search_and_read, which duplicates an existing flag.
The retrieval lifecycle is well covered: search, rank, read content inline, fan out over known URLs, and a gap-filling composite. Missing pieces are minor conveniences (e.g. a way to inspect rate-limit status or paginate beyond the 50-result cap), and documented caveats on deepseek_fetch give agents a workaround.