内容
# mcp-searxng-relay
一款提供基于 SearXNG 的网络搜索和 URL 获取功能的 Model Context Protocol (MCP) 服务器,适用于需要搜索功能留在批准的基础设施上且每个查询都必须可审计的环境中。无需第三方搜索 API,也无需外部数据代理;查询永远不会离开您控制的基础设施。
**适用人群**:在企业或政府环境中运行 AI 代理的团队,这些环境中对外搜索受到限制、监控或两者兼有 —— 而且“我们使用托管的搜索 API”不是一个可接受的答案。该项目优先考虑可防御的安全态势和清晰的审计轨迹,而不是功能的广泛性。
该 MCP 服务器支持 **stdio** 传输(用于与 Claude Desktop 等类似客户端进行本地使用)和 **Streamable HTTP** 传输(用于网络化或容器化部署)。
---
## 目录
- [功能](#功能)
- [需求](#需求)
- [在 SearXNG 中启用 JSON 格式](#在-searxng-中启用-json-格式)
- [快速入门](#快速入门)
- [Docker(推荐)](#docker推荐)
- [Docker Compose](#docker-compose)
- [构建容器镜像](#构建容器镜像)
- [Kubernetes](#kubernetes)
- [配置](#配置)
- [令牌文件格式](#令牌文件格式)
- [会话模式](#会话模式)
- [调整会话清理器](#调整会话清理器)
- [MCP 工具](#mcp-工具)
- [`searxng_web_search`](#searxng_web_search)
- [`searxng_read_url`](#searxng_read_url)
- [`searxng_url_metadata`](#searxng_url_metadata)
- [与 Claude Desktop(stdio 模式)一起使用](#与-claude-desktopstdio-模式一起使用)
- [与 Claude Desktop(HTTP 模式)一起使用](#与-claude-desktophttp-模式一起使用)
- [安全说明](#安全说明)
- [速率限制](#速率限制)
- [会话限制](#会话限制)
- [操作](#操作)
- [健康检查端点](#健康检查端点)
- [`--healthcheck` CLI 标志](#--healthcheck-cli-标志)
- [优雅关闭](#优雅关闭)
- [HTTP 服务器超时](#http-服务器超时)
- [构建 Docker 镜像](#构建-docker-镜像)
- [日志记录](#日志记录)
- [指标](#指标)
---
## 功能
- **网络搜索**:通过 SearXNG 进行,完全控制语言、类别、时间范围、安全搜索级别和结果数量
- **URL 获取**:带有结构化 Markdown 输出 — 标题、列表、表格、代码块和内联强调全部保留
- **URL 元数据分类**:`searxng_url_metadata` 只返回标题、作者、发布日期、语言、站点名称、描述、图像、类别和标签作为 JSON,比获取完整内容所需的令牌成本低一个数量级。有用的 URL 选择标准。缓存与 `searxng_read_url` 共享,因此元数据获取后是内容获取(或反之亦然)只需一个上游 HTTP 请求,而不是两个。
- **PDF 文本提取**:从获取的 URL
- **办公文档提取**:DOCX、XLSX、PPTX 以及旧的 DOC、XLS、PPT。文档渲染为 Markdown 而不是纯文本,因此标题、表格和列表结构保留到模型的上下文中(特别是电子表格受益;Markdown 表格比 CSV 扁平单元格更有用)
- **长文档分页**:响应窗口化为 100k 字符,截断的响应以通知结束,通知中包含总大小和下一个调用的确切 `start_index`。提取的完整文本(最多 `MAX_EXTRACTED_CHARS`)被缓存,因此浏览大 PDF 的成本是一个上游获取,而不是每个页面一个。
- **图像响应**:JPEG、PNG、GIF 和 WebP URL 作为 MCP `ImageContent` 块返回,供视觉模型使用(SDK 在线路上对原始字节进行 base64 编码)。SVG 被故意排除 — 作为文本比作为二进制 blob 对模型更有用。原始大小由 `MAX_IMAGE_BYTES` 限制,与 `MAX_BODY_BYTES` 分开,因此图像和文本限制可以独立调整。
- **自动字符集检测**:非 UTF-8 页面(Shift-JIS、windows-1252、ISO-8859-1 等)被正确解码后解析
- **可读性样式内容提取**:导航栏、页脚、侧边栏和 cookie 横幅自动剥离
- **搜索引擎归属**:每个结果都包含返回它的 SearXNG 后端引擎列表。一个 URL 由三个引擎显示的信号与一个引擎显示的信号不同,代理可以对其进行权衡,而无需服务器在上面施加排名。
- **每域获取指标**:`/metrics` 公开 `mcp_fetches_by_domain_total{domain="…",outcome="success|error"}`,这样操作员可以看到哪些目标主机是健康的,哪些不是。基数有界:最多跟踪 512 个不同域,其余的汇总在 `domain="__overflow__"` 下。
- **响应缓存**:具有可配置的 TTL 和每个请求的缓存绕过
- **SSRF 保护**:在 TCP 拨号时阻止非全局可路由地址(环路、链路本地、私有、多播、广播、未指定,加上涵盖 CGNAT、TEST-NET-{1,2,3}、基准、IETF 协议分配、NAT64、Teredo、6to4、IPv6 文档、ORCHID、丢弃前缀、未来保留的 240/4 和其他保留范围的硬编码阻止列表)。重定向链在每个跳跃处重新验证,以关闭 DNS 重新绑定窗口。操作员可以选择通过 `FETCH_ALLOWED_HOSTS` / `FETCH_ALLOWED_CIDRS` 访问内部资源(Confluence、Jira、wiki)。
- **Bearer 令牌身份验证**:具有多令牌表(`MCP_AUTH_TOKEN`、`MCP_AUTH_TOKENS` 或 `MCP_AUTH_TOKEN_FILE`)和每个身份的审计日志记录
- **每个调用者的速率限制**:通过身份验证时按身份限制令牌桶,源 IP 否则。配置 RPS 和突发,默认为 5 rps / 突发 10。在 `mcp_rate_limit_rejections_total` 处公开。
- **提示围栏**:每个工具响应都包装在带有每个响应随机_nonce的签名 `<sec:fence>` 元素中,实现 arXiv:2511.19727。公共密钥在 `/fence/public-key` 处公开,以与验证客户端向前兼容。
- **可复制的容器构建**:比特对比特。给定相同的源代码提交和 `SOURCE_DATE_EPOCH`,构建产生一个字节相同的镜像,可通过 `docker save <image> | sha256sum` 验证。工具链按摘要固定,`go.sum` 冻结,无嵌入路径,VCS 状态或构建 ID。详细信息在 [`docs/supply-chain.md`](docs/supply-chain.md) 中。
- **结构化启动横幅**:所有配置值在启动时打印到 stderr(秘密被遮蔽)
---
## 需求
- 运行中的 SearXNG 实例,并启用 JSON 输出格式
- Go 1.26+(用于从源代码构建)或 Docker
### 在 SearXNG 中启用 JSON 格式
将以下内容添加到您的 SearXNG `settings.yml` 中:
```yaml
search:
formats:
- html
- json
```
---
## 快速入门
### Docker(推荐)
```bash
docker run -d \
-e SEARXNG_URL=https://your-searxng-instance.example.com \
-e MCP_PORT=8080 \
-e MCP_AUTH_TOKEN=$(openssl rand -hex 32) \
-p 8080:8080 \
ghcr.io/your-org/mcp-searxng-relay:latest
```
### Docker Compose
```yaml
services:
mcp-searxng:
image: ghcr.io/your-org/mcp-searxng-relay:latest
restart: unless-stopped
environment:
SEARXNG_URL: https://your-searxng-instance.example.com
MCP_PORT: "8080"
MCP_AUTH_TOKEN: your-strong-random-token
ports:
- "8080:8080"
```
## 构建容器镜像
计算两个可复制性输入一次,然后选择您的构建工具:
```bash
SOURCE_DATE_EPOCH="$(git log -1 --pretty=%ct HEAD)"
SERVER_VERSION="$(git describe --tags --always)"
```
**Docker(使用 BuildKit / buildx)**:
```bash
docker buildx build \
--build-arg SERVER_VERSION="${SERVER_VERSION}" \
--build-arg SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH}" \
--output type=docker,rewrite-timestamp=true \
-t mcp-searxng-relay:"${SERVER_VERSION}" .
```
**Podman**:
```bash
podman build \
--build-arg SERVER_VERSION="${SERVER_VERSION}" \
--build-arg SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH}" \
--timestamp "${SOURCE_DATE_EPOCH}" \
-t mcp-searxng-relay:"${SERVER_VERSION}" .
```
多阶段构建在 `golang:1.26.3-trixie` 构建器上编译二进制文件,并仅将静态二进制文件和 CA 证书复制到 `scratch` 运行时镜像中。
**可复制性**:给定相同的源代码提交和 `SOURCE_DATE_EPOCH`(规范为提交的自身时间戳),任一调用都生成一个字节相同的镜像 — 可通过 `docker save <image> | sha256sum` 或 `podman save <image> | sha256sum` 验证。工具链按内容摘要固定,模块图通过 `go.sum` 冻结,构建设置 `-trimpath`、`-buildvcs=false`、`-buildid=` 和 `-Wl,--build-id=none`,以便路径、VCS 状态或链接时间构建 ID 不会泄露到二进制文件中。BuildKit 的 `rewrite-timestamp` 和 Podman 的 `--timestamp` 都将所有层文件时间戳固定为相同的值,因此镜像信封是可复制的,而不仅仅是内部的二进制文件。参见 [`docs/supply-chain.md`](docs/supply-chain.md) 以获取完整的供应链声明和验证步骤。
注意,Docker 和 Podman 使用略微不同的磁盘清单编码,因此使用其中一个构建并通过另一个保存的镜像不会具有匹配的 SHA-256,即使功能相同。请选择一个构建工具,并为跨机器的可复制性检查坚持使用它。
### Kubernetes
可应用的清单位于存储库根目录:[`deployment.yaml`](deployment.yaml) 具有锁定的 `securityContext`,[`service.yaml`](service.yaml),[`kustomization.yaml`](kustomization.yaml) 和 [`secret_example.yaml`](secret_example.yaml) 作为 `MCP_AUTH_TOKEN_FILE` 的模板。应用 `kubectl apply -k .` 之前,请使用真实的 Secret 创建一个 Secret — 从 `secret_example.yaml` 复制它,填写使用 `openssl rand -hex 32` 生成的令牌,并一次性应用该文件(它故意不列在 `kustomization.yaml` 中,以免重新应用无法将真实 Secret 恢复到占位符值)。`Deployment` 默认情况下为审计友好的行为设置为单个副本的 stateful 模式;通过设置 `MCP_STATELESS=true` 并扩展 `replicas` 切换到无状态的多副本模式。对于与外部秘密存储(External Secrets Operator、Sealed Secrets、CSI Secrets Store)的集成,请用适当的资源替换 Secret,并在您自己的覆盖层中。
---
## 配置
所有配置都通过环境变量进行。服务器如果未设置 `SEARXNG_URL`,则拒绝启动。如果设置了 `MCP_PORT`,则需要至少一个 `MCP_AUTH_TOKEN` / `MCP_AUTH_TOKENS` / `MCP_AUTH_TOKEN_FILE`。
| 变量 | 必需 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `SEARXNG_URL` | **是** | — | 您的 SearXNG 实例的基本 URL(自动剥离尾部斜杠) |
| `MCP_PORT` | 否 | — | HTTP 模式下监听的端口。如果未设置,服务器使用 stdio |
| `MCP_AUTH_TOKEN` | HTTP 模式¹ | — | 单个 bearer 令牌;身份记录为 `"default"`。与单租户部署向后兼容 |
| `MCP_AUTH_TOKENS` | HTTP 模式¹ | — | 用逗号分隔的 `identity:token` 对,用于小型静态舰队,例如 `alice:abc...,bob:def...` |
| `MCP_AUTH_TOKEN_FILE` | HTTP 模式¹ | — | 具有每行一个 `identity:token` 的文件;`#` 注释和空行被忽略 |
| `MCP_STATELESS` | 否 | `false` | 如果 `true`,SDK 跳过 session-ID 验证,并将每个请求视为一个全新的临时会话。参见下面的“会话模式” |
| `MCP_SESSION_MAX_AGE` | 否 | `168h` | 有状态模式。会话在被清理器关闭之前可能存在的最大时长。Go 时长语法(`30m`、`12h`、`168h` — 无 `d` 或 `w`) |
| `MCP_SESSION_JANITOR_INTERVAL` | 否 | `15m` | 有状态模式。清理器多久扫描一次过期的会话。相同的时长语法 |
| `MCP_RATE_LIMIT_RPS` | 否 | `5` | 每个调用者的持续请求率(请求/秒)。设置为 `0` 以禁用。支持分数值(例如 `0.5` = 每 2 秒一个请求) |
| `MCP_RATE_LIMIT_BURST` | 否 | `2 × RPS, min 1` | 令牌桶突发容量 — 调用者可以在持续速率启动前连续发送的请求数量 |
| `MCP_RATE_LIMIT_EXEMPT` | 否 | — | 以逗号分隔的身份名称列表,完全绕过速率限制器(例如 `ci,uptime-monitor`)。适用于受信任的内部调用者和监控身份 |
| `AUTH_USERNAME` | 否 | — | SearXNG 的 HTTP 基本身份验证用户名(如果您的实例需要它) |
| `AUTH_PASSWORD` | 否 | — | SearXNG 的 HTTP 基本身份验证密码 |
| `USER_AGENT` | 否 | `mcp-searxng-relay/<version>` | 对所有出站请求发送的 User-Agent 头 |
| `CACHE_TTL_SECONDS` | 否 | `300` | 获取的 URL 内容的缓存时长(秒) |
| `CACHE_MAX_ENTRIES` | 否 | `1000` | 内存缓存中保存的最大 URL 数量。达到上限时自动驱逐最旧的条目 |
| `MAX_BODY_BYTES` | 否 | `500000` | 从获取的 URL 读取的最大响应体大小(字节) |
| `MAX_PDF_BYTES` | 否 | `50000000` | PDF URL 的最大响应体大小(字节)。PDF 有单独的较大限制,因为多页文档可以轻松达到 50 MB |
| `MAX_OFFICE_BYTES` | 否 | `50000000` | Office 文档 URL 的最大响应体大小(DOCX、XLSX、PPTX + 旧 DOC、XLS、PPT)(字节)。现代 OOXML 文件是 ZIP 存档,常规嵌入图像、字体和图表数据,因此它们有自己的上限,与 `MAX_BODY_BYTES` 分开 |
| `MAX_IMAGE_BYTES` | 否 | `7500000` | 图像响应的最大原始大小(字节)。线路上的 wire 格式在 base64 编码后约大 33% |
| `MAX_EXTRACTED_CHARS` | 否 | `1000000` | 提取的文本上限(与原始响应体大小不同)每个 URL 保存(和缓存)。这是 `searxng_read_url` 分页浏览的部分;每个响应最多返回 100k 字符。内存注意:最坏情况缓存持有 `CACHE_MAX_ENTRIES × MAX_EXTRACTED_CHARS` 字节的内容(默认为 1 GB,尽管实际页面很少接近上限)— 降低任意值以适应内存预算,提高此值以深入到非常大的文档中 |
| `FETCH_ALLOWED_HOSTS` | 否 | — | 以逗号分隔的主机名列表,其获取绕过公共 IP SSRF 检查,以便获取工具可以访问命名内部资源(例如 `confluence.corp,wiki.internal`)。与请求主机名(不区分大小写和结尾点)精确匹配,并在每个重定向跳跃处重新检查。参见 [SSRF 保护](#security-notes) |
| `FETCH_ALLOWED_CIDRS` | 否 | — | 逗号分隔的 IP 范围列表,即使默认策略会阻止它们,也视为可达(例如 `10.0.0.0/8,192.168.0.0/16`)。在拨号时检查解析的 IP,并在每个重定向跳转处检查以保持对 DNS 重新绑定的鲁棒性。错误的条目导致启动失败。参见 [SSRF 保护](#security-notes) |
| `LOG_LEVEL` | 否 | `info` | 日志详细程度:`debug`、`info`、`warn`、`error`、`off` |
| `LOG_FORMAT` | 否 | `text` | 日志格式:`text` 或 `json` |
¹ HTTP 模式需要至少一个三个身份验证变量。它们也可以组合:后续来源如果出现相同的摘要,则覆盖前一个来源。如果多个摘要出现在多个位置,则所有令牌都独立验证以达到 32 个字符的最小长度。
生成强令牌:```bash
openssl rand -hex 32
### 配置文件格式
使用 `MCP_AUTH_TOKEN_FILE` 时,每个非注释文件中的示例:
```
# 这是一条注释。
alice:7f3a8c2e9b1d4f6a0c8e2b9d4f6a0c8e2b9d4f6a0c8e2b9d4f6a
bob:0e1d2c3b4a596877665544332211ffeedccbbaa998877665544332211ffeedc
service-ci:9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba
# 身份轮换:以下两行 alice 都被接受,直到删除旧令牌。
# 有助于零停机时间令牌轮换。
alice:newtokenvaluefor32charsminimum0123456789abcdef0123456789abcdef
```
将文件模式设置为 `0600` 并将其放在 `tmpfs` 上(或 Docker 秘密 / Kubernetes 投影卷),如果您的威胁模型包括主机上的其他用户。
---
## 快速入门
### Docker(推荐)
```bash
docker run -d \
-e SEARXNG_URL=https://your-searxng-instance.example.com \
-e MCP_PORT=8080 \
-e MCP_AUTH_TOKEN=$(openssl rand -hex 32) \
-p 8080:8080 \
ghcr.io/your-org/mcp-searxng-relay:latest
```
### Docker Compose
```yaml
services:
mcp-searxng:
image: ghcr.io/your-org/mcp-searxng-relay:latest
restart: unless-stopped
environment:
SEARXNG_URL: https://your-searxng-instance.example.com
MCP_PORT: "8080"
MCP_AUTH_TOKEN: your-strong-random-token
ports:
- "8080: 8080"
```
## 构建容器镜像
计算两个可复制性输入一次,然后选择您的构建工具:
```bash
SOURCE_DATE_EPOCH="$(git log -1 --pretty=%ct HEAD)"
SERVER_VERSION="$(git describe --tags --always)"
```
**Docker(使用 BuildKit / buildx)**:
```bash
docker buildx build \
--build-arg SERVER_VERSION="${SERVER_VERSION}" \
--build-arg SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH}" \
--output type=docker,rewrite-timestamp=true \
-t mcp-searxng-relay:"${SERVER_VERSION}" .
```
**Podman**:
```bash
podman build \
--build-arg SERVER_VERSION="${SERVER_VERSION}" \
--build-arg SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH}" \
--timestamp "${SOURCE_DATE_EPOCH}" \
-t mcp-searxng-relay:"${SERVER_VERSION}" .
```
多阶段构建在 `golang:1.26.3-trixie` 构建器上编译二进制文件,并仅将静态二进制文件和 CA 证书复制到 `scratch` 运行时镜像中。
**可复制性**:给定相同的源代码提交和 `SOURCE_DATE_EPOCH`(规范为提交的自身时间戳),任一调用都生成一个字节相同的镜像 — 可通过 `docker save <image> | sha256sum` 或 `podman save <image> | sha256sum` 验证。工具链按摘要固定,模块图通过 `go.sum` 冻结,构建设置 `-trimpath`、`-buildvcs=false`、`-buildid=` 和 `-Wl,--build-id=none`,以便路径、VCS 状态或链接时间构建 ID 不会泄露到二进制文件中。BuildKit 的 `rewrite-timestamp` 和 Podman 的 `--timestamp` 都将所有层文件时间戳固定为相同的值,因此镜像信封是可复制的,而不仅仅是内部的二进制文件。参见 [`docs/supply-chain.md`](docs/supply-chain.md) 以获取完整的供应链声明和验证步骤。
注意,Docker 和 Podman 使用略微不同的磁盘清单编码,因此使用一个构建并通过另一个保存的镜像不会具有匹配的 SHA-256,即使功能相同。请选择一个构建工具,并为跨机器的可复制性检查坚持使用它。
### Kubernetes
可应用的清单位于存储库根目录:[`deployment.yaml`](deployment.yaml) 具有锁定的 `securityContext`,[`service.yaml`](service.yaml),[`kustomization.yaml`](kustomization.yaml) 和 [`secret_example.yaml`](secret_example.yaml) 作为 `MCP_AUTH_TOKEN_FILE` 的模板。应用 `kubectl apply -k .` 之前,请使用真实的 Secret 创建一个 Secret — 从 `secret_example.yaml` 复制它,填写使用 `openssl rand -hex 32` 生成的令牌,并一次性应用该文件(它故意不列在 `kustomization.yaml` 中,以免重新应用无法将真实 Secret 恢复到占位符值)。`Deployment` 默认情况下为审计友好的行为设置为单个副本的 stateful 模式;通过设置 `MCP_STATELESS=true` 并扩展 `replicas` 切换到无状态的多副本模式。对于与外部秘密存储(External Secrets Operator、Sealed Secrets、CSI Secrets Store)的集成,请用适当的资源替换 Secret,并在您自己的覆盖层中。
---
## 配置
所有配置都通过环境变量进行。服务器如果未设置 `SEARXNG_URL`,则拒绝启动。如果设置了 `MCP_PORT`,则需要至少一个 `MCP_AUTH_TOKEN` / `MCP_AUTH_TOKENS` / `MCP_AUTH_TOKEN_FILE`。
| 变量 | 必需 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `SEARXNG_URL` | **是** | — | 您的 SearXNG 实例的基本 URL(自动剥离尾部斜杠) |
| `MCP_PORT` | 否 | — | HTTP 模式下监听的端口。如果未设置,服务器使用 stdio |
| `MCP_AUTH_TOKEN` | HTTP 模式¹ | — | 单个 bearer 令牌;身份记录为 `"default"`。与单租户部署向后兼容 |
| `MCP_AUTH_TOKENS` | HTTP 模式¹ | — | 用逗号分隔的 `identity:token` 对,用于小型静态舰队,例如 `alice:abc...,bob:def...` |
| `MCP_AUTH_TOKEN_FILE` | HTTP 模式¹ | — | 具有每行一个 `identity:token` 的文件;`#` 注释和空行被忽略 |
| `MCP_STATELESS` | 否 | `false` | 如果 `true`,SDK 跳过 session-ID 验证,并将每个请求视为一个全新的临时会话。参见下面的“会话模式” |
| `MCP_SESSION_MAX_AGE` | 否 | `168h` | 有状态模式。会话在被清理器关闭之前可能存在的最大时长。Go 时长语法(`30m`、`12h`、`168h` — 无 `d` 或 `w`) |
| `MCP_SESSION_JANITOR_INTERVAL` | 否 | `15m` | 有状态模式。清理器多久扫描一次过期的会话。相同的时长语法 |
| `MCP_RATE_LIMIT_RPS` | 否 | `5` | 每个调用者的持续请求率(请求/秒)。设置为 `0` 以禁用。支持分数值(例如 `0.5` = 每 2 秒 1 个请求) |
| `MCP_RATE_LIMIT_BURST` | 否 | `2 × RPS, min 1` | 令牌桶突发容量 — 调用者可以在持续速率启动前连续发送的请求数量 |
| `MCP_RATE_LIMIT_EXEMPT` | 否 | — | 以逗号分隔的身份名称列表,完全绕过速率限制器(例如 `ci,uptime-monitor`)。适用于受信任的内部调用者和监控身份 |
| `AUTH_USERNAME` | 否 | — | SearXNG 的 HTTP 基本身份验证用户名(如果您的实例需要它) |
| `AUTH_PASSWORD` | 否 | — | SearXNG 的 HTTP 基本身份验证密码 |
| `USER_AGENT` | 否 | `mcp-searxng-relay/<version>` | 发送所有出站请求的 User-Agent 头 |
| `CACHE_TTL_SECONDS` | 否 | `300` | 获取的 URL 内容的缓存时长(秒) |
| `CACHE_MAX_ENTRIES` | 否 | `1000` | 内存缓存中保存的最大 URL 数量。达到上限时自动驱逐最旧的条目 |
| `MAX_BODY_BYTES` | 否 | `500000` | 从获取的 URL 读取的最大响应体大小(字节) |
| `MAX_PDF_BYTES` | 否 | `50000000` | PDF URL 的最大响应体大小(字节)。PDF 有单独的较大限制,因为多页文档可以轻松达到 50 MB |
| `MAX_OFFICE_BYTES` | 否 | `50000000` | Office 文档 URL 的最大响应体大小(DOCX、XLSX、PPTX + 旧 DOC、XLS、PPT)(字节)。现代 OOXML 文件是 ZIP 存档,可以常规嵌入图像、字体和图表数据,因此它们有自己的上限,与 `MAX_BODY_BYTES` 分开 |
| `MAX_IMAGE_BYTES` | 否 | `7500000` | 图像响应的最大原始大小(字节)。线路上的 wire 格式在 base64 编码后约大 33% |
| `MAX_EXTRACTED_CHARS` | 否 | `1000000` | 提取的文本上限(与原始响应体大小不同)每个 URL 保存(和缓存)。这是 `searxng_read_url` 分页浏览的部分;每个响应最多返回 100k 字符。内存注意:最坏情况缓存持有 `CACHE_MAX_ENTRIES × MAX_EXTRACTED_CHARS` 字节的内容(默认为 1 GB,尽管实际页面很少接近上限)— 降低任意值以适应内存预算,提高此值以深入到非常大的文档中 |
| `FETCH_ALLOWED_HOSTS` | 否 | — | 以逗号分隔的主机名列表,其获取绕过公共 IP SSRF 检查,以便获取工具可以访问命名内部资源(例如 `confluence.corp,wiki.internal`)。与请求主机名(不区分大小写和结尾点)精确匹配,并在每个重定向跳转处重新检查。参见 [SSRF 保护](#security-notes) |
| `FETCH_ALLOWED_CIDRS` | 否 | — | 以逗号分隔的 IP 范围列表,即使默认策略会阻止它们,也视为可达(例如 `10.0.0.0/8,192.168.0.0/16`)。在拨号时检查解析的 IP,并在每个重定向处检查以保持对 DNS 重绑定的鲁棒性。错误的条目导致启动失败。参见 [SSRF 保护](#security-notes) |
| `LOG_LEVEL` | 否 | `info` | 日志详细程度:`debug`、`info`、`warn`、`error`、`off` |
| `LOG_FORMAT` | 否 | `text` | 日志格式:`text` 或 `json` |
¹ HTTP 模式需要 *至少一个* 三个身份验证令牌变量中的一个。它们也可以组合:后续来源如果出现相同的摘要,则覆盖前一个来源。如果多个摘要出现在多个位置,则所有令牌都独立验证以达到 32 个字符的最小长度。
生成强令牌:
```bash
openssl rand -hex 32
```
### 令牌文件格式
使用 `MCP_AUTH_TOKEN_FILE` 时,每行非注释内容均为 `identity:token`。分割发生在第一个 `:` 处,因此令牌中可以包含冒号;身份中不可以。身份是任意字符串,仅用于日志关联 — 通常是用户名、代理名称或服务帐户标签。
```
# 这是一条注释。
alice:7f3a8c2e9b1d4f6a0c8e2b9d4f6a0c8e2b9d4f6a0c8e2b9d4f6a0c8e2b9d4f6a
bob:0e1d2c3b4a596877665544332211ffeedccbbaa998877665544332211ffeedc
service-ci:9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba
# 身份轮换:以下两行 alice 都被接受,直到删除旧令牌。
# 有助于零停机时间令牌轮换。
alice:newtokenvaluefor32charsminimum0123456789abcdef0123456789abcdef
```
将文件模式设置为 `0600` 并将其放在 `tmpfs` 上(或 Docker 秘密 / Kubernetes 投影卷),如果您的威胁模型包括主机上的其他用户。
---
## 快速入门
### Docker(推荐)
```bash
docker run -d \
-e SEARXNG_URL=https://your-searxng-instance.example.com \
-e MCP_PORT=totime \
-e MCP_AUTH_TOKEN=$(openssl rand -hex 32) \
-p 8080:8080 \
ghcr.io/your-org/mcp-searxng-relay:latest
```
### Docker Compose
```yaml
services:
mcp-searxng:
image: ghcr.io/your-org/mcp-searxng-relay:latest
restart: unless-stopped
environment:
SEARXNG_URL: https://your-searxng-instance.example.com
MCP_PORT: "8080"
MCP_AUTH_TOKEN: your-strong-random-token
ports:
- "8080:8080"
```
## 构建容器镜像
计算两个可复制性输入一次,然后选择您的构建工具:
```bash
SOURCE_DATE_EPOCH="$(git log -1 --pretty=%ct HEAD)"
SERVER_VERSION="$(git describe --tags --always)"
```
**Docker(使用 BuildKit / buildx)**:
```bash
docker buildx build \
--build-arg SERVER_VERSION="${SERVER_VERSION}" \
--build-arg SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH}" \
--output type=docker,rewrite-timestamp=true \
-t mcp-searxng-relay:"${SERVER_VERSION}" .
```
**Podman**:
```bash
podman build \
--build-arg SERVER_VERSION="${SERVER_VERSION}" \
--build-arg SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH}" \
--timestamp "${SOURCE_DATE_EPOCH}" \
-t mcp-searxng-relay:"${SERVER_VERSION}" .
```
多阶段构建在 `golang:1.26.3-trixie` 构建器上编译二进制文件,并仅将静态二进制文件和 CA 证书复制到 `scratch` 运行时镜像中。
**可复制性**:给定相同的源代码提交和 `SOURCE_DATE_EPOCH`(规范为提交的自身时间戳),任一调用都生成一个字节相同的镜像 — 可通过 `docker save <image> | sha256sum` 或 `podman save <image> | sha256sum` 验证。工具链按摘要固定,模块图通过 `go.sum` 冻结,构建设置 `-trimpath`、`-buildvcs=false`、`-buildid=` 和 `-Wl,--build-id=none`,以便路径、VCS 状态或链接时间构建 ID 不会泄露到二进制文件中。BuildKit 的 `rewrite-timestamp` 和 Podman 的 `--timestamp` 都将所有层文件时间戳固定为相同的值,因此镜像信封是可复制的,而不仅仅是内部的二进制文件。参见 [`docs/supply-chain.md`](docs/supply-chain.md) 以获取完整的供应链声明和验证步骤。
注意,Docker 和 Podman 使用略微不同的磁盘清单编码,因此使用一个构建并通过另一个保存的镜像不会具有匹配的 SHA-256,即使功能相同。请选择一个构建工具,并为跨机器的可复制性检查坚持使用它。
### Kubernetes
可应用的清单位于存储库根目录:[`deployment.yaml`](deployment.yaml) 具有锁定的 `securityContext`,[`service.yaml`](service.yaml),[`kustomization.yaml`](kustomization.yaml) 和 [`secret_example.yaml`](secret_example.yaml) 作为 `MCP_AUTH_TOKEN_FILE` 的模板。应用 `kubectl apply -k .` 之前,请使用真实的 Secret 创建一个 Secret — 从 `secret_example.yaml` 复制它,填写使用 `openssl rand -hex 32` 生成的令牌,并一次性应用该文件(它故意不列在 `kustomization.yaml` 中,以免重新应用无法将真实 Secret 恢复到占位符值)。`Deployment` 默认情况下为审计友好的行为设置为单个副本的 stateful 模式;通过设置 `MCP_STATELESS=true` 并扩展 `replicas` 切换到无状态的多副本模式。对于与外部秘密存储(External Secrets Operator、Sealed Secrets、CSI Secrets Store)的集成,请用适当的资源替换 Secret,并在您自己的覆盖层中。
---
## 配置
所有配置都通过环境变量进行。服务器如果未设置 `SEARXNG_URL`,则拒绝启动。如果设置了 `MCP_PORT`,则需要至少一个 `MCP_AUTH_TOKEN` / `MCP_AUTH_TOKENS` / `MCP_AUTH_TOKEN_FILE`。
| 变量 | 必需 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `SEARXNG_URL` | **是** | — | 您的 SearXNG 实例的基本 URL(自动剥离尾部斜杠) |
| `MCP_PORT` | 否 | — | HTTP 模式下监听的端口。如果未设置,服务器使用 stdio |
| `MCP_AUTH_TOKEN` | HTTP 模式¹ | — | 单个 bearer 令牌;身份记录为 `"default"`。与单租户部署向后兼容 |
| `MCP_AUTH_TOKENS` | HTTP 模式¹ | — | 用逗号分隔的 `identity:token` 对,用于小型静态舰队,例如 `alice:abc...,bob:def...` |
| `MCP_AUTH_TOKEN_FILE` | HTTP 模式¹ | — | 具有每行一个 `identity:token` 的文件;`#` 注释和空行被忽略 |
| `MCP_STATELESS` | 否 | `false` | 如果 `true`,SDK 跳过 session-ID 验证,并将每个请求视为一个全新的临时会话。参见下面的“会话模式” |
| `MCP_SESSION_MAX_AGE` | 否 | `168h` | 有状态模式。会话在被清理器关闭之前可能存在的最大时长。Go 时长语法(`30m`、`12h`、`168h` — 无 `d` 或 `w`) |
| `MCP_SESSION_JANITOR_INTERVAL` | 否 | `15m` | 有状态模式。清理器多久扫描一次过期的会话。相同的时长语法 |
| `MCP_RATE_LIMIT_RPS` | 否 | `5` | 每个调用者的持续请求率(请求/秒)。设置为 `0` 以禁用。支持分数值(例如 `0.5` = 每 2 秒 1 个请求) |
| `MCP_RATE_LIMIT_BURST` | 否 | `2 × RPS, min 1` | 令牌桶突发容量 — 调用者可以在持续速率启动前连续发送的请求数量 |
| `MCP_RATE_LIMIT_EXEMPT` | 否 | — | 以逗号分隔的身份名称列表,完全绕过速率限制器
### 会话模式
MCP Streamable HTTP 传输默认是有状态的:SDK 在 `initialize` 时分配一个会话 ID,客户端在后续每个请求中都会回显它,SDK 会在内存映射中查找它。当服务器重启时,该映射会被重建为空——客户端的旧会话 ID 返回 404,许多 MCP 客户端尽管规范要求它们自动重新初始化,但仍然无法重新初始化。结果就是“我重新部署了,但我的代理卡住了,直到我重启它。”
| 模式 | `MCP_STATELESS` | 何时使用 | 权衡 |
|---|---|---|---|
| **有状态** *(默认)* | `false` | 多租户部署,其中 `session_id` 必须是服务器颁发且防伪的 | 代理必须在每次服务器重启后重新握手 |
| **无状态** | `true` | 需要在服务器重启时无需客户端重新连接的部署 | `session_id` 变成客户端断言的(而非服务器验证的);服务器发起的通知无法到达客户端 |
在有状态模式下进行审计关联时,每行工具调用日志都带有 `identity` (用于身份验证请求的令牌)和 `session_id` (用于初始化握手的请求所属的会话 ID)。`session_id` 将工具调用链接回同一会话的 `"会话已初始化"` 日志行——在握手时记录了客户端的身份。空闲会话在 `MCP_SESSION_MAX_AGE` 后被后台清理程序回收(默认为 7 天);客户端干净地关闭的会话(DELETE)也被跟踪并立即释放。
在无状态模式下,`session_id` 字段仍然存在,并且在来自同一客户端的请求中保持稳定,但绑定较弱:SDK 使用客户端在 `Mcp-Session-Id` 中发送的值,而不验证它(仅在客户端未提供任何值时生成一个新的值)。这意味着一个经过身份验证的客户端原则上可以伪造另一个 `session_id`,这对诚实的客户端是无害的,但意味着您不能将 `session_id` 视为服务器验证的属性。`identity` 在两种模式下都保持服务器验证,并且是防伪的关键。
如果您不希望在日志中出现客户端断言的 `session_id`,请在 `server.go:buildMCPServer` 中将 `mcp.ServerOptions.GetSessionID` 设置为 `func() string { return "" }`。然后,SDK 将省略 `Mcp-Session-Id` 响应头,并且 `req.Session.ID()` 对于每个请求都返回空字符串——真正的“无会话”模式。由于用例有限,因此未作为环境变量公开。
#### 调整会话清理程序
两个清理程序旋钮服务于不同的目的,在更改默认值之前了解它们很有用:
- **`MCP_SESSION_MAX_AGE`** 是一个*策略*设置。它限制了任何一个会话允许存在的最大时长。如果您的环境每天轮换身份验证令牌,则应降低它(例如 `24h`)——旧于轮换周期的会话正在使用一个不再存在于表中的令牌,因此回收它们强制进行干净的重新握手与当前的令牌。对于需要定期重新身份验证的合规框架,可以进一步降低它。如果您的代理合法地长时间空闲,则可以提高它(例如 `720h` / 30 天)。
- **`MCP_SESSION_JANITOR_INTERVAL`** 是一个*机制*设置。它控制清理传递的运行频率。较短的间隔可以更早地捕获过期的会话,但代价是会发生少量的互斥锁争用;较长的间隔则更便宜,但允许超过 `MCP_SESSION_MAX_AGE` 的时间更长。默认的 `15m` 意味着会话可能在其最大年龄后最多 15 分钟才被关闭——对于“大约一周”的策略来说已经足够,但如果您的最大年龄本身很短,则值得降低。
如果您在负载下看不到会话上限(`mcp_active_sessions` 在 `/metrics` 中)增加,则默认值正在发挥作用,无需调整。
---
## MCP 工具
### `searxng_web_search`
执行网页搜索并返回标题、网址和摘要。
| 参数 | 类型 | 必需 | 默认值 | 描述 |
|---|---|---|---|---|
| `query` | 字符串 | **是** | — | 搜索查询 |
| `num_results` | 数字 | 否 | `10` | 要返回的结果数量(最多 20) |
| `pageno` | 数字 | 否 | `1` | 结果页码(最多 100) |
| `categories` | 字符串 | 否 | general | 以逗号分隔的 SearXNG 类别:`news`、`science`、`files`、`images` 等 |
| `language` | 字符串 | 否 | `all` | 语言代码,例如 `en`、`de`、`fr` |
| `time_range` | 字符串 | 否 | — | 按时间范围筛选:`day`、`month` 或 `year` |
| `safesearch` | 数字 | 否 | `0` | 安全搜索级别:`0` = 关闭、`1` = 中度、`2` = 严格 |
**示例 — 英文近期新闻:**
```json
{
"query": "fusion energy breakthrough",
"categories": "news",
"language": "en",
"time_range": "month",
"num_results": 5
}
```
**输出格式。** 每个结果都渲染为一个文本块,格式如下:
标题:示例文章标题
网址:https://example.com/article
摘要:页面的第一句或两句...
引擎:google, bing, duckduckgo
如果 SearXNG 未返回该字段(较旧的 SearXNG 版本或单个引擎配置的结果),则省略 `Engines` 行。该列表反映了 SearXNG 提供的引擎,按顺序排列。顶部没有计算分数——代理可以自由读取引擎计数作为验证信号或忽略它。
---
### `searxng_read_url`
获取一个网址并返回其内容。处理 HTML(转换为结构化 Markdown)、PDF(通过 `pdf_oxide` 提取文本)、Office 文档(DOCX、XLSX、PPTX,以及旧的 DOC、XLS、PPT——通过 `office_oxide` 转换为 Markdown)、纯文本(字符解码)、图像(JPEG、PNG、GIF、WebP 作为 MCP `ImageContent` 块返回,供视觉模型使用——SVG 被故意排除,因为它作为文本比作为 base64 编码的二进制 blob 更有用)。默认情况下缓存结果;图像响应绕过文本缓存。
长文档分页。每个响应返回最多 100,000 个字符的提取文本;如果文本更长,则响应以类似 `[content truncated — showing chars 0-100000 of 348211; call searxng_read_url again with start_index=100000 to continue]` 的通知结束。完整提取的文本(最多 `MAX_EXTRACTED_CHARS`)在第一次获取时缓存,因此后续页面是缓存命中,不会产生上游请求。通知中的偏移量是精确的——代理原样回显它们,服务器会捕捉任何可能分割多字节字符的偏移量,并保证每个页面都会推进,因此遵循继续提示始终终止。
PDF 文本由 `--- [PDF page N of M] ---` 标记行分隔,每页一个,以便代理可以回答“第 47 页有什么内容”,引用页码,并在任何分页窗口中定位。标记是建议性的:它们位于不受信任的内容围栏内,恶意 PDF 可以嵌入类似文本(参见 [SECURITY.md](docs/SECURITY.md))。Office 文档没有页面标记——DOCX 没有固有的页面(分页在渲染时计算,而不是存储在文件中),因此保留的 Markdown 标题是导航锚点;PPTX 幻灯片和 XLSX 工作表作为标题中断面出现。
| 参数 | 类型 | 必需 | 默认值 | 描述 |
|---|---|---|---|---|
| `url` | 字符串 | **是** | — | 要获取的 URL(仅限 http/https) |
| `force_refresh` | 布尔值 | 否 | `false` | 绕过缓存并获取一份新副本 |
| `start_index` | 整数 | 否 | `0` | 从提取的文本中开始的偏移量。使用前一个响应中的截断通知中的值 |
| `max_chars` | 整数 | 否 | `100000` | 此响应中要返回的提取文本的字符数(上限:100000) |
图像网址忽略这两个分页参数,整个图像内容作为图像内容块返回。
**示例 — 强制刷新:**
```json
{
"url": "https://example.com/article",
"force_refresh": true
}
```
**示例 — 从最后一个响应停止的地方继续阅读长文档:**
```json
{
"url": "https://example.com/big-report.pdf",
"start_index": 100000
}
```
---
### `searxng_url_metadata`
仅获取一个 URL 的结构化元数据——标题、作者、发布日期、语言、站点名称、描述、图像、类别和标签——而不返回页面正文。对于 PDF,还返回 `page_count`,以便代理可以衡量候选者是 3 页的备忘录还是 400 页的报告,然后再提交完整读取(它在 Office 文档中故意省略:DOCX 没有固有的页数,因为分页在渲染时计算,而不是存储在文件中)。大约比 `searxng_read_url` 便宜一个数量级的令牌,旨在作为在完整读取候选 URL 之前进行分类的步骤。结果被缓存,缓存与 `searxng_read_url` 共享:元数据获取后是内容获取(或反之亦然)仅需一个上游 HTTP 请求,而不是两个。
| 参数 | 类型 | 必需 | 默认值 | 描述 |
|---|---|---|---|---|
| `url` | 字符串 | **是** | — | 要获取元数据的 URL(仅限 http/https) |
| `force_refresh` | 布尔值 | 否 | `false` | 绕过缓存并获取一份新副本 |
**示例 — 在完整读取一个之前分类三个候选者:**
```json
{ "url": "https://example.com/article-a" }
{ "url": "https://example.com/article-b" }
{ "url": "https://example.com/article-c" }
```
**输出格式。** 一个具有精选元数据字段的 JSON 对象。提取器无法填充的字段被省略,而不是渲染为空字符串或 `null`,因此响应是可变的;至少 `url` 始终存在:
```json
{
"url": "https://example.com/article",
"title": "示例文章标题",
"author": "Jane Doe",
"description": "第一段或元描述。",
"site_name": "Example.com",
"date": "2026-03-12T14:23:00Z",
"language": "en",
"image": "https://example.com/article/cover.jpg",
"categories": ["technology"],
"tags": ["distributed-systems", "go"]
}
```
**何时使用此工具与 `searxng_read_url`。** 使用 `searxng_url_metadata` 来分类多个候选 URL 中哪一个值得完整阅读,用于引文构建,以及在不需要正文时进行日期/作者/站点验证。一旦您决定阅读特定 URL,请使用 `searxng_read_url`。这两个工具共享一个缓存,因此先进行元数据分类,然后再完整阅读所选 URL 不会增加上游负载。
---
## 与 Claude Desktop 结合使用(stdio 模式)
在您的 `claude_desktop_config.json` 中添加以下内容:
```json
{
"mcpServers": {
"searxng": {
"command": "/path/to/mcp-searxng-relay",
"env": {
"SEARXNG_URL": "https://your-searxng-instance.example.com"
}
}
}
}
```
在 stdio 模式下,不需要 `MCP_PORT` 或 `MCP_AUTH_TOKEN`——该进程通过 stdin/stdout 通信,不可通过网络访问。
---
## 与 Claude Desktop 结合使用(HTTP 模式)
如果您更喜欢将服务器作为持久的后台进程运行,而不是每次会话都启动:
```json
{
"mcpServers": {
"searxng": {
"type": "http",
"url": "http://localhost:8080",
"headers": {
"Authorization": "Bearer your-strong-random-token"
}
}
}
}
```
> **注意:** 在任何非本地部署中,请在 TLS 终止反向代理(nginx、Caddy、Traefik)后面运行 HTTP 服务器。服务器本身使用纯 HTTP 进行通信。
## 安全说明
**提示注入。** 两个工具返回的内容都来源于开放的网络——由第三方编写的标题、摘要和页面内容。恶意网站可以尝试在这些内容中嵌入指令(包括在不可见或隐藏的元素中),以便劫持代理的行为,引起意外的工具调用,或者窃取对话上下文。这是使用此服务器与LLM代理的主要运行时风险。
该服务器实现了来自 [Peh, S. (2025), "Prompt Fencing: A Cryptographic Approach to Establishing Security Boundaries in Large Language Model Prompts" (arXiv:2511.19727)](https://arxiv.org/abs/2511.19727) 的提示围栏规范。每个工具响应都被 `<sec:fence>` 元素包装,并带有结构化元数据,之前会有一个简短的意识前言,告诉消费模型如何解释边界:
```xml
<sec:fence xmlns:sec="http://promptfence.org/security/1.0"
signature="MEYCIQDx5w2l7..."
nonce="a9f7e2c14b8d6f31..."
rating="untrusted"
source="https://example.com/article"
timestamp="2026-05-07T14:23:00Z"
type="content">
<extracted content>
</sec:fence>
```
这目前提供:
- **边界逃逸保护。** 每个围栏都带有128位随机`nonce`(来自`crypto/rand`)。攻击者无法猜测`nonce`,因此他们无法伪造一个过早结束围栏的关闭标签或在其中打开一个新的“受信任”的围栏。意识前言告诉消费模型只尊重由每个响应`nonce`标识的边界。
- **前向兼容的签名。** 每个围栏都带有Ed25519签名,以便未来的围栏验证客户端(或外部验证网关)可以验证围栏内容是由此特定服务器进程发出的。签名字节是域分隔、长度前缀的序列化——`"PromptFence/v1.0" || 0x00 || uint64_be(len(content)) || content || canonical_metadata`——馈送到PureEd25519,根据RFC 8032 §5.1(签名操作在内部使用SHA-512哈希;我们不进行预哈希)。这是对论文§4.3中字面上的`Ed25519(SHA-256(C || M))`构造的故意偏离,该构造通过将32字节的摘要馈送到已经哈希其输入的签名方案中,静默地更改了安全参数。域标签防止跨协议签名混淆;长度前缀消除了裸`content || canonical_metadata`连接留下的边界歧义。内容在XML转义之前签名,因此验证器在验证之前会对解析的元素体进行xml转义。确切的线路格式在`fence.go`的`computeFenceSignature`和`buildFenceSigningInput`注释块中记录。**目前没有MCP客户端验证这些签名**;它们存在于前向兼容。
限制,诚实陈述:
- 没有验证器,签名不提供任何加密保证。边界逃逸保护完全依赖于每个响应的nonce。
- Prompt Fencing论文在他们的实验环境中测量了100%的直接注入预防(n=300次尝试,跨两个前沿模型),但结果取决于模型对意识前言的遵守。较小或专门的模型可能会表现出不同的行为。
- 语义攻击——未信任内容试图*说服*而不是*模仿*——不受任何围栏方案的保护。
**公钥。** 运行服务器的Ed25519公钥在`GET /fence/public-key`处公开(HTTP模式,无身份验证——公钥定义上不是秘密)。签名密钥在每次服务器重启时都会重新生成,因此密钥指纹在进程生命周期内会发生变化。需要跨重启稳定验证的运算符应该在服务器后面运行一个持有长期密钥的监护程序——超出此服务器的范围。
对于高风险部署,请考虑将工具限制在已知的域名允许列表中,以最小的权限范围运行代理,并在应用程序层中审计工具调用序列。
**SSRF保护。** URL获取工具(`searxng_read_url`)在TCP对接时解析主机名,并拒绝任何非全局可路由的单播IP地址。两个层在每个对接和每个重定向跳转上运行:
1. 标准库谓词:`IsLoopback`, `IsLinkLocalUnicast`, `IsLinkLocalMulticast`, `IsPrivate`(RFC 1918 + RFC 4193 ULA),`IsUnspecified`, `IsMulticast`, 和`!IsGlobalUnicast`(捕获IPv4定向广播)。
2. 一个硬编码的保留CIDR列表,标准库谓词会遗漏,每个都带有保留它的RFC注释:`0.0.0.0/8`(RFC 1122),`100.64.0.0/10` CGNAT(RFC 6598),`192.0.0.0/24` IETF协议分配(RFC 6890),`192.0.2.0/24` / `198.51.100.0/24` / `203.0.113.0/24` TEST-NET-1/2/3(RFC 5737),`192.88.99.0/24`废弃的6to4 anycast(RFC 7526),`198.18.0.0/15`基准(RFC 2544),`240.0.0.0/4`未来保留包括`255.255.255.255`(RFC 1112),`64:ff9b::/96`和`64:ff9b:1::/48` NAT64(RFC 6052/8215),`100::/64`丢弃前缀(RFC 6666),`2001::/32` Teredo(RFC 4380),`2001:2::/48` IPv6基准(RFC 5180),`2001:10::/28`和`2001:20::/28` ORCHID/ORCHIDv2(RFC 4843/7343),`2001:db8::/32`文档(RFC 3849),`2002::/16` 6to4(RFC 3056)。
两个检查在任何字节到达线路之前运行,重定向链在每个跳转处重新验证,因此攻击者控制的DNS无法在检查和连接之间重新绑定到内部地址。
*访问内部资源(按需)。* 默认情况下,上述阻止所有非公共地址,这是针对获取受攻击者影响的URL的工具的正确姿势。运算符可以在受信任的网络内运行中继并希望它读取内部资源——自托管的Confluence,Jira,GitLab或wiki——可以通过两个允许列表来放宽策略,**默认情况下两个列表都是空的**(因此默认行为保持不变):
- `FETCH_ALLOWED_HOSTS`——完全跳过公共IP检查的精确主机名。匹配基于*请求*主机名,而不是解析的IP,因此您可以命名内部主机而不固定其地址;调用者无法伪造它(主机名来自身份验证的调用者请求的URL),并且您控制自己的DNS,因此这不会重新打开绑定漏洞。匹配不区分大小写和尾随点——`confluence.corp` **不**匹配`sub.confluence.corp`。
- `FETCH_ALLOWED_CIDRS`——视为可达的IP范围。在对接时和每个重定向跳转处针对*解析*的IP进行检查,因此它仍然是防绑定的:攻击者将外观公共的名称重新绑定到私有IP,除非该确切IP落在您列出的范围内,否则仍然被阻止。
这两个是独立的(OR语义):如果主机在允许列表中,或者其解析的IP地址是公共的,或者其解析的IP地址在允许的CIDR内,则允许获取。如果两个列表都不为空,则启动横幅反映放宽的策略(一个`fetch policy`行和您配置的精确`allowed hosts` / `allowed cidrs`),并且会发出一个`warn`级别的审计日志行,以便从日志中可以明显看出获取工具现在可以访问内部目标以及哪些目标。
**身份验证。** 输入的`Authorization`标头会经过一次SHA-256哈希处理,然后在内存中查找与每个配置的`"Bearer <token>"`的SHA-256哈希值相匹配的表。查找操作在固定长度的32字节密钥上进行,因此它不会通过响应时间差异泄露令牌长度(纯粹按字节比较会过早在第一个不同的字节处短路)。只有摘要存储在进程内存中——原始令牌仅在解析期间从环境变量/令牌文件中读取。令牌本身从不出现在日志中;启动横幅仅显示配置的令牌数量和不同的身份。对话成功匹配后,与该令牌关联的身份将附加到请求上下文中,并记录在每个工具调用日志行中(`identity=<name>`),用于审计关联。
**跨源保护。** Streamable HTTP传输由Go的`net/http.CrossOriginProtection`包装(go-sdk v1.4.1+,作为CVE-2026-33252的修复程序——“针对HTTP服务器的跨站点工具执行,无需身份验证”)。浏览器发起的POST请求,其`Sec-Fetch-Site`或`Origin`标头指示跨源请求将被拒绝,POST请求如果没有`Content-Type: application/json`标头也会被拒绝。非浏览器客户端——`curl`,Go `http.Client`,AI代理流量——不会发送`Sec-Fetch-Site`或`Origin`,因此不受影响,因此合法的远程代理使用不受影响。这是除了承载令牌身份验证之外的附加保护:跨源检查在请求处理之前触发,但任何通过检查的请求仍然需要呈现有效的令牌才能访问MCP处理程序。
**容器强化。** Docker镜像以非根用户(UID 1001)在最小的`scratch`基础上运行——运行时镜像仅包含静态链接的二进制文件和CA证书,没有shell,包管理器或OS用户区。
**PDF和Office安全性。** PDF提取使用`pdf_oxide`,Office提取(DOCX/XLSX/PPTX +旧版DOC/XLS/PPT)使用`office_oxide`,两者都是Rust核心,保证所有输入都不会出现恐慌和超时。畸形或对抗性构造的文档将返回错误,而不是使服务器进程崩溃。
**报告和来源。** 安全问题应私下报告——请参见[`docs/SECURITY.md`](docs/SECURITY.md)获取披露过程和范围。代码库主要由AI生成并由单个人类维护者审查、构建和测试后发布;[`docs/supply-chain.md`](docs/supply-chain.md)是完整的依赖项、构建来源和开发过程声明,供审查者在受控环境中评估项目。
---
## 速率限制
在HTTP模式下,服务器对`/`下的请求应用每个调用者的令牌桶速率限制。默认值为每秒`5`个请求,突发`10`个——对于单个代理推理工具来说足够舒适(典型模式是每个代理回合1-3个工具调用,模型思考时间间隔为秒),同时仍然限制了失控代理或令牌泄漏可能造成的损害。设置`MCP_RATE_LIMIT_RPS=0`以禁用。
桶按**身份**进行索引,如果请求带有识别的承载令牌,否则按**远程IP**进行索引。备用方案是有意的:未经身份验证的攻击者从单个主机强行破解令牌,无论呈现哪个令牌猜测,都共享一个按IP索引的桶,因此限制器在网络边缘而不是在身份验证检查处限制攻击。身份验证的调用者按其身份计费——使用相同令牌的多个代理共享一个桶,这是“此凭证的使用预算”的正确语义。
拒绝返回HTTP `429 Too Many Requests`,并带有包含整数秒数的`Retry-After`标头。每次拒绝都会发出一个结构化的WARN日志行,包含`identity`(如果已知)、`remote`、`method`、`path`和`retry_after`,以便审计跟踪记录被拒绝的流量与记录未授权的流量相同。Prometheus计数器`mcp_rate_limit_rejections_total`聚合拒绝的仪表板和警报(无身份标签设计——当需要取证时,结构化日志中已经有了拒绝事件)。
桶存储是一个LRU,最大容量为10,000个条目。身份由配置的身份验证令牌表绑定,因此它们都舒适地适应;上限限制了内存占用,代价是被逐出的桶在下一次联系时重置为满(这不会实质上影响不同攻击者的节流)。
**不涵盖的内容。** `/health`永远不会被速率限制,因此负载均衡器无法标记为滥用。`/metrics`也免除——固定的时间间隔进行抓取的抓取程序不应在Prometheus中产生间隙,看起来像是停机,而滥用抓取程序最好通过扣留令牌而不是对指标端点进行429限制来控制。`/fence/public-key`是无身份验证和无限流的(它是公钥,公开的)。 Stidio模式没有HTTP中间件,因此没有速率限制,但它也是一个具有远程攻击表面的单个受信任进程。
**豁免名单。** `MCP_RATE_LIMIT_EXEMPT=ci,uptime-monitor`完全跳过限制器,仅适用于这些身份。使用它用于内部监控代理,命中MCP根端点(而不是`/metrics`),以及运行高速率功能测试的CI管道。豁免身份的令牌仍应来自强大的源——豁免是关于容量,而不是信任。
**调优说明。**
- *单个代理。* 默认值很好。推理代理每次调用的工具数量很少,远低于5 rps。
- *多个并发代理使用一个身份。* 如果您在单个令牌前面有多个代理,请计算`(agents × peak-burst-per-agent)`并设置`MCP_RATE_LIMIT_BURST`以涵盖它,同时将`MCP_RATE_LIMIT_RPS`设置为实际想要的每个身份的持续预算。或者,拆分为每个代理一个身份,让限制自然堆叠。
- *多副本部署。* 桶按进程划分。在轮询路由下,有效的每个调用者速率为`(replicas × RPS)`;在粘性会话路由下,它是`RPS`。如果您需要全局强制预算,请在Ingress处终止并设置`MCP_RATE_LIMIT_RPS=0`在Pod上。
- *公共/面向互联网。* 将RPS限制为对SearXNG友好的速率,并保持`MCP_RATE_LIMIT_BURST`接近该速率——突发是攻击者首先利用的内容。
---
## 会话限制
在HTTP模式下,服务器将并发会话限制在1,000个。请求初始化超出此限制的请求将收到`503 Service Unavailable`响应。会话在客户端发送`DELETE`请求时被删除。
---
## 操作
在生产中运行服务器的说明。大多数内容都存在于代码和注释中,但这是操作员在第一个事件之前需要了解的详细信息,而不是之后。
### 健康端点
`GET /health`是一个未经身份验证的活动 + 就绪探测。它返回:
| 状态 | 正文 | 含义 |
|---|---|---|
| `200 OK` | `{"status":"ok","searxng":"reachable"}` | 服务器正在运行,上游SearXNG实例的HTTP < 500响应。 |
| `503 Service Unavailable` | `{"status":"degraded","searxng":"unreachable"}` | 服务器正在运行,但上游SearXNG探测失败。 |
上游可达性结果在10秒内缓存,因此轮询负载均衡器不会给SearXNG带来压力。该端点故意未经身份验证(探测不需要携带令牌身份验证),并且故意不受速率限制(高频率的负载均衡器轮询器不应从`/health`收到429)。
包含的[`deployment.yaml`](deployment.yaml)仅使用`/health`作为就绪探测;活动是一个纯粹的TCP套接字探测。这是故意的:暂时的SearXNG停机不应级联到kubelet杀死pod,而是流量被路由到SearXNG恢复之前被路由走。
### `--healthcheck` CLI标志
容器`HEALTHCHECK`指令在[`Dockerfile`](Dockerfile)中调用`mcp-searxng-relay --healthcheck`,这是一个自我检测:二进制文件发出单个`GET`请求到`http://127.0.0.1:$MCP_PORT/health`,超时5秒,如果响应是`200`,则退出`0`,否则退出`1`。标志存在于`scratch`运行时镜像没有shell,`curl`或`wget`编写常规探测——二进制文件必须是它自己的探测。
这适用于纯`docker run`/Compose部署。Kubernetes使用`deployment.yaml`中的HTTP探测器并忽略`HEALTHCHECK`指令。
### 优雅关闭
在`SIGTERM`或`SIGINT`上,服务器停止接受新连接,然后给飞行中的请求提供**30秒**的时间完成退出。如果排水窗口过期且仍有飞行中的请求,进程以非零退出。
两个部署旋钮与此交互:
- **Kubernetes `terminationGracePeriodSeconds。** 大多数集群默认为30s,与排水超时完全匹配 - 留给kubelet交付SIGTERM,服务器接收它,以及响应刷新。设置`terminationGracePeriodSeconds: 45`(或更高)Pod规格,以便排水有真正的机会完成。
- **Compose `stop_grace_period。** 默认值为10s,比服务器的排水超时短。设置`stop_grace_period: 45s`以确保SIGKILL不会在排水期间到达。
对于多副本部署后的Ingress或负载均衡器,LB需要在SIGTERM到达之前将Pod从注册表中注销,否则流量在排水窗口期间继续到达。Kubernetes自动处理此操作,一旦就绪探测开始失败,这就是`/health`是*就绪*探测而不是活动探测的原因。
### HTTP 服务器超时
服务器的stdlib `http.Server` 配置了三个特定的值:
| 设置 | 值 | 原因 |
|---|---|---|
| `ReadTimeout` | `30s` | 限制慢速客户端读取请求行、头部和正文的时间。时间足够长,以适应典型的 JSON-RPC 正文;又足够短,以阻止类似 slowloris 的攻击。 |
| `WriteTimeout` | **禁用(`0`)** | go-sdk 管理 SSE 响应的每个流的截止日期。服务器级的写入截止日期会过早关闭在工具调用期间超过几秒钟的长生存期事件流。 |
| `IdleTimeout` | `120s` | 保持活动空闲窗口。高于典型客户端在工具调用之间的思考时间;低于死连接积累的点。 |
当在服务器前面使用反向代理(推荐用于任何非本地部署 - 请参阅[安全说明](#安全说明))时,代理自己的超时必须适应流式响应:
- **nginx.** 将 `proxy_read_timeout` 和 `proxy_send_timeout` 设置为至少您预期的最长工具调用时间 - 一个推理代理在大型 PDF 或 Office 文档上可能需要 30 秒以上。禁用 MCP 路由的 `proxy_buffering`,以便 SSE 分片立即到达客户端。
- **Caddy.** 捆绑的 [`Caddyfile`](Caddyfile) 在 MCP `reverse_proxy` 指令上设置 `flush_interval -1`,这就是禁用 Caddy 对流式响应的缓冲。
- **Traefik.** 使用 `forwardingTimeouts.responseHeaderTimeout` 字段,并确保入口点未配置为激进的空闲超时。
如果您在反向代理部署中看到工具调用失败并出现截断的 SSE 流,通常是代理的读写超时导致,而不是中继的。
---
## 构建 Docker 镜像
```bash
docker build -t mcp-searxng-relay .
```
多阶段构建在 `golang:1.26.3-trixie` 构建器上编译二进制文件,并仅复制静态二进制文件和 CA 证书到 `scratch` 运行时镜像中。
---
## 日志记录
所有日志输出都发送到 **stderr**。设置 `LOG_FORMAT=json` 以获得与日志聚合器兼容的结构化日志记录。
启动时,服务器无论日志级别如何,都将配置横幅打印到 stderr。横幅列出所有活动的设置,并对机密进行屏蔽。当设置了 `AUTH_USERNAME` 时,它才会显示。
```
######################################################################################################################
mcp-searxng-relay v1.0.0
######################################################################################################################
模式 streamable-http
地址 :3000
searxng http://searxng:8080
密码 [未设置]
用户代理 Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36
缓存 TTL 5m0s
缓存条目 1000 最大
正文限制 500000 字节
PDF 限制 50000000 字节
Office 限制 50000000 字节
图像限制 7500000 字节
日志级别 信息
日志格式 文本
会话模式 无状态
身份验证令牌 3 个配置(3 个身份)
速率限制 5 rps,突发 10
围栏密钥 3e21267250e41cbb
######################################################################################################################
```
服务器运行时,典型的日志行如下所示(有状态模式,`LOG_FORMAT=text`):
```
时间=2026-05-24T07:41:10.301Z 级别=INFO 消息="URL 已获取" URL=https://github.com/asgeirtj/system_prompts_leaks 内容类型="text/html; charset=utf-8" bytes_raw=372821 chars_extracted=5469
时间=2026-05-24T07:41:10.302Z 级别=INFO 消息="获取完成" URL=https://github.com/asgeirtj/system_prompts_leaks 类型=文本 身份=zed 会话 ID=O3GD67SQIYXDYN57XCVQMZYKDI
时间=2026-05-24T07:43:39.212Z 级别=INFO 消息="搜索完成" 查询="site:github.com/asgeirtj/system_prompts_leaks \"Claude Code\" 系统提示" 页=1 结果=10 类别="" 身份=zed 会话 ID=O3GD67SQIYXDYN57XCVQMZYKDI
时间=2026-05-24T07:43:52.249Z 级别=INFO 消息="URL 已获取" URL=https://github.com/asgeirtj/system_prompts_leaks/blob/main/Anthropic/claude-code.md 内容类型="text/html; charset=utf-8" bytes_raw=500000 chars_extracted=185
时间=2026-05-24T07:43:52.253Z 级别=INFO 消息="获取完成" URL=https://github.com/asgeirtj/system_prompts_leaks/blob/main/Anthropic/claude-code.md 类型=文本 身份=zed 会话 ID=O3GD67SQIYXDYN57XCVQMZYKDI
时间=2026-05-24T07:44:07.656Z 级别=INFO 消息="URL 已获取" URL=https://raw.githubusercontent.com/asgeirtj/system_prompts_leaks/main/Anthropic/claude-code.md 内容类型="text/plain; charset=utf-8" bytes_raw=58874 chars_extracted=58873
时间=2026-05-24T07:44:07.657Z 级别=INFO 消息="获取完成" URL=https://raw.githubusercontent.com/asgeirtj/system_prompts_leaks/main/Anthropic/claude-code.md 类型=文本 身份=zed 会话 ID=O3GD67SQIYXDYN57XCVQMZYKDI
```
`session_id` 字段将每个工具调用返回到 `"会话初始化"` 行,其中客户端的 `identity` 首次记录;它们共同形成审计跟踪。`"unauthorized request"` 行显示了一个失败的 bearer-token 尝试的样子 - 拒绝的 `Authorization` 值永远不会被记录,只有远程地址。在 `LOG_FORMAT=json` 中,相同的字段作为每个行的扁平 JSON 对象出现,这就是大多数日志聚合器期望的。
---
## 指标
在 HTTP 模式下,`GET /metrics` 返回 Prometheus 文本格式的计数器。身份验证适用(与工具端点相同的 bearer token)。
暴露的系列是:
| 系列 | 标签 | 备注 |
|---|---|---|
| `mcp_searches_total` | — | 所有 `searxng_web_search` 调用 |
| `mcp_search_errors_total` | — | 上述返回错误的子集 |
| `mcp_metadata_total` | — | 所有 `searxng_url_metadata` 调用 |
| `mcp_metadata_errors_total` | — | 上述返回错误的子集 |
| `mcp_fetches_total` | — | 所有 `searxng_read_url` 调用 |
| `mcp_fetch_errors_total` | — | 返回错误的子集 |
| `mcp_fetches_by_type_total` | `type=html\|pdf\|office\|plain\|image` | 按使用的提取器划分的成功获取 |
| `mcp_fetches_by_domain_total` | `domain=<host>`,`outcome=success\|error` | 按域划分的成功/失败计数器 |
| `mcp_cache_hits_total` | — | 从缓存中提供的 `searxng_read_url` 请求 |
| `mcp_cache_misses_total` | — | 落入网络获取的请求 |
| `mcp_cache_force_refresh_total` | — | `force_refresh=true` 的请求 |
| `mcp_rate_limit_rejections_total` | — | 每调用者速率限制器拒绝的 HTTP 请求(429 响应)。拒绝详细信息 - 身份,远程,重试 - 位于结构化 WARN 日志中;此处没有按身份划分的标签 |
| `mcp_active_sessions` | — | 仪表:当前活跃的 MCP 会话(仅有状态模式) |
### 按域划分的基数
`mcp_fetches_by_domain_total` 被限制为 512 个不同域。一旦达到此上限,额外的唯一目标将被聚合在合成标签值 `domain="__overflow__"` 下,而不是进一步扩展标签集。容量是一个刻意的设计选择:一个代理获取许多唯一主机不应无限制地增长进程内存或 Prometheus 索引。
如果您的环境中溢出计数器非零,则您的代理舰队合法地触摸了超过 512 个域(在这种情况下,请在 `metrics.go` 中提高 `maxTrackedDomains` 并重建),或者您传递给工具的查询有问题(在这种情况下,溢出通过发出信号来完成其工作)。想要对每个 URL 进行完整审计的运算符应依赖于结构化的获取日志行(`url=…`)而不是指标计数器;指标是可观察性,而不是来源。
### 按域划分的指标是什么 *不是*
它不是服务器读取的阻止列表输入。该项目不会根据失败率自动阻止域 - 该决定属于操作员。预期的工作流程是:操作员在 Prometheus / Grafana 设置中审查按域划分的失败计数,决定哪些(如果有)主机丢弃,并相应地更新其静态配置。与其行为会发生变化的系统相比,这保持了服务器在任何给定时刻的行为仅受其配置的控制,这使其可审计。
MCP 配置
以下是此 MCP Server 的配置信息,可直接复制到 Cursor 或其他 MCP 客户端中使用。
mcp.json
连接信息
猜你喜欢
暂无推荐服务器