关于 agent-web-search-setup
agent-web-search-setup 是用于诊断和配置 Agent 网页搜索工具的技能。它测试实际模型端点,并指导移除不可用的内置工具,以便使用已配置的替代工具。
主要功能
- 01探测 Anthropic 和 OpenAI 搜索端点
- 02区分工具不可用与结果不确定
- 03识别后端和 API 路由限制
- 04配置在 Claude Code 和 Codex 中移除工具
- 05保留现有设置与凭据
适用场景
- 01排查 Agent 无法返回网页结果
- 02检查经由中继的托管搜索是否可用
- 03在客户端设置中停用失效的搜索工具
- 04在 Agent 会话中验证工具可用性
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ daymade/ claude-code-skills --skill agent-web-search-setup安装后在 Claude Code 中输入/agent-web-search-setup确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/backends.mdscripts/diagnose.pytests/test_diagnose.py详细介绍
Agent 网页搜索设置
无法搜索的模型后端,与搜索后什么也没找到的模型并不相同,但在对话中两者看起来完全一样。此技能会在你当前使用的 Agent 上,把前者变成后者。它配置 Agent 的工具;它本身不是搜索引擎,因此这里没有任何内容能直接回答查询——完成的证明是 Agent 在自己的会话中调用了真实工具。
实际会出什么问题
Anthropic 的 web_search 和 web_fetch 是服务端工具:API 会在请求期间运行它们,并将结果拼接到同一响应中。OpenAI 的 Responses API web_search 也是如此。客户端只需声明该工具。
将请求代理到其他云服务的转售商无法执行这些工具。Anthropic 自己的文档指出,Amazon Bedrock 和 Google Cloud 上无法使用网页抓取;实测发现,搜索在这两者上也同样失败。返回的并不是错误,而是一个空壳,因此模型会得出该主题不存在的结论,并且信心十足地这么说。这就是整个问题的症结,也是为什么用户报告说自己的 Agent“变笨了”,而不是说工具坏了。
由此可知以下三点,每一点都花了不少时间才确认:
- 切换模型没有帮助。 实测发现,一家转售商将四个模型名称路由到三个不同的后端;没有一个能正常搜索。
web_fetch也会随之失效。 只修复搜索,会让故障从“我什么也没找到”变成“我找到了链接,但打不开”。- 单独安装替代工具还不够。 让失效工具继续留在可选列表中时,模型调用了 21 次网页搜索,却一次也没有调用已安装的替代工具,尽管其描述指出内置工具已失效。内置工具会在直接竞争中胜出。必须移除失效工具,否则其他操作都无济于事。
1. 在这台机器上验证
不要只听用户说哪个工具坏了,也不要只听你自己的判断。
下面的 <skill> 是存放此 SKILL.md 的目录——请替换为实际路径。如果不知道路径,可以用 find ~ -type d -name agent-web-search-setup 找到它。
python3 <skill>/scripts/diagnose.py
它会读取 ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN / ANTHROPIC_MODEL;使用 --api openai 时,则读取对应的 OPENAI_ 变量;也可以通过 --url --key --model 传入。退出码:0 表示无需修复,1 表示至少有一个工具失效,2 表示无法确定。使用 --json 以便解析。
这些值通常不在 shell 中——请从客户端读取。 使用中转服务的人通常通过服务商切换器或设置界面进行配置,而不是导出变量,因此普通 shell 看不到任何内容,探测也会在开始前停止。Claude Code 的这些值位于 ~/.claude/settings.json 的 env 区块中;Codex 的基础 URL 位于 ~/.codex/config.toml 的 [model_providers.*] 下,密钥则位于 ~/.codex/auth.json 中。从这些位置读取后,通过 --url 和 --key 传入。切勿打印密钥。
此步骤需要可用的 python3,而在全新的 Mac 上,首次运行时会弹出一个对话框。 /usr/bin/python3 附带的只是一个桩程序,首次调用时会打开 Command Line Tools 安装程序。请提前说明会出现此提示,而不是等它出现后再解释。脚本本身只使用标准库,可在 macOS 附带的 Python 3.9 上运行,因此无需安装其他内容——这正是弹出这个对话框与需要 Node 加 SDK 的 MCP 服务之间的区别。
它会给出每个工具的判定结果,并根据工具调用 ID 前缀指出实际后端(toolu_vrtx_ 表示 Vertex AI,toolu_bdrk_ 表示 Bedrock,ws_ 表示 OpenAI 托管工具)。如果模型只是拒绝调用工具,结果会报告为无法确定,而非工具失效;请重新运行或尝试其他模型,不要把沉默当作证据。
慢不代表不可用,而这两者很容易混淆。 实测发现,某个网关对两个模型 ID 的响应时间约为 15 秒,对第三个则约为 146 秒。如果把超时时间设在这两个数之间,就会把一个完全健康的端点误判为故障,而这正是此技能要纠正的误诊。默认值已考虑较慢的情况;如果缩短超时时间,超时仍会报告为无法确定,而非给出结论。
Codex 通过不同的 API 访问其后端,因此要探测那个 API。 它的托管搜索位于 OpenAI Responses API 中,正常工作的端点会返回一个 web_search_call 项。--api openai 会向该 API 发起请求。只询问 Anthropic 格式,就会让 Codex 用户得不到任何结论。
这种探测最常见的失败原因不是请求被拒绝,而是缺少路由;这一点比看起来更能说明问题。 实测中,某个中继对旧版 /v1/chat/completions 返回 HTTP 200,却对 /v1/responses 返回 HTTP 404。这两点必须结合来看:该端点确实兼容 OpenAI,只是没有实现托管工具所使用的接口。因此,托管搜索完全无法在那里运行;对 Codex 来说,这就足以得出结论,因为它的托管工具使用的正是这条路由。探测会自行发起这第二个请求,并报告为 unavailable;如果没有可用的 Chat Completions 路由,那么单独一个 404 仍会被判为无法确定,因为更可能是密钥或基础 URL 有误。
两个 API 的模型命名空间互不相通,因此 --api both 还需要 --openai-model(或 OPENAI_MODEL)。如果没有提供,OpenAI 探测会被跳过,并明确说明原因,而不会把 Claude 模型 ID 发送到 /v1/responses,再得到一条关于模型的 400 错误,误以为这是对工具的结论。
在 Codex 端,该设置是 web_search。已见过的值有 disabled、cached、indexed 和 live,但请把这份列表视为快照:设置无效时,Codex 会自行打印完整列表;这是确认当前语法最快的方式,也是唯一不会过时的方式。
如果所有探测都表明工具可用,就先停下来检查其他原因,再进行任何配置。 这些工具在该端点上运行正常,因此安装替代工具只会增加另一种搜索方式,并不能修复原有问题。在真实机器上见过两种原因:另一个已安装的组件会在任何工具被调用前响应类似搜索的请求;或者客户端自身的设置关闭了搜索。在对话中,这两种情况看起来都与后端失效完全相同。
2. 移除失效工具
只移除 diagnose.py 实际报告为失效的工具。移除正常工作的工具是回归问题,不是什么安全余量。
| Agent | 位置 | 操作 |
|---|---|---|
| Claude Code,终端或桌面应用的 Code 界面 | ~/.claude/settings.json | 将失效工具的名称添加到 permissions.deny |
| Codex | ~/.codex/config.toml | web_search = "disabled" |
| Claude Desktop,聊天界面 | 不支持的目标 | 见下文 |
客户端使用的名称与 API 不同,写错后不会报错。 diagnose.py 会报告 web_search 和 web_fetch,因为 API 使用这些名称。而拒绝列表需要的是 WebSearch 和 WebFetch。把 API 使用的名称写入 permissions.deny,不会匹配任何工具;因此文件虽然改了,运行也报告成功,但该工具仍然出现在模型的可用列表中——这与什么都不改没有区别。诊断会在结论旁打印客户端使用的名称,正是出于这个原因;请使用这个名称。
在 permissions.deny 中填写裸工具名称,会将该工具完全从模型的上下文中移除,这正是你想要的效果。已通过读取会话的 init 事件进行验证:该工具不会出现在模型收到的列表中,而不是仍在列表中、只附带不要使用它的提示。
下文会多次提到该事件,下面介绍如何实际获取它。
不要直接取第一行——在配置了 SessionStart 钩子的机器上,实测它出现在第 24 行(共 24 行),排在钩子自身输出之后。
应按类型筛选,下面就是这样做的:
claude -p "reply ok" --output-format stream-json --verbose < /dev/null > /tmp/init.jsonl
python3 -c "
import json
for line in open('/tmp/init.jsonl'):
d = json.loads(line)
if d.get('type') == 'system' and d.get('subtype') == 'init':
print('WebSearch:', 'WebSearch' in d['tools'], ' WebFetch:', 'WebFetch' in d['tools'])
break"
两个 False 值表示这些工具已从模型接收的内容中移除。同一事件还包含 permissionMode,建议同时读取,原因见步骤 4。
拒绝权限的影响范围比授予权限更大,这种不对称性在步骤 4 中很重要。
即使在一个从未受信任的目录中,项目自身 .claude/settings.json 里的 permissions.deny 也会生效——实测时读取了 init 事件,其中列出了 24 个工具,WebSearch 和 WebFetch 不在其中,且没有打印任何警告。步骤 4 中的授权则不会这样生效。遵守来自不受信任文件的限制是安全的;授予权限则不然,客户端对这两者采取不同处理是合理的。
**第二种不对称性与同步有关,它决定了多配置文件机器上的拒绝权限应放在哪里。**步骤 4 会将授权写入其他配置文件同步来源的配置文件,因为同步器一旦合并 settings.json,授权就会从其他位置丢失。但拒绝权限不能放在那里:来源配置文件是其他所有配置文件的复制来源,因此放在其中的拒绝权限会影响内置搜索正常工作的配置文件,而移除可用工具正是本步骤开头提醒要避免的回归。既按配置文件区分、又不受同步器影响的层级,是配置文件启动器通过 --settings 传入的设置文件;同一市场中的 claude-switch-models-setup 说明了该文件,以及拒绝权限从中生效的实测结果。操作错误时有一个值得立即识别的症状:修复在执行它的会话中有效,但在你要求用户重启后就失效;这看起来像是重启没有生效,而不是有东西重写了文件。
这两处修改都很简单,直接展示出来比描述一种还得让读者自行构造的格式更好:
{ "permissions": { "deny": ["WebSearch", "WebFetch"] } }
web_search = "disabled"
**这行 TOML 必须放在文件中第一个 [table] 标题之前,把它追加到文件末尾就会出错。**它是一个独立键,因此 TOML 会将其绑定到前面的表——若把它放在 [model_providers.…] 块之后(步骤 1 刚让你进入同一个文件读取内容),它就会变成 model_providers.<name>.web_search,没有任何程序会读取这个设置。文件确实改了,命令也成功了,但托管搜索仍处于启用状态。将它单独放在文件顶部一行,然后重新读取文件确认。
**合并,绝不替换。**这些文件保存着用户自己的配置。只添加这一个键,其余内容保持字节级不变。向用户展示修改前后的内容。
**优先引用键,而不是复制进去。**如果客户端支持从环境变量读取凭据,就注册该引用,而不是把密钥写入配置文件。这些文件可能会被截屏并附在求助请求中,也可能被整体分享;粘贴进去的密钥也会随之泄露。
**桌面应用其实有两个客户端,而不是一个。**它的 Code 界面会运行应用下载的真实
claude 二进制文件,并承载 Claude Code 设置引擎,因此上面的条目适用于它。应用自身的
程序包决定了该引擎会在哪里查找配置:如果应用的设置存储中设有配置根目录,就使用该值;
否则使用 process.env.CLAUDE_CONFIG_DIR;再否则使用 ~/.claude。应用在自己的错误信息中
明确说明了这一点——在 桌面设置 中将 CLAUDE_CONFIG_DIR 指向目标目录,即可更改配置根目录。
因此,编辑 ~/.claude/settings.json 会同时影响两个客户端;唯一会打破这一假设的情况,是用户
在该界面中更改了配置根目录。在告诉任何人问题已修复之前,先询问用户,或者读取会话的
init 事件,确认工具确实已消失。不要顺手修改应用的 coworkWebSearchEnabled
偏好设置——应用会根据账户是否可使用此功能来写入该值,而不是将其作为设置读取,因此手动
修改会在下次启动时被覆盖。
作用域的重要性超乎表面。 claude mcp add 接受 --scope,可选值为
local、user 或 project,默认值为 local——这个名字听起来像是仅限本地,
但实际会写入用户真正的 ~/.claude.json。始终明确传入 --scope,并在修改前说明
即将更改哪个文件。
**聊天界面不在适用范围内,坦白说明胜过临场发挥。**目前没有找到关闭其内置搜索的方法; 它的连接器流程需要在自己的设置中逐步点击,脚本无法操作,而且这里没有记录点击步骤和 菜单名称——因此,尝试操作的 Agent 只能猜测界面。不要这样做。明确告知用户此流程 不涵盖该界面,并将操作转到适用的环境:同一台机器上的 Claude Code,无论是在终端中还是在 桌面应用的 Code 界面中,下面的每一步都只需编辑文件。
如果网关针对工具定义返回 HTTP 400,而不是静默地返回空结果,那么移除工具就不再只是 偏好,而会变成前置条件:请求会在模型读取任何内容之前就验证失败。
3. 安装模型确实可以调用的替代工具
替代工具必须是由客户端执行的工具,因为损坏的工具恰恰缺少这一特性。MCP 服务符合 条件:Agent 在本地运行它,后端只会看到普通的函数调用。
**技能包也要经过同样的检验,因为有些技能包无法通过。**许多“为 Agent 提供互联网访问能力”的
软件包根本不是服务——它们只是一些指令,用来协调模型已经声明的能力。是否会导致问题,
完全取决于具体使用了哪种能力:如果某个软件包的指令最终调用内置的 WebSearch 或
web_fetch,就会继承这里要修复的同一个故障,只是在其上再加一层容易令人信服的包装;
而通过 shell 驱动本地进程(例如 CLI、curl 或脚本)则不受影响。
因此,安装之前先弄清楚软件包实际上是如何访问网络的。这并不意味着应该一概不信任这类软件包: 在这里检查过的软件包中,有两个已在本地安装并经过审阅,结果发现它们都通过 shell 中转, 一个使用 CLI,另一个使用 Python 脚本。无法正常工作的类型范围更窄,菜单中也有明确说明—— 这类软件包会向模型提供一组搜索 URL,并期望模型使用自身的抓取工具读取结果;而在此端点上, 该工具可能已经失效。
我们通过一个内置搜索已失效的网关进行了端到端验证,使用的是一个模型不可能预先知道的随机数。 返回结果与直接 API 对照运行逐字节相同;测试使用了两个模型 ID,而同一网关会将它们路由到 两个不同的云服务——因此,这一往返流程并不依赖某一家供应商的透传功能恰好正常工作。 阅读 references/backends.md,了解当前可选项:每个选项在注册和本地安装方面会给用户带来什么成本、分别可以从哪里访问,以及标记为默认的选项。按以下标准依次选择:
- 无需注册优先于任何需要账户的选项。
- 无需本地安装优先于任何需要运行时的选项。原生 macOS 既没有 Node,也没有可用的
python3——系统自带的那个只是个占位程序,首次使用时会打开 Xcode 安装器对话框;而这里考察的每个 MCP 服务都需要一个不属于任何标准库的 SDK。 - 从用户实际所在的位置可访问。
- **能用用户提问所用的语言回答。**这不是对质量的偏好。一个用中文工作的人收到英文来源,会认为工具仍然无法正常工作,而这正是整个流程要避免的结果。
- 最后才考虑结果质量。能正常工作的搜索胜过更好的搜索,即使后者无法运行。
不要向非技术用户展示全部选项。直接选择,花一句话告诉他们你选了什么以及原因,并将其他选项留作他们提出要求时再说。
在 Codex 上,不要使用 codex mcp add 注册托管服务。请自行写入这两行。codex mcp add <name> --url <url> 会写入配置项,随后发现该端点声明了 OAuth,便会启动浏览器流程、打印授权 URL,并无限期阻塞——因此,同步执行此命令的 Agent 会卡住自己的会话,且没有任何等待时限。实际上无需等待:流程启动前配置项就已写入磁盘,而默认后端之后在未完成该流程的情况下匿名作出了响应。跳过此命令,追加以下内容;它与该命令写入的内容逐字节相同:
[mcp_servers.exa]
url = "https://mcp.exa.ai/mcp"
与步骤 2 中的 web_search 行不同,这一行自带表头,因此放在文件末尾是安全的。
其次,若在 Git 仓库之外运行 codex exec,会因 Not inside a trusted directory and --skip-git-repo-check was not specified 而拒绝运行;请传入该标志,或在受版本控制的目录中运行。对于原生 Mac,timeout 也无法解决上述任一问题——基础系统中没有它,只有 Homebrew 的 coreutils 中才有。
上述整个部分都针对真实后端进行了端到端测试,而不是纸上推演:web_search = "disabled",执行了一次 codex mcp add,随后模型通过调用 mcp__exa__web_search_exa 和 mcp__exa__web_fetch_exa 回答了一个时事问题;它打开的页面所标注的发布日期比测试日期早一天——足够新,不可能由任何训练数据提供。它没有托管搜索工具可作备用:它自行列出的工具中并没有这类工具。
4. 验证它能正常工作
写入配置不等于交付。运行一个模型不可能已经知道答案的查询——比如询问本周发生的事——并向用户展示真实结果和真实 URL。非技术人员若无法分辨“已配置”和“能正常工作”,那就还没有完成。
**验证时只能启用刚安装的那个工具。**如果注册了多个后端,正常工作的那个会替无法工作的那个作答,检查便会在配置实际上并不可用的情况下显示通过。请明确确认是哪个工具生成了结果,并给出工具名称,而不是只要结果出现就接受。同样的陷阱曾经影响过一款已发布的产品;现在,该产品会在连接测试期间禁用自己的备用方案,正是出于这个原因。
这项检查物有所值。整个流程是在一个实时中继服务上运行的,而同一诊断刚刚报告该服务的内置搜索已失效;会话报告的权限为严格的 default,机器上还连接着另外四个本可提供答案的 MCP 服务。记录显示只调用了替代工具,没有调用其他工具;本次运行的计数器显示内置搜索调用次数为零;答案中还包含当周发布的真实 URL。请先看工具名称,不要直接跳到答案——较早一次运行同样的步骤看起来通过了,但原因不对,只有工具名称揭示了这一点。
直接从运行自身的输出中读取,而不要去找记录文件。 在验证命令中添加 --verbose --output-format stream-json,运行就会打印上文读取的 init 事件,以及每个 tool_use 块对应的一个 assistant 事件;统计这些块中的名称即可完成这一步,而且它们只能来自本次运行。这些标志会让 stdout 输出 JSONL,因此用户看到的答案来自最后的 result 事件,而不是屏幕——上面的 stderr 重定向不受影响,因为它是单独的流。转而查找记录文件会因三种情况得到自信却错误的答案:-p 子进程会把记录存到根据它自己的工作目录生成的目录,而不是调用方的目录,因此显而易见目录中的最新文件其实是调用方自己的会话;该会话的记录会记下调用方之前调用内置搜索的情况,看起来像是在证明修复无效,尽管修复实际上已经生效;而非 ASCII 提示词会以转义形式存储,因此用原样输入的提示词搜索文件时什么也找不到。有个会话因此浪费了三次尝试,最后还把修复前的运行报告成修复成功的证据。
先授予权限。 本节后面会详细说明,因为相关证据应放在那里,但授权必须在运行此命令之前完成——在未授权的服务上运行检查会得到拒绝提示,看起来像是修复失败,而不是尚未完成授权步骤。
在 Claude Code 中,只需在验证运行时使用两个隔离标志,且两者都已确认出现在 CLI 中。用一条命令运行检查:
claude -p "search the web for news published this week about <topic>, open one of them and quote a line from the page" \
--disallowedTools WebSearch,WebFetch \
--disable-slash-commands < /dev/null 2> /tmp/verify-stderr.log
查询有意同时要求搜索和抓取:如果只检查搜索,那么即使抓取工具从未获得授权,配置也会通过,这正是原始故障中“找到了,但打不开”的那一半。
2> /tmp/verify-stderr.log 和这些标志同样重要。解释授权为何被静默丢弃的那条警告只会出现在这里,不会出现在其他地方,因此之后要读取该文件——文件为空也是通过检查的一部分,并不意味着缺少信息。
< /dev/null 不是装饰。没有它,命令会等待永远不会到来的输入,并打印一条关于 stdin 的警告;在用户旁边观察的人看来,这和你要修复的故障一模一样。
不要用 CLAUDE_CONFIG_DIR 来实现隔离。 凭据存放在配置目录中,因此将它指向一个空目录会得到 Not logged in,这与搜索无关,还会让会话走上错误的排查路径。这两个标志可以隔离运行,同时不影响登录凭据所在的位置。
在使用多个配置文件的机器上,这条消息还有第二个成因,而且表现完全相同。如果凭据存放在配置文件启动器通过 --settings 传入的设置文件中,那么只设置 CLAUDE_CONFIG_DIR 却不传入该标志的运行也会得到 Not logged in,因为没有其他配置会加载令牌。请复现用户自己的启动命令,并保留其中的所有标志,而不是自行拼出一个看似等效的命令。
--disallowedTools 会将内置工具排除在比较之外,并使用客户端的工具名称拼写方式,和拒绝列表相同。--disable-slash-commands 会关闭所有已安装的技能,因此在考虑新工具之前,不会有任何技能抢先处理查询。
确认模型按名称调用了新工具。仅凭结果无法证明这一点:
在考虑调用任何工具之前,另一个已安装的技能就可能接手一个搜索类请求;实测在真实机器上确实发生过这种情况,并让用户在错误路径上花掉了 1 美元的令牌费用。
**在 Codex 上,对应的检查需要运行两条命令,而且看似显而易见的 grep 命令是错的。**运行查询,然后读取它刚刚写入的 rollout:
codex exec --skip-git-repo-check \
"search the web for news published this week about <topic>, open one and quote a line" \
2> /tmp/codex-verify.log
SID=$(grep -o 'session id: [0-9a-f-]*' /tmp/codex-verify.log | tail -1 | awk '{print $3}')
[ -z "$SID" ] && echo "no session id in the log -- stop and read it" && exit 1
ROLLOUT=$(find "${CODEX_HOME:-$HOME/.codex}/sessions" -name "rollout-*$SID.jsonl" | head -1)
[ -z "$ROLLOUT" ] && echo "no rollout file for $SID -- is CODEX_HOME the one codex just wrote to?" && exit 1
python3 -c "
import json, re, sys
names = set()
for line in open(sys.argv[1]):
try: d = json.loads(line)
except Exception: continue
p = d.get('payload') or d
if p.get('type') in ('custom_tool_call', 'function_call'):
names.update(re.findall(r'mcp__[a-z0-9_]+', json.dumps(p)))
print('\n'.join(sorted(names)) if names else 'NO MCP TOOL CALLS IN THIS SESSION')
" "$ROLLOUT"
其中有三点至关重要,而那些看似显而易见的捷径全都行不通。
根据 Codex 在 stderr 中打印的会话 ID 查找文件,而不是用 glob 查找最新的 rollout,这不是小题大做。在普通安装中,累积了 9,021 个会话时,实测 ls -t .../sessions/*/*/*/rollout-*.jsonl 会因 argument list too long 而失败;如果再通过管道传给 head,整行命令就会以退出码 0 结束且不输出任何内容,看起来恰好像是“模型没有调用 MCP”。某个验证步骤因为无法运行而报告成功,正是这个技能要纠正的问题,只不过换了个伪装。find 会逐步遍历,而不会展开参数列表,因此无论会话数量有多少,都不会遇到这种上限。
**这两行 exit 1 都有其必要,因为两种无输出情况的原因不同。**如果没有第一行,codex exec 未能启动时会留下空日志,命令也就解析不到任何内容。如果没有第二行,CODEX_HOME 指向了错误的会话目录树时就匹配不到文件,解析器——包括它自己的 NO MCP TOOL CALLS IN THIS SESSION 回退逻辑——根本不会运行,因此检查会悄无声息地返回 0。这里的每种情况要么打印工具名称,要么打印没有工具调用,要么说明哪一步未能完成。
只读取工具调用记录也同样重要。对文件直接运行 grep mcp__ 也会匹配模型收到的工具列表,因此即使服务器已声明但从未调用,它也会显示为通过。我们用一次真实会话从两个方向进行了校准:上面的解析器会返回实际调用过的两个 Exa 工具,同时不会返回同一文件中仅作为声明出现的 38 个 mcp__codex_apps__* 工具。
而且,不要 grep function_call——这里实测的构建版本通过 exec 接口向模型提供 MCP 工具,因此模型会编写 JavaScript,调用 tools.mcp__exa__web_search_exa(...),而记录的类型是名为 exec 的 custom_tool_call。在运行完全成功的情况下,function_call grep 仍会返回 0。解析器支持这两种形式。
**注册服务并不是最后一步,授予权限才是。**新添加的 MCP 服务默认未经批准,在正常权限下运行的会话中,它的工具会被拒绝,并显示 Claude requested permissions to use mcp__<server>__<tool>, but you haven't granted it yet。随后模型会告诉用户它需要授权,然后停止——站在用户的角度,这和他们要求你修复的搜索故障没有区别。将带前缀的工具名称添加到 permissions.allow,即可授予权限——诊断程序和客户端使用的正是这些名称。交互式运行一次 claude 并接受授权,也会起到相同作用。
**为你依赖的每个工具授予权限,而不只是搜索工具。**如果 web_fetch 也失效了,而你安装的后端会同时替代这两个工具,那么抓取工具也需要单独添加条目;只授予搜索权限会导致运行时能找到页面却无法打开页面,而仅搜索的验证查询不会发现这个问题。对于默认后端,需要添加两个名称:
{ "permissions": { "allow": ["mcp__exa__web_search_exa", "mcp__exa__web_fetch_exa"] } }
如果这台机器上运行着多个 Claude Code 配置配置文件,请将授权添加到这些配置文件同步所依据的那个配置文件中。 会让各个配置文件中的 settings.json 保持一致的配置文件同步器,会将嵌套集合(包括 permissions.allow)视为整体值,并从源配置文件中取值。因此,写入其他配置文件的授权会在下次同步时被丢弃。同一 marketplace 中的 claude-switch-models-setup 就是这样一个同步器,其决策方式可作为参考;这里只特别说明这一点,是因为步骤 4 会将授权写入它恰好会同步的文件。
claude mcp list 不会告诉你某个服务公开了哪些工具,claude mcp get 也不会。references/backends.md 列出了部分条目对应的工具名称,但并非全部——默认项的两个工具名称已列出,其他几个没有,而有一个候选项的工具名称经确认,即使没有账户也无法发现。
因此,请直接查看拒绝信息,其中总会写出工具名称。 先不授予任何权限,运行一次查询,然后从 Claude requested permissions to use mcp__<server>__<tool> 中直接读取名称,或者从 --output-format json 输出中的 permission_denials[].tool_name 读取。这是适用于那些无人记录过信息的后端的唯一方法,而且只需运行一次。
请将授权写入 ~/.claude/settings.json。写入项目自己的 .claude/settings.json 会被静默忽略,而此技能的早期版本曾引导用户将授权写到那里。 Claude Code 只有在用户接受了工作区的信任对话框后,才会接受项目共享设置文件中的 permissions.allow。其他情况下,它会丢弃该条目,并在 stderr 中报告这一情况——绝不会出现在 --output-format json 中,因此读取结构化输出的 Agent 只会看到拒绝信息,却不知道原因:
Ignoring 2 permissions.allow entries from .claude/settings.json: this workspace has
not been trusted. Run Claude Code interactively here once and accept the trust
dialog, or set projects["<dir>"].hasTrustDialogAccepted: true in <config>/.claude.json.
计数表示被丢弃的条目数,因此会随你授予的工具数量变化。请搜索 has not been trusted,而不是搜索整句话。
在同一台机器上通过中继运行了五次:五次的服务注册方式完全相同,唯一变化的是授权的位置;此外还将失败案例作为流捕获了一次,以便读取模式:该会话的 init 事件报告 permissionMode: default,也就是严格模式;同一事件还显示有 24 个工具,且 WebSearch 已不在其中。五次运行的结果如下:
~/.claude/settings.json 可用;.claude/settings.local.json 可用;项目共享的 .claude/settings.json 被拒绝,并在 stderr 中出现警告;在包含 hasTrustDialogAccepted 的工作区中,同一个文件可用;完全不添加任何授权也会像不受信任的共享文件那样失败——这正是关键所在,也是难以在对话中察觉的原因。两种拒绝情况最终都会表现为模型请求用户授予权限。
这就是全部机制。问题在于信任,而不是 project 这个词,所以不要去寻找能解决此问题的作用域标志。
出于同样的原因,请在相同层级注册服务。将授权写入用户文件,同时将服务注册到某个项目中,这样配置可以正常工作;但只要用户在另一个文件夹中打开 Agent,就会停止工作——而这并不是请求“互联网访问”的人所想要的结果。
在你的机器上通过的权限检查,在别人的机器上可能会失败,而且这种情况确实发生过。 两次使用完全相同配置的运行结果不同:一次会话报告 permissionMode: auto,可以自由调用工具;另一次报告 permissionMode: default,同样的调用却遭到拒绝。差异在于凭据——让客户端使用中继令牌,会使会话脱离已登录账户原本享有的任何宽松模式。因此,请使用用户实际采用的配置(包括中继令牌)进行验证,否则你得到的绿灯只对你的机器有效,对他们的机器无效。
claude mcp list 也不是就绪检查。 它报告的是状态,不是能力,而且在两次运行中都显示 Pending approval——无论工具正常响应,还是遭到拒绝。一旦 Agent 把列表当作验证结果,就会重新安装已经安装好的东西,却仍然不知道它能不能用。
curl 能访问的服务仍可能加载失败,而健康检查却会显示 Connected。 整套表现是:已注册,状态行显示 Connected,其自身详情却显示 tools fetch failed — MCP error -32001: Request timed out,模型上下文中没有这些工具,而对同一 URL 直接运行 curl 始终返回 200。一台机器上的原因是每次往返延迟过高,而非无法访问——initialize 用时 7.1s,tools/list 用时 8.1s,冷启动握手耗时 23.2s,超过了客户端允许的时限;用户重启代理后,同一握手耗时 7.3s,工具便成功加载。因此,应测量往返时间,并在同一台机器上测量另一个主机作为对照:一个主机不到一秒就能响应,而 MCP 主机却要花几秒,这说明问题出在网络路径上,而非配置。不要轻易接受看似顺理成章的解释。 那次会话将问题归咎于 MCP 主机被解析到了代理的 fake-IP 范围,后续检查却推翻了这个说法:两个主机都在该范围内,但延迟相差 20 倍。位于该范围内并不是主机能否正常工作的区别所在;区别在于往返时间。
claude mcp get <name> 可以对服务进行健康检查,并报告 Status: ✔ Connected 或实际传输错误,但不要在 Agent 对话中打印未经筛选的输出:stdio Environment 和远程请求头可能包含凭据。若只需要注册范围,请使用字段限定的检查约定。如果必须查看完整的传输详情,请让人工在 Agent 对话之外检查,并且只报告非机密的错误类别,不要报告原始输出。这两种方式都无法告诉你授权是否已生效,也无法告诉你服务提供了哪些工具。只有实时查询能回答就绪情况,而且查询时必须从本次运行自身的 init 和 tool_use 事件中读取工具名称。
要查看的是触发词列表,而不是名称。 阅读已安装技能的 front matter,找出触发词中包含单独搜索词的技能,例如“search”“find”“搜”“查”。已确认有一个全局安装的路由技能正好带有这类列表,因此在那台机器上,每个验证查询都会先被它接管,MCP 工具根本不会得到考虑。对本次检查而言,--disable-slash-commands 可以解决这个问题;日常使用时,用户必须知道还有另一个技能存在。
抽查 URL,并在判定链接造假之前做好校准。 后端可能会针对一个无法解析的 URL 返回看似合理的标题,因此值得对引用给用户的链接运行 curl -sIL。单独出现非 200 状态并不意味着链接是编造的:新闻网站会拒绝无人值守的请求,而在 curl 中返回 401 或 404 的页面通常能在浏览器中正常打开。下结论之前,先用相同命令抓取网站根路径——如果根路径也以相同方式失败,就说明网站拒绝的是客户端,而非链接是伪造的。两次运行这一流程时,都先入为主地认定“后端瞎编的”,结果都错了:一次是网站所有页面都返回 404,另一次是该域名对所有请求都返回 401。
用答案对应的语言搜索。用中文提问中国商品的相关问题,使用中文搜索通常能找到更好的来源。
撤销操作
有人会要求恢复原状——一次没有帮助的试用、一个开始拒绝请求的后端,或一台要交给别人的机器。以下两个命令各自都会打印它所修改的确切文件:
claude mcp remove <name> --scope user
codex mcp remove <name>
这两个命令都已运行。它们都不会改变文件的其他字节;Codex 的命令会保留 config.toml 中所有其他键不变。
陷阱在于,这样只撤销了整个流程的一半,而且撤销错了。 已失效的内置工具仍被拒绝,因此停在这一步的用户将完全无法搜索,而不是恢复到最初的搜索故障状态——按照卸载说明操作,结果反而比开始时更糟。移除替代工具,也意味着要恢复步骤 2 删除的内容:从 permissions.deny 中删除这些工具名称;对于 Codex,还要恢复 web_search,因为 codex mcp remove 不会处理它——之后读取 config.toml 并发现 web_search = "disabled" 仍然存在,就能确认这一点。
不要指望自己记得原来的值。 在步骤 2 中,先读取设置再修改,并将结果写入给用户的消息中,因为这是唯一能在会话结束后保留下来的地方。如果文件中原本没有这个键,记录这一点就更有用:这样回滚时应删除该行,而不是猜测默认值。恢复一个无人记录的值,就只能自行选择;在这里,看似合理但错误的选择会导致一个原本支持托管搜索的后端仍无法使用托管搜索。
完成的标准
diagnose.py报告了哪些工具已失效,并且只移除了这些工具。- 已注册并获授权使用替代工具——模型会调用它,且不会因权限问题遭到拒绝。
- 实时查询返回了真实、最新、可引用的 URL,并展示给用户。
- 本次运行的输出明确指出了负责回答的工具,而且它就是新工具。
- 查询是在用户实际使用的配置下运行的,包括中继凭据,而不是在这台机器上碰巧存在的、更宽松的配置下运行。
- 授权设置位于客户端实际会读取的位置,并且验证运行时,stderr 中没有关于忽略某个条目的提示。
- 步骤 2 修改的每项设置的旧值都有记录,因此可以撤销整个流程,且不会让用户的处境比开始时更糟。
- 所有修改过的文件都经过合并,而非覆盖,并且用户看到了差异。
参考
- references/backends.md — 可选方案、各自的代价,以及当前默认选项。