Files
codex-app-server/docs/web-search-mcp.md
T
host b72a2abae4 feat: websearch MCP worker wiring + egress (public SearXNG route)
Worker --websearch-mcp (default off) seeds tenant codex-home with
websearch MCP on _tenant_dirs (container path); old tenants merge
on next task. Autoscaler unit gains the flag. Tinyproxy filter
adds Perplexity/Exa/Tavily/Brave/DuckDuckGo + exact
searx.diante.me (no diante.me wildcard, control plane stays out).
Tests: test_websearch_worker 4 + websearch_mcp 12 + tenant 7 = 23 OK.
Not pushed, not merged, not deployed; SearXNG host/DNS/nginx and
tinyproxy reload are host steps.
2026-10-28 17:26:31 +08:00

118 lines
5.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 网页搜索 MCP v1(oh-my-pi 模式,Python 实现)
目标:给 codex 任务一个可用的 `web_search` / `web_fetch`,
不把 oh-my-pi 的 TypeScript 代码原样搬过来(codex 认不出
pi 的工具),只复用它的模式:统一工具 + provider 抽象 +
`auto` 链式回退 + 抓取转干净文本。
## 文件
- `cloud/websearch.py`:provider 链、抓取、codex-home 播种
助手。纯标准库,无第三方依赖,这样它能不经 pip 安装就
在宿主和 Docker 容器里跑。
- `cloud/websearch_mcp.py`:stdio JSON-RPC MCP 服务器,
只做协议帧,逻辑全在 websearch.py。日志只写 stderr,
stdout 是 JSON-RPC 流,绝不能混日志。
- `cloud/tests/test_websearch_mcp.py`:12 个单测,全部
mock HTTP,不联网、不需要真实 key。
## 工具
- `web_search(query, limit=5, provider="auto")`:返回
`{provider, results:[{title, url, snippet, score?}], answer?, attempted}`。
`answer` 只有 provider 本身给合成答案时才有(目前是
Perplexity Sonar);其它 provider 的总结仍由主模型做。
- `web_fetch(url, char_limit=15000)`:返回
`{url, title, text, links, truncated}`。超长按头 75% +
尾 25% 截断并标 `[TRUNCATED]`;只接受 http(s),拒绝
localhost 与字面私有 IP。
## Provider 链(auto 顺序)
1. `perplexity`:`POST https://api.perplexity.ai/chat/completions`,
模型 `sonar`,`Authorization: Bearer $PERPLEXITY_API_KEY`。
返回合成答案 + `search_results` / `citations`。
2. `exa`:`POST https://api.exa.ai/search`,头 `x-api-key`,
`type: auto`,`contents.text.maxCharacters: 2000` +
`highlights: true`。
3. `tavily`:`POST https://api.tavily.com/search`,
`Authorization: Bearer $TAVILY_API_KEY`,
`search_depth: basic`,`include_answer: false`(工具只返
结果,总结留给模型)。
4. `brave`:`GET https://api.search.brave.com/res/v1/web/search`,
头 `X-Subscription-Token`,`extra_snippets: true`。
5. `searxng`:`GET $SEARXNG_URL/search?format=json`,需自托管
实例且 JSON 格式已打开。
6. `duckduckgo`:解析 `https://html.duckduckgo.com/html/`,
免 key 兜底,布局漂移时只会返回空结果并继续走链,
不会把工具打崩。
规则:auto 只试已配置的后端(有 key 的付费后端 + 已配
URL 的 SearXNG + DuckDuckGo);任一后端 HTTP 失败或
0 结果都继续下一个;全部失败返回各后端错误(不含 key)。
`provider` pin 单个名字时不回退,失败即报错。
可用 `WEBSEARCH_PROVIDERS`(逗号列表)改默认链顺序,
`WEBSEARCH_TIMEOUT`(秒,默认 20)改单次超时。
## 接入 codex(测试项目做法)
codex 从 codex-home 的 `config.toml` 读 MCP 配置:
```toml
[mcp_servers.websearch]
command = "python3"
args = ["/path/to/codex-home/websearch_mcp.py"]
```
`ensure_codex_home_websearch(codex_home_dir, command_args=...)`
会把 `websearch.py` / `websearch_mcp.py` 拷进该 codex-home,
并在缺失时追加上面这段(已有 section 绝不重写;key 绝不
写进 config.toml,key 只走 MCP 进程的环境变量)。
注意:同一个 config.toml 不能同时装宿主路径和容器路径,
Docker 租户必须传容器内路径
`/var/lib/codex-app-server/codex-home/websearch_mcp.py`,
因为该目录正是租户 codex-home 的容器内挂载点。
## 生产接线(worker + egress + 自托管 SearXNG)
- worker 自动播种:`cloud/worker.py --websearch-mcp`(默认关)。
开启后 `_tenant_dirs` 在播种 config.toml 之后调播种助手,
写容器内路径 `/var/lib/codex-app-server/codex-home/websearch_mcp.py`。
老租户下一次任务自动合并(`_tenant_dirs` 每次派发都调到),
已有 section 不重写。autoscaler unit 已加 `--websearch-mcp`,
但 unit 文件不在 deploy tar 里,主机上须单独 scp +
daemon-reload + restart autoscaler 才生效(见部署教训)。
- egress 白名单:`deploy/egress/tinyproxy-filter` 已加
Perplexity、Exa、Tavily、Brave、DuckDuckGo 与
`searx.diante.me`(精确匹配,不用 diante.me 通配,否则
任务经代理也能摸到 api.diante.me 控制面)。主机上须把
该文件拷到 `/etc/tinyproxy/tinyproxy-filter` 并重载
tinyproxy,重载后先验中转域名还通,再验新域名。
- 自托管 SearXNG(公开子域名路):主机 Docker 跑
`searxng/searxng`,经 nginx 反代到 `searx.diante.me`
(DNS A 记录 + certbot 证书),MCP 侧
`SEARXNG_URL=https://searx.diante.me` 走 worker 进程
环境(`/etc/codex-app-server/env`)继承到 codex 再到
MCP,不写进 config.toml。公开实例的代价是可能被外人
刷:SearXNG limiter 保持开启,JSON 格式是 MCP 要用的
唯一格式。未加域名进白名单之前,Docker 任务的搜索
会被代理 403 挡住(fail closed,工具报错,不会静默绕过)。
- 付费后端实测:付费后端只以 mock 响应验证了请求
字段与结果映射,未用真实 key 跑过;上生产前须用真实
key 在测试项目跑一遍,再谈默认链顺序是否合理。
## 诚实边界
- 本批只做代码 + 单测 + 测试租户目录的配置合并验证,
未推分支、未建 PR、未合并、未部署、未改生产 worker
与 egress 配置、未跑真实 codex 任务。
- DuckDuckGo 的 HTML 解析是兜底手段,布局一变就可能
0 结果;它存在的意义是“没 key 也能走通链路”,不是
长期依赖的搜索后端。实测(2026-10-28,VM 出口):
html / lite 端点均回 202 + 机器人挑战页(anomaly),
不是解析器 bug——在这个出口下免 key 搜索实际不通,
免 key 可用性最终要靠自托管 SearXNG 或付费后端。
- `web_fetch` 不是安全边界:danger-full-access 的任务
本来就能 curl,fetch 只挡了最明显的非 http(s) 与
本地私有地址,不应把它当沙箱用。