内容
# ddg-mcp
一个遵循 [模型上下文协议(MCP)](https://modelcontextprotocol.io) 的服务器,通过 [DuckDuckGo](https://duckduckgo.com) 提供网页搜索和网页抓取功能。
暴露 `web_search` 和 `web_fetch` 工具,MCP兼容的客户端(Claude Desktop、Cursor、Windsurf、opencode等)可以使用这些工具来搜索网页和检索页面内容。
## 功能
- **网页搜索**,通过DuckDuckGo Lite端点(无需API密钥)
- 每个结果的标题、URL、摘要和域名信息
- 零点击/即时答案提取
- 通过偏移/vqd令牌进行分页
- 地区偏好(`kl`)、安全搜索(`kp`)和时间范围(`df`)过滤器
- 赞助结果检测
- 当HTML结构发生变化时,检测解析器故障并发出警告
- 在速率限制(HTTP 429)时自动重试,指数退避 + 抖动
- **网页抓取** - 从URL中检索和提取可读内容
- 去除脚本、样式、导航、头部、页脚元素
- 内容截断,配置最大长度
- SSRF保护(阻止重定向到私有/保留IP)
- 查询验证,长度限制
- 用于MCP兼容性的标准输入输出传输
- 通过GoReleaser实现跨平台二进制文件
## 安装
### 预编译二进制文件
从 [发布页面](https://github.com/bcambl/ddg-mcp/releases) 下载最新版本。
### Go安装
```bash
go install github.com/bcambl/ddg-mcp/cmd/ddg-mcp@latest
```
### 从源代码构建
```bash
git clone https://github.com/bcambl/ddg-mcp.git
cd ddg-mcp
make build
```
## 使用
### 运行服务器
```bash
ddg-mcp
```
服务器通过标准输入输出使用MCP协议进行通信。它设计为由MCP兼容的客户端启动。
### 环境变量
以下环境变量可用于自定义服务器行为,而无需修改二进制文件或客户端配置:
| 变量 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `DDG_SEARCH_URL` | 字符串 | `https://lite.duckduckgo.com/lite/` | DuckDuckGo搜索端点URL |
| `DDG_TIMEOUT` | 时长 | `15s` | 搜索请求的HTTP超时 |
| `DDG_FETCH_TIMEOUT` | 时长 | `15s` | 获取请求的HTTP超时 |
| `DDG_MAX_RETRIES` | 整数 | `2` | 搜索HTTP 429(速率限制)的最大重试次数 |
| `DDG_MAX_BODY_SIZE` | 整数(字节) | `2097152`(2 MiB) | 搜索和获取的最大响应体大小 |
| `DDG_USER_AGENT` | 字符串 | Chrome 120 UA | 搜索请求的User-Agent |
| `DDG_FETCH_USER_AGENT` | 字符串 | Chrome 120 UA | 获取请求的User-Agent |
| `DDG_DEFAULT_REGION` | 字符串 | *(无)* | 默认区域偏好(例如`us-en`、`uk-en`、`de-de`),在请求中未指定时 |
| `DDG_SSRF_PROTECTION` | 布尔值 | `true` | 为`web_fetch`启用SSRF保护(阻止重定向到私有IP) |
| `DDG_LOG_LEVEL` | 字符串 | `info` | 日志级别:`debug`、`info`、`warn`、`error` |
持续时间值接受Go样式字符串:`15s`、`1m`、`500ms`。
布尔值接受:`true`、`false`、`1`、`0`。
**示例,使用Go安装:**
```bash
DDG_DEFAULT_REGION=de-de DDG_TIMEOUT=30s ddg-mcp
```
### MCP客户端配置
将服务器添加到MCP客户端配置中。
#### Claude Desktop
编辑 `~/Library/Application Support/Claude/claude_desktop_config.json`(macOS)或 `%APPDATA%\Claude\claude_desktop_config.json`(Windows):
```json
{
"mcpServers": {
"ddg-search": {
"command": "ddg-mcp"
}
}
}
```
#### Cursor
添加到项目或全局配置中的 `.cursor/mcp.json`:
```json
{
"mcpServers": {
"ddg-search": {
"command": "ddg-mcp"
}
}
}
```
#### Windsurf
添加到Windsurf的MCP设置中:
```json
{
"mcpServers": {
"ddg-search": {
"command": "ddg-mcp"
}
}
}
```
#### opencode
添加到 `opencode.json`:
```json
{
"mcp": {
"ddg-search": {
"command": "ddg-mcp"
}
}
}
```
#### 使用Go安装路径
如果通过 `go install` 安装,请使用完整路径:
```json
{
"mcpServers": {
"ddg-search": {
"command": "go",
"args": ["run", "github.com/bcambl/ddg-mcp/cmd/ddg-mcp@latest"]
}
}
}
```
### 工具:`web_search`
使用DuckDuckGo搜索网页。返回包含标题、URL、摘要和域名信息的搜索结果。支持分页、地区偏好、安全搜索和时间范围过滤。包含零点击/即时答案结果。
**输入:**
| 参数 | 类型 | 必需 | 描述 |
| --- | --- | --- | --- |
| `query` | 字符串 | 是 | 搜索查询(最大500个字符) |
| `region` | 字符串 | 否 | 搜索偏好的地区代码(例如`us-en`、`uk-en`、`de-de`、`wt-wt`表示无地区) |
| `safe_search` | 整数 | 否 | 安全搜索级别:`1` = 严格、`-1` = 中等、`-2` = 关闭。默认值:`0`(DuckDuckGo默认值) |
| `time_range` | 字符串 | 否 | 时间范围过滤器:`d` = 天、`w` = 周、`m` = 月、`y` = 年。空 = 任意时间 |
| `offset` | 整数 | 否 | 分页偏移量(0、10、20等) |
| `vqd` | 字符串 | 否 | 上一次搜索响应中的分页令牌,`offset > 0`时必填 |
**输出:
```json
{
"query": "golang mcp",
"result_count": 3,
"results": [
{
"title": "示例结果",
"url": "https://example.com/page",
"snippet": "搜索结果的简要描述。",
"domain": "example.com",
"sponsored": false
}
],
"zero_click": {
"title": "即时答案标题",
"url": "https://example.com/answer",
"description": "DuckDuckGo即时答案的简要答案。"
},
"has_next_page": true,
"next_offset": 10,
"vqd": "3-123-456",
"parser_warning": ""
}
```
**分页:**
要获取下一页结果,请传递上一次响应中的`offset`和`vqd`:
```json
{
"query": "golang mcp",
"offset": 10,
"vqd": "3-123-456"
}
```
**错误情况:
- 空查询:`"查询不能为空"`
- 查询过长:`"查询超过最大长度500字符"`
- 无效地区:`"地区必须符合xx-xx格式(例如us-en、uk-en、de-de)"`
- 无效安全搜索:`"安全搜索必须为以下之一:0(默认)、1(严格)、-1(中等)、-2(关闭)"`
- 无效时间范围:`"时间范围必须为以下之一:d(天)、w(周)、m(月)、y(年)或空"`
- 分页请求无vqd:`"vqd令牌对于分页请求(偏移量> 0)是必需的"`
- 速率限制:`"被DuckDuckGo速率限制(HTTP 429),请稍后重试"`
- 服务器错误:`"DuckDuckGo返回HTTP 500:..."`
- 解析器故障:`"解析器可能已损坏:HTML包含结果标记但未提取结果"`
### 工具:`web_fetch`
从URL中获取并提取可读内容。返回页面标题和文本内容,去除脚本、样式、导航、头部和页脚元素。仅支持`text/html`和`text/plain`内容类型。
**输入:**
| 参数 | 类型 | 必需 | 描述 |
| --- | --- | --- | --- |
| `url` | 字符串 | 是 | 要获取的URL(必须是HTTP或HTTPS) |
| `max_length` | 整数 | 否 | 最大内容长度(字符)(默认10000,最大50000) |
**输出:
```json
{
"url": "https://example.com/page",
"title": "示例页面标题",
"content": "从页面中提取的可读文本内容...",
"content_length": 8234,
"content_type": "text/html; charset=utf-8",
"truncated": false
}
```
**错误情况:
- 空URL:`"URL不能为空"`
- 无效URL:`"URL必须是有效的HTTP或HTTPS URL"`
- 私有IP(启用SSRF保护):`"URL不能指向私有或保留IP地址"`
- 不支持的内容类型:`"不支持的内容类型:application/pdf(仅支持text/html和text/plain)"`
- 服务器错误:`"获取返回HTTP 404"`
## 开发
### 前提条件
- Go 1.26+
### 构建
```bash
make build
```
### 测试
```bash
make test
```
### 运行所有检查
```bash
make check
```
### 集成测试
集成测试会访问真实的DuckDuckGo端点,需要构建标签:
```bash
go test -tags=integration -v ./...
```
## 工作原理
服务器抓取DuckDuckGo的Lite搜索端点(`lite.duckduckgo.com/lite/`),而不是使用正式的API。这种方法:
- 无需API密钥
- 返回类似于浏览器搜索的结果
- 受DuckDuckGo的速率限制
### 搜索
DuckDuckGo用重定向链接(例如`//duckduckgo.com/l/?uddg=<encoded-url>`)包装结果URL。服务器从`uddg`查询参数中提取真实URL,并使用直接链接的原始`href`作为备用。
分页通过在`offset > 0`时将表单数据(包括`vqd`令牌和偏移量)发送回Lite端点来实现。`vqd`令牌标识搜索会话,并从结果页面的隐藏输入中提取。
解析器检测潜在的破坏 - 如果响应HTML包含结果标记(CSS类,重定向链接模式)但未提取结果,则在响应中包含`parser_warning`。
### 获取
获取工具向提供的URL发送GET请求,去除不需要的HTML元素(脚本、样式、导航、头部、页脚),并返回可读的文本内容。它强制执行可配置的最大内容长度,并在启用SSRF保护(默认)时阻止请求私有/保留IP。
## 限制
- 搜索结果限制在每个请求的第一页(~10个结果);分页需要显式的偏移/vqd令牌
- 受DuckDuckGo速率限制;避免快速连续的查询
- 如果DuckDuckGo更改页面结构,HTML抓取可能会中断(检测到时包含`parser_warning`)
- 不支持图片搜索、新闻搜索或其他DuckDuckGo功能
- 获取仅支持`text/html`和`text/plain`内容类型
## 许可证
[MIT](LICENSE)