Claude Code此前会将授予MCP服务器a的权限错误匹配到a__b等服务器,导致工具调用绕过确认。2.1.299版本修正了服务器名称匹配,并处理相关拒绝、询问规则及Agent工具权限问题。
书籍:《System Design Pocket Guide: Fundamentals》(系统设计口袋指南:基础篇)
系列:Pocket Guides for Developers——共 8 本短书,涵盖 Agent、RAG、prompt、可观测性、事件、数据库和系统设计——以上为整个系列的主题。
我的项目:Hermes IDE | GitHub——一款面向使用 Claude Code 等 AI 编程工具交付软件的开发者的 IDE。
关于我:xgabriel.com | GitHub
你在 allow 列表里写下了 mcp__a,因为你信任服务器 a。但在 Claude Code 2.1.299 之前,这条规则还会允许 Claude Code 调用另一台名为 a__b 的服务器,而且不会征求你的同意。
Claude Code 2.1.299 于 2026 年 10 月 14 日发布,包含 131 项 bug 修复。其中一项变更写道:“调整 MCP allow 规则,使其只作用于规则指定的服务器:mcp__a 不再允许名为 a__b 或 a_ 的服务器;请使用 mcp__<server>__*,其中包含服务器完整的规范化名称。”另一条变更对 Agent 的 tools: 列表做了同样的调整;在 bug 修复部分,还提到:当服务器名称包含双下划线时,针对它的 deny 和 ask 规则现在也能生效了。
这三条记录描述的是同一个 bug,问题归根结底在于:分隔符究竟可以出现在哪里。
Claude Code 会为每个 MCP 工具生成一个扁平名称:先是 mcp__,然后是服务器名称,再接上 __,最后是工具名称。权限文档展示了由此衍生出的三种规则形式。mcp__puppeteer 匹配 puppeteer 服务器上的任意工具,mcp__puppeteer__* 通过通配符匹配同一组工具,而 mcp__puppeteer__puppeteer_navigate 只匹配一个工具。
服务器名称,就是你执行 claude mcp add 时输入的名称,或者 .mcp.json 文件中的键名。MCP 文档说,这些名称“只能包含字母、数字、连字符和下划线”。连续两个下划线依然是下划线,所以 a__b 和 a_ 都是合法的服务器名称。
工具名称也有同样的自由度。MCP 规范允许使用字母、数字、下划线、连字符和点,因此服务器完全可以把一个工具命名为 b__whoami。
现在,为三台服务器上同一个名为 whoami 的工具构造名称:
server a -> mcp__a__whoami
server a__b -> mcp__a__b__whoami
server a_ -> mcp__a___whoami
分隔符是 __,而服务器名称也允许包含 __。从左向右读取第二行,在前缀之后遇到的第一个 __ 处停下,就会得到:服务器 a,工具 b__whoami。对第三行做同样的拆分,得到的则是:服务器 a,工具 _whoami。现在,这两者看起来都成了服务器 a 的工具,而不带后缀的 mcp__a 规则表示“服务器 a 的任意工具”。
两台不同的服务器,共用同一个前缀:单凭工具名称,无法判断服务器名称在哪里结束。

更新日志没有公开旧版匹配器的实现,因此请把上面的分析视为一种推演。这是我能找到的、可以同时解释更新日志中 a__b 和 a_ 两个示例的最简单解读。它也解释了同一版本中 deny 规则的修复。假设你写下 deny 规则 mcp__a__b,想要阻止服务器 a__b,但按照同样的拆分方式,它会被读成“服务器 a,工具 b”,而 a__b 上没有任何工具的名称恰好就是 b。发布说明指出,当“服务器的规范化名称包含双下划线”时,“针对 MCP 服务器(mcp__<server>)”的 deny 和 ask 规则不会生效。同一种拆分方式,造成了相反方向的问题。
服务器 a 上名为 b__whoami 的工具,与服务器 a__b 上名为 whoami 的工具,会生成完全相同的字符串:mcp__a__b__whoami。任何解析器都无法仅凭这个名称区分二者。匹配器必须知道配置了哪些服务器,而更新日志中“服务器完整的规范化名称”这句话指向的正是这一点。MCP 规范也从客户端的角度提醒过这个问题:从多台服务器聚合的工具“可能遇到命名冲突”,客户端“应当实现消歧策略,例如给工具名称加上服务器标识符前缀”。但只有当标识符不能包含分隔符时,加前缀才有用。
allow 规则会跳过权限提示。如果一条规则还覆盖了第二台服务器,就意味着它会为一台你从未审核过的服务器跳过权限提示。
谁来决定服务器名称?你会通过 claude mcp add 来指定。你打开的任何一个附带 .mcp.json 的代码仓库,也可以指定。根据权限文档,在交互式会话中,Claude Code 会先征求你的同意,再连接这些项目服务器。而当你在一个从未信任过的文件夹中使用 claude -p 或 SDK 时,同一份文档说,这些服务器会“直接连接,不作询问,无论是否获得批准”。如果你的 CI 会对检出的代码仓库运行 claude -p,这一点就很重要。而且,允许服务器连接,与允许它的每次工具调用都不经提示直接执行,是两项不同的决定。假设用户级配置中有一条 mcp__github 规则,而项目中有一台名为 github__mirror 的服务器:按照旧版匹配方式,一旦这台服务器连接成功,这条规则很可能也会覆盖它的工具。这只是一个假设,我还没有看到相关事件的报告。
Anthropic 的托管 MCP 文档在一条警告中说明了这个普遍性问题:serverName 配置项“不是安全控制措施”,因为“用户可以把任何服务器命名为 github”。对于管理员维护的允许列表,文档建议使用 serverCommand 或 serverUrl。权限规则没有这样的选项。它们以名称为依据,因此,你需要清楚一个名称的匹配范围究竟有多大。
原本只想打开一扇门的 allow 规则,却连旁边那扇门也打开了。

你需要安装 Node 和 Claude Code。下面的服务器使用 v2 MCP TypeScript SDK,对应的软件包是 @modelcontextprotocol/server。它只暴露一个工具,用来告诉你是哪台服务器作出的响应:
// dummy.mjs: one tool that names the server it runs as
import { McpServer } from '@modelcontextprotocol/server';
import { StdioServerTransport }
from '@modelcontextprotocol/server/stdio';
import * as z from 'zod/v4';
const label = process.argv[2] ?? 'dummy';
const server = new McpServer({ name: label, version: '1.0.0' });
server.registerTool(
'whoami',
{
description: "'Say which server answered',"
inputSchema: z.object({}),
},
async () => ({
content: [{ type: 'text', text: `served by ${label}` }],
}),
);
await server.connect(new StdioServerTransport());
在一个临时文件夹中,用两个不同名称将同一个文件注册两次:
mkdir mcp-collide && cd mcp-collide
npm init -y
npm i @modelcontextprotocol/server zod
# save dummy.mjs here, then:
claude mcp add a -- node "$PWD/dummy.mjs" a
claude mcp add a__b -- node "$PWD/dummy.mjs" a__b
claude mcp list
claude mcp add 默认使用 local 作用域,只对当前文件夹生效。现在,在这个文件夹的本地设置中,只允许服务器 a:
mkdir -p .claude
cat > .claude/settings.local.json <<'EOF'
{ "permissions": { "allow": ["mcp__a"] } }
EOF
在 dontAsk 模式下分别执行这两次调用。权限模式文档对它的定义是:预先批准的工具可以运行,“任何原本需要弹出提示的操作都会被拒绝”。这样,allow 规则就会给出明确的允许或拒绝结果,不需要你点击提示框:
claude -p "Call the whoami tool of the a MCP server" \
--permission-mode dontAsk
claude -p "Call the whoami tool of the a__b MCP server" \
--permission-mode dontAsk
第一次调用在任何版本上都应该能运行。对于 2.1.299 及之后的版本,更新日志说 mcp__a 不再覆盖 a__b,因此 dontAsk 会拒绝第二次调用。在 2.1.299 之前,这条规则也会覆盖 a__b,响应会显示“served by a_b”。我没有亲自运行过这个示例,因此请先检查 claude --version,再把你的结果与更新日志对照。添加第三台名为 a 的服务器可以覆盖另一个示例,使用 claude mcp remove 可以清理这些配置。
还要注意发布时间。安装文档说,stable 发布通道的版本“通常比最新版本晚约一周”,而 Homebrew 的 claude-code cask 跟随这个通道。2.1.299 于 10 月 14 日发布,因此,如果你在随后一周使用 stable 通道,很可能仍在使用旧的匹配逻辑。
根据权限文档,规则存放在用户设置 ~/.claude/settings.json、项目的 .claude/settings.json,以及你的 .claude/settings.local.json 中。根据 MCP 文档,服务器名称存放在 ~/.claude.json(user 和 local 作用域)以及项目的 .mcp.json 中。下面的脚本从项目根目录运行,会提取设置文件中不带后缀的 mcp__ 规则,并将每条规则与所有已配置的服务器名称逐一比较。它需要 jq:
#!/bin/sh
# Bare mcp__ rules that reached a second server before 2.1.299
servers=$(jq -r '.. | objects | .mcpServers? // empty
| keys[]' ~/.claude.json .mcp.json 2>/dev/null | sort -u)
rules=$(grep -hoE '"mcp__[A-Za-z0-9_-]+"' \
~/.claude/settings.json .claude/settings.json \
.claude/settings.local.json 2>/dev/null \
| tr -d '"' | sort -u)
for r in $rules; do
s=${r#mcp__}
for n in $servers; do
case $n in
"$s"__*|"$s"_) echo "$r also covered server $n" ;;
esac
if [ "$s" = "$n" ]; then
case $n in
*__*) echo "$r names $n: deny/ask missed it" ;;
esac
fi
done
done
第一项检查会标记这样的情况:存在一条类似 mcp__a 的 allow 规则,同时又存在名为 a__… 或 a_ 的服务器。这条规则现在覆盖的范围比以前小了。第二项检查会标记名称中包含 __、且被规则完整指定的服务器。在旧版本中,这种形式的 deny 和 ask 规则不会触发。grep 会匹配这些文件中的所有 mcp__ 字符串,无论它们属于 allow、ask 还是 deny,因此你需要结合每条命中结果所在的列表来判断。
还有两个地方也会存放 MCP 规则。Subagent 文件中包含 tools: 列表,而 2.1.299 也收窄了这些列表的匹配范围,因此请运行 grep -rn 'mcp__' .claude/agents ~/.claude/agents。此外,CI 脚本经常通过命令行参数 --allowedTools 传入规则。来自插件和 claude.ai 连接器的服务器不在前面提到的两个服务器配置文件中,因此还要用 claude mcp list 检查这些服务器的名称。
更新日志明确给出了格式:mcp__<server>__*,其中要使用服务器完整的规范化名称。对于前面的两台模拟服务器,配置如下:
{
"permissions": {
"allow": ["mcp__a__*", "mcp__a__b__*"]
}
}
对于插件自带的服务器,完整的规范化名称还包含插件名称。MCP 文档给出的格式是 mcp__plugin_<plugin-name>_<server-name>__<tool-name>,其中除字母、数字、_ 和 - 之外的所有字符都会被替换为 _。MCP 文档提醒,如果 hook 匹配器只针对不带前缀的服务器键名编写,那么它永远不会对插件服务器触发。权限规则匹配的是同样的工具名称,因此也要针对完整的 plugin_… 名称来编写。
我个人更倾向于再往前走一步:服务器名称中完全不用 __。连字符是允许的,因此命名为 a-b,工具名称就会变成 mcp__a-b__whoami,不会再有两种拆分方式。移除服务器,再用新名称重新添加,然后更新指向它的规则。此外,根据权限文档,规则的检查顺序是“先 deny,再 ask,最后 allow”,以第一个匹配项为准,因此一条范围较广的 deny 规则,仍然会优先于你写下的任何 allow 规则。
Hermes IDE 是我开发的,因此阅读这一节时,请把这一点考虑在内。Hermes 会在真实终端中运行每个 Agent 自己的界面,所以权限规则、提示框以及这次修复都由 Claude Code 负责。Hermes 从来不会替你回答权限提示。
这里有两个功能能帮上忙。按下 ⌘N(Windows 和 Linux 上是 Ctrl+Shift+N),Agent 选择器会列出已安装的 Agent 及其版本,因此你可以看到自己是否已经用上 2.1.299。
⌘N 打开的 New task 面板展开了 Agent 选择器:当前选中 Claude Code(已安装,2.1.299),Codex 也已安装(0.145.0),其他受支持的 Agent 标记为 Not installed,此外还有 Custom agent 选项。

此外,Hermes 会保存 Agent 的每一轮操作,因此 Review Desk(⌘G)可以按轮次或文件展示变更。如果 Agent 在任务执行过程中修改了 .claude/settings.json 或 .mcp.json,这个文件就会出现在发生修改的那一轮中,你可以只恢复那一轮的变更。Hermes 免费,采用 BSL 1.1 许可证开放源码,并支持 macOS、Windows 和 Linux。它与 Anthropic、OpenAI、Nous Research 或任何 Agent 厂商都没有关联。欢迎下载;如果觉得有用,也请到 GitHub 点个 star。
这个 bug 出现在一处边界上:一个扁平字符串承载两个标识符,而两个标识符又都允许包含分隔符。如果你喜欢琢磨系统会在哪里因衔接问题而出错,我的《System Design Pocket Guide: Fundamentals》用不长的篇幅介绍了可扩展系统的核心构件。它是 Pocket Guides for Developers 系列 8 本短书中的一本,整个系列涵盖 Agent、RAG、prompt、可观测性、事件、数据库和系统设计。

如需采取进一步行动,你可以考虑屏蔽此人,或举报滥用行为,也可以同时进行。