内容
# web-search-prime-fixer
web-search-prime-fixer 是一个**标准化的 MCP 服务器**(不是代理),基于官方的 Go MCP SDK 构建,旨在使代理的**首次**网络搜索尝试成功——无论它如何猜测工具名称或混淆参数——同时引导它朝着一种规范形式。
它作为一个 Go 二进制文件运行,用于单个开发人员运行单个本地实例,以供其自己的 MCP 客户端(pi、Claude Code、Cursor)使用。
## 它解决了什么问题
当代理调用网络搜索 MCP 工具时,两个故障层同时出现,使得第一次尝试返回为空或错误(PRD §1/§2):
- **层 1 — 错误的工具名称。** 客户端为每个工具添加前缀名称(例如 `web_search_web_search_prime`),因此代理的猜测 `web_search(...)` 调用了一个不存在的工具。
- **层 2 — 错误的参数/形状。** 即使工具解析,代理也会发送 `{"query": "..."}` 或 `{"q": "..."}` 或一个裸字符串,而上游期望 `{"search_query": "..."}`,结果为空或错误,并重试。
解决方法是**两部分**(PRD §5):**第 A 部分**解锁客户端,使工具按其裸名称公开,而**第 B 部分**(此服务器)宣传一个规范工具,从任何输入形状中提取查询,委托一个干净的调用到 z.ai,并在结果后附加一个简短的警告,如果用法是非规范的。第一次尝试应该直接成功。
## 它是什么(和不是什么)
- **它是**一个真正的 MCP 服务器,拥有 JSON-RPC 接口和 `tools/list`,宣传一个工具(`web_search`)。为了回答调用,它充当 MCP **客户端**到 z.ai:一个延迟初始化的会话,被重用,并在过期时重新初始化。
- **它不是**一个字节级直通代理或字节转发器,也不是一个开放的代理。它重写和标准化,然后委托每个入站请求的调用。
- 它仅绑定 `127.0.0.1` 并仅拨打配置的上游,因此不能用作开放中继。
## 规范接口
服务器教授一种形式(PRD §9.4):
- **工具:** `web_search`
- **参数:** `query`(字符串,必填)
- **可选参数**(如果提供,则传递给 z.ai):`location`、`content_size`、`search_recency_filter`
```json
{ "query": "rust async runtime" }
```
任何其他内容——不同的工具名称、不同的参数名称、嵌套/裸字符串/数组输入或推断的可选参数——都会触发警告(参见 [代理看到的内容](#代理看到的内容))。z.ai 品牌的工具名称 `web_search_prime` 是服务器内部调用的上游工具,但**从未**向代理公开(如果它出现在配置的 `tools` 列表中,则会被拒绝)。
## 工作原理
对于每个入站的 `tools/call`:
1. **提取** 查询从任何输入形状(`extract.go`)——任何配置的 `query_aliases` 或裸字符串/数组/嵌套对象。
2. **如果未找到查询**,则立即返回警告并 **不** 触及上游。
3. **否则**,使用规范的 `query` 参数委托 **一个** 清洁调用到 z.ai,映射可选别名(`location`、`content_size`、`search_recency_filter`),如果代理使用非规范名称。
4. **返回结果**,如果调用是非规范的,则在结果后 **追加** 警告。
修复的两部分:
- **第 A 部分 — 客户端解锁:** 配置 MCP 客户端以裸名称公开工具(`toolPrefix: "none"`)。如果没有这个,代理仍然无法按裸名称调用 `web_search`。
- **第 B 部分 — 此服务器:** 公告 `web_search`,从任何输入中提取查询,委托一个干净的调用,并教授规范形式。
## 配置您的 MCP 客户端
这是 **第 A 部分** 解锁——代理必须能够按裸名称访问 `web_search`。将此块放入 `~/.pi/agent/mcp.json`(PRD §20):
```json
{
"settings": { "toolPrefix": "none" },
"mcpServers": {
"web_search": {
"type": "http",
"url": "http://127.0.0.1:8787/mcp",
"headers": { "Authorization": "Bearer ${Z_AI_API_KEY}" }
}
}
}
```
- **`toolPrefix: "none"`** 使 pi 按裸名称公开每个工具,因此 `web_search` 可以作为 `web_search` 调用,而不是 `web_search_web_search_prime`。它在操作员的所有 MCP 服务器上全局应用。如果两个服务器曾经公开相同的工具名称,则操作员负责处理该冲突(参见 [非目标](#非目标))。
- **`Authorization` 保留在客户端。** 服务器原样转发它到 z.ai,并且不持有自己的密钥(PRD §17,FR-7)。`${Z_AI_API_KEY}` 是操作员自己的 z.ai API 密钥。
- **其他客户端(Claude Code、Cursor):** 使用使它们公开裸工具名称的内容(按客户端记录);URL `http://127.0.0.1:8787/mcp` 是相同的。
## 代理看到的内容
三种情况(FR-6:`isError` **从未** 为任何规范、指导或警告情况设置):
1. **规范调用** (`web_search` + `query`) → 仅结果。无警告,无重试。
2. **非规范调用**(任何其他工具名称/参数名称/嵌套/裸字符串/数组/推断或规范化可选参数)→ **结果优先**,然后警告 **附加在结果后**:
```
[web-search-prime-fixer] 警告:此调用使用 "<tool>"/"<source>" 而不是规范形式。结果在上方。下次调用:web_search with { "query": "..." } — 例如 web_search({ "query": "rust async runtime" })。
```
3. **空输入**(任何地方没有可用的查询)→ 立即警告 **下方**,并且 **不** 进行上游调用:
```
[web-search-prime-fixer] 警告:无法在参数中找到搜索查询;未运行搜索。调用:web_search with { "query": "..." } — 例如 web_search({ "query": "rust async runtime" })。
```
示例查询 `"rust async runtime"` 是一个固定文字,两个警告都使用 Unicode EM DASH(`—`,U+2014)之前 `e.g.`。
## 构建
需要 Go — 在 `go.mod` 中查看 `go` 指令(当前为 `go 1.25.0`)。服务器依赖于官方的 Go MCP SDK(`github.com/modelcontextprotocol/go-sdk` v1.6.1);解析它,然后构建:
```
go mod tidy
go build -o web-search-prime-fixer .
```
输出二进制文件是 `./web-search-prime-fixer`。要设置 `/healthz` 报告的版本,请在构建时注入它(普通构建报告 `dev`):
```
go build -ldflags "-X main.version=0.2.0" -o web-search-prime-fixer .
```
## 运行
无需参数和配置文件,服务器监听 `127.0.0.1:8787` 并委托给 z.ai 上游:
```
./web-search-prime-fixer
```
更改绑定地址:
```
WSPF_LISTEN=127.0.0.1:9000 ./web-search-prime-fixer
```
加载配置文件(`WSPF_CONFIG` 原样使用;如果命名文件丢失,服务器退出并出现错误,而不是回落到默认值):
```
WSPF_CONFIG=./my-config.json ./web-search-prime-fixer
```
服务器仅绑定 `127.0.0.1`(它不是一个开放代理)。TLS 在环回 HTTP 到 TLS 上游时是多余的。它在 `Ctrl-C`(`SIGINT`)或 `SIGTERM` 上正常关闭,在 10 秒内排空正在进行的上游调用。
## 健康检查
确认服务器正在运行:
```
curl http://127.0.0.1:8787/healthz
```
`GET /healthz` 返回 `200`,带有正文 `{"ok":true,"version":"<version>"}`(`Content-Type: application/json`)。非 GET 请求返回 `405`。`/healthz` **不** 触及上游;它是一个本地活动检查。`version` 默认值为 `dev`,在构建时通过 `-ldflags "-X main.version=..."` 设置。
## 配置
服务器从内置默认值运行,无需配置文件。覆盖环境变量或 JSON 配置文件。环境变量(设置且非空时)具有最高优先级,在加载文件后应用。
### JSON 键和默认值(来自 `config.go` `DefaultConfig`)
| JSON 键 | 默认值 | 备注 |
| --- | --- | --- |
| `upstream` | `https://api.z.ai/api/mcp/web_search_prime/mcp` | 必须是绝对 URL。 |
| `listen` | `127.0.0.1:8787` | 必须解析为 `host:port`。仅本地。 |
| `path` | `/mcp` | 信息性/保留。 |
| `tools` | `["web_search"]` | 公告的工具名称;`tools[0]` 是规范的。永远不是 `web_search_prime`。 |
| `canonical_tool` | `web_search` | 必须存在于 `tools` 中。 |
| `canonical_param` | `query` | 我们教授的参数。 |
| `query_aliases` | `["query","search_query","q","search","searchQuery","search_term","term","text","input","prompt","question","keywords","topic","searchString"]` | 有序切片;索引顺序 = 提取优先级。**从 v1 的 `aliases` 重新命名** — 参见下面的重大更改。 |
| `optional_aliases` | `{"location":["country","region"],"content_size":["size","contentSize","detail"],"search_recency_filter":["recency","freshness","time_filter","date_filter"]}` | z.ai 规范可选的映射 → 别名优先级切片。 |
| `target_tool` | `web_search_prime` | 内部调用的 z.ai 工具;永远不会公开。 |
| `target_param` | `search_query` | 发送到上游的 z.ai 参数;如果为空,则强制为 `search_query`。 |
| `log_level` | `info` | `debug`、`info`、`warn`、`error` 之一。 |
### 环境变量
| 环境变量 | 覆盖 | 备注 |
| --- | --- | --- |
| `WSPF_CONFIG` | 配置文件路径 | 原样使用。缺少命名文件是 **硬错误** (退出非零),而不是静默回落到默认值。 |
| `WSPF_UPSTREAM` | `upstream` | 空值被忽略。 |
| `WSPF_LISTEN` | `listen` | 空值被忽略。 |
| `WSPF_LOG_LEVEL` | `log_level` | 空值被忽略。 |
没有 `path`、`tools`、`canonical_tool`、`canonical_param`、`query_aliases`、`optional_aliases`、`target_tool` 或 `target_param` 的环境变量覆盖 — 在配置文件中设置这些。
### 配置文件发现
1. `WSPF_CONFIG`(如果设置,则原样使用;缺少文件是硬错误)。
2. 否则,以下 **存在** 的文件:
- `./web-search-prime-fixer.json`
- `$XDG_CONFIG_HOME/web-search-prime-fixer/config.json`(默认为 Linux/macOS 上的 `~/.config/web-search-prime-fixer/config.json`,通过 `os.UserConfigDir` 可移植解析)
3. 否则,无文件 — 服务器使用内置默认值(无需零配置)。
### 重大更改:`aliases` → `query_aliases`
v1 JSON 键 **`aliases`** 已重命名为 **`query_aliases`**(v2 中具有相同的 `[]string` 语义)。**具有 v1 配置文件的运算符必须重命名键 `aliases` → `query_aliases`** — 否则该键将被静默忽略(未知字段将被丢弃),并且操作员的自定义别名列表将丢失(提取将回落到内置默认列表)。新 v2 字段没有 v1 等效字段:`tools`、`canonical_tool`、`canonical_param`、`optional_aliases`、`target_tool`。(磁盘 `config.example.json` 通过并行更改更新为 v2 架构。)
### 验证
任何验证失败时,服务器都会记录并以非零退出。它检查:
- `listen` 解析为 `host:port`(`net.SplitHostPort`)。
- `upstream` 是绝对 URL(`url.Parse` + `IsAbs`)。
- `tools` 非空 **且** 包含 `canonical_tool`。
- 没有 `tools` 条目等于 `target_tool`(永远不要公开 z.ai 的真实名称)。
- 未知 JSON 字段被忽略。
- `Config` **不持有** 凭证字段(授权是客户端拥有的请求头,而非服务器 — 因此启动日志记录无法泄漏密钥)。
参见 `config.example.json` 以获取一个可以直接复制的示例。
## 非目标
服务器的任务明确是 **规范化** 为一种规范形式。它不:
- 充当通用搜索引擎(它委托给 z.ai 的 `web_search_prime`;没有合成、排名、缓存或结果发明);
- 充当通用或开放代理(它绑定 `127.0.0.1` 并仅拨打配置的上游);
- 拥有凭证(它转发客户端的 `Authorization` 头并持有自己的密钥);
- 重试、速率限制或缓存上游调用(每个入站调用一个委托调用;上游错误诚实地显示);
- 截断查询文本(提取的查询原样转发);
- 提供多工具语义(每个公开的工具执行相同的操作);
- 公告 z.ai 品牌名称(`web_search_prime` 永远不会暴露给代理);
- 对 `toolPrefix: "none"` 下的工具名称冲突承担责任(操作员负责该冲突)。
## 测试
```
go test ./...
```
测试套件使用进程内 `httptest` 假人和金色 SSE 夹具。它不进行网络调用,不需要凭证。