Skip to main content
Glama
evilwk

DeepSeek Search MCP

by evilwk
README.md
# deepseek-search-mcp

[![CI](https://github.com/evilwk/deepseek-search-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/evilwk/deepseek-search-mcp/actions/workflows/ci.yml)
[![npm](https://img.shields.io/npm/v/@evilwk/deepseek-search-mcp.svg)](https://www.npmjs.com/package/@evilwk/deepseek-search-mcp)
[![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

> **Unofficial.** A community project. Not affiliated with, endorsed by, or supported by DeepSeek. It only calls the public [Search / Fetch REST APIs](https://api-docs.deepseek.com/zh-cn/guides/search) with your own API key.

A TypeScript MCP server that wraps DeepSeek's Search and Fetch APIs into two read-only tools, so any MCP-capable agent can use DeepSeek's own search engine regardless of which model it runs on.

| Tool | What it does | Upstream |
|---|---|---|
| `search` | Ranked web results (`title` / `url` / `snippet` / `summary` / `date` / `authority`); supports `site:`, `lang:`, `after:`, `before:`, quoted phrases | `POST /v1/tools/search` |
| `fetch` | One page as Markdown text, truncated to `MAX_CONTENT_CHARS` | `POST /v1/tools/fetch` |

Verified end to end: 6/6 checks in [`scripts/smoke.ts`](scripts/smoke.ts) (discovery, happy path, 429 retry, 401 surfacing, missing key, truncation), run against a mock API so no key or quota is needed.

## Install

```bash
# from npm
npx -y @evilwk/deepseek-search-mcp

# straight from GitHub, no registry involved
npx -y github:evilwk/deepseek-search-mcp

# from source
git clone https://github.com/evilwk/deepseek-search-mcp
cd deepseek-search-mcp && pnpm install && pnpm build
```

Get a key at <https://platform.deepseek.com/api_keys>. Search and Fetch are billed per successful request (¥5 / 1000), so the account needs a positive balance — an HTTP 402 from the server means it ran out.

## Run

```bash
export DEEPSEEK_API_KEY=sk-...
node dist/index.js          # stdio, for local agents
node dist/index.js --http   # Streamable HTTP, for sharing
```

A one-liner sanity check of the stdio handshake:

```bash
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"t","version":"1"}}}' \
  | node dist/index.js
```

## Client configuration

Each snippet below launches the server from a local build (`node /abs/path/.../dist/index.js`). If you installed from npm or GitHub instead, swap that for `npx`:

```jsonc
// from npm
{ "command": "npx", "args": ["-y", "@evilwk/deepseek-search-mcp"] }

// straight from GitHub
{ "command": "npx", "args": ["-y", "github:evilwk/deepseek-search-mcp"] }
```

**Claude Code**

```bash
claude mcp add deepseek-search -e DEEPSEEK_API_KEY=sk-... -- node /abs/path/deepseek-search-mcp/dist/index.js
```

**Generic `mcpServers` block** (Cursor, Claude Desktop, DSH, most hosts)

```json
{
  "mcpServers": {
    "deepseek-search": {
      "command": "node",
      "args": ["/abs/path/deepseek-search-mcp/dist/index.js"],
      "env": { "DEEPSEEK_API_KEY": "sk-..." }
    }
  }
}
```

**Codex CLI** (`~/.codex/config.toml`)

```toml
[mcp_servers.deepseek-search]
command = "node"
args = ["/abs/path/deepseek-search-mcp/dist/index.js"]
env = { DEEPSEEK_API_KEY = "sk-..." }
```

**Pi** (`~/.pi/agent/mcp.json`, or `.pi/mcp.json` in a trusted project)

```json
{
  "mcpServers": {
    "deepseek-search": {
      "command": "node",
      "args": ["/abs/path/deepseek-search-mcp/dist/index.js"],
      "env": { "DEEPSEEK_API_KEY": "${DEEPSEEK_API_KEY}" },
      "description": "DeepSeek 自研搜索引擎:网页搜索与单页 Markdown 抓取",
      "exposure": "direct",
      "timeout": 60
    }
  }
}
```

Pi's default `exposure` is `codemode`: the tools are registered but never declared to the model, so it would only reach them from codemode scripts or via `tool_search`. Two read-only tools belong at the top level — hence `"exposure": "direct"`. Validate with `pi mcp list`, inspect with `/mcp`, and run `/reload` after editing the file outside a session.

**OpenCode** (`~/.config/opencode/opencode.json`, or project `opencode.json`)

```jsonc
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "deepseek-search": {
      "type": "local",
      "command": ["node", "/abs/path/deepseek-search-mcp/dist/index.js"],
      "enabled": true,
      "environment": { "DEEPSEEK_API_KEY": "{env:DEEPSEEK_API_KEY}" }
    }
  }
}
```

Note the shape differences from the Claude-style blocks above: `command` is an **array**, the env map is called `environment`, and interpolation is `{env:NAME}` rather than `${NAME}`. For the HTTP mode below, use `"type": "remote"` with `url` + `headers` and set `"oauth": false`, otherwise OpenCode will try an OAuth flow the bearer token already replaces.

## HTTP mode (shared / remote)

```bash
MCP_PORT=3000 MCP_AUTH_TOKEN=change-me DEEPSEEK_API_KEY=sk-... node dist/index.js --http
# -> http://127.0.0.1:3000/mcp   (Streamable HTTP, session-based)
```

The DeepSeek key stays server-side; callers only need `MCP_AUTH_TOKEN` as a bearer token. There is no OAuth flow here — if you expose this beyond localhost, put it behind a real gateway. It bills your account.

## Environment variables

| Var | Default | Notes |
|---|---|---|
| `DEEPSEEK_API_KEY` | — | Required for tool calls |
| `DEEPSEEK_BASE_URL` | `https://api.deepseek.com` | For testing against a mock |
| `MAX_CONTENT_CHARS` | `40000` | `fetch` truncation limit |
| `MCP_PORT` / `MCP_HOST` | `3000` / `127.0.0.1` | HTTP mode only |
| `MCP_AUTH_TOKEN` | unset | HTTP mode; unset = no auth, localhost only |

## Design notes

- **stdout is forbidden territory in stdio mode.** All logging goes to `stderr`; a stray `console.log` corrupts the JSON-RPC stream and produces the classic "server disconnected" symptom.
- **Retries match the documented policy.** 408 / 429 / 5xx get two retries with 500 ms → 1 s backoff (enough for the account-wide 5 QPS cap). 402 is a hard stop — retrying only burns quota.
- **Errors are model-readable.** Failures come back as `isError: true` with an explicit "retry" / "do not retry" hint, so the agent does not hammer a permanent 4xx.
- **`need_content` defaults to false.** `summary` (600–800 chars) answers most questions; full page bodies would inflate the context window by orders of magnitude.
- **`readOnlyHint: true`** on both tools, so hosts can auto-approve them without a prompt.
- **The API client is transport-agnostic.** `src/deepseek.ts` imports nothing from the MCP SDK — it can back a CLI or an HTTP tool just as easily as this server.

## Layout

```
src/deepseek.ts   REST client: retry, timeouts, truncation, typed errors
src/server.ts     McpServer + tool registration (zod schemas, annotations)
src/index.ts      entry: stdio by default, --http for Streamable HTTP
src/http.ts       express 5 + session map + bearer auth
scripts/smoke.ts  mock-API end-to-end test, no real key needed
```

## Skill or MCP?

Both are thin shells over `src/deepseek.ts`. Use MCP when the host cannot execute shell commands or needs typed tool schemas; use a Skill when the host has `bash` and you want to avoid a resident process and per-tool schema overhead. This repo is the MCP half.

TDQS

A4.5/5.0

Scored across 2 tools

Disambiguation5/5

Search and fetch target clearly different operations: search returns ranked results, while fetch retrieves a single page's full content. The descriptions reinforce the intended use sequence (search then fetch), leaving no meaningful ambiguity.

Naming Consistency5/5

Both tool names are simple lowercase verbs (search, fetch), following a consistent single-verb convention. There is no mixing of styles or noun phrasing.

Tool Count4/5

Two tools is slightly below the typical 3–15 range, but each is well-scoped and earns its place for a focused web search and retrieval server. The surface is minimal yet sufficient for the core workflow.

Completeness5/5

The server is read-only, and its domain lifecycle is search then fetch; both operations are present with useful filtering parameters. No obvious gaps exist for basic web search and page retrieval.