Hermes MCP include 空列表:显式全禁现在在所有界面都算数

你给团队的 Hermes Agent 接了一个文件系统的 MCP 服务器,为了安全,你把它默认能用的工具全部取消勾选,打算等以后真的需要时再逐个放开。配置里确实写下了空列表——tools.include: [],你也以为这事已经生效。直到某天你顺手跑了一下 hermes mcp list,才看见那个服务器旁边写着一个 all:agent 依然能读写那些目录。原因很简单:这个“空列表”在好几个界面上被读成了“这个键没写”,也就是“不过滤”,等于全部放开。
先花一句话说清背景:MCP(Model Context Protocol)是让 Hermes 调用外部服务器所提供工具的协议;而 tools.include 就是某个 MCP 服务器的工具白名单——只有写进这个列表的工具才允许被调用。
先搞清三个状态:没写、写了一部分、写了空列表
tools 这一块其实只有三种状态,但它们的语义差别很大:
- 没写:整块
tools不出现,或者只写了别的字段而没有include。这是“不过滤”,该服务器暴露的工具全部启用。 - 写了一部分:
include: [read_file, list_directory]。这是真正的白名单,只有列出来的工具能用。 - 写了空列表:
include: []。这是显式全禁——一个工具都不注册。
麻烦点在于,后两种在代码里极易被折叠成同一个分支。在 Python 里空列表是假值(falsy),所以 cfg.get("include") or [] 这种写法一旦用上,“用户写了空列表”和“用户什么都没写”就再也区分不开了——而后者恰好是用户意图的反面。
值得注意的是,运行时那一侧一直是对的。tools/mcp_tool_registration.py 在遇到 include: [] 时什么都不注册,一个工具也不会暴露给 agent。也就是说,你的封锁在事实上是生效的,只是界面在骗你。
空列表在一半界面上失踪了
既然运行时没问题,那问题就出在“读取方”。有多个界面在读取配置时,用 or [] 或简单的真值判断处理 include,于是空列表和“键缺失”被当成了同一件事,统一显示成“全部启用”。PR #109188(2026-09-12 合入 main)修的就是这几个分叉的读取方,标题是 “fix(mcp): include: [] no longer reopens as “all tools enabled” in CLI, catalog and desktop”,对应 issue #12865,改动 +107 / −31,涉及 8 个文件。
用一张表看得更清楚(表中的“修前 / 修后”指 PR #109188 之前与之后):
| 检查点 | 修复前 | 修复后 |
|---|---|---|
hermes mcp list |
显示 all |
显示 0 selected |
hermes tools 列表行 |
all tools enabled |
[include only: (none)] |
hermes tools 选择器 |
每个工具都已预勾选,按确认会把它们全部重新启用 | 没有预勾选,按确认保持 include: [] |
运行时 _make_tool_filter("demo")("a") |
False | False(不变) |
具体到代码,四段改动值得记住:
hermes_cli/mcp_config.py的_tool_filters:只要键里装的是列表就返回那个列表(包括空列表);只有键缺失、或者值不是列表时才返回None。列表输出与 configure 选择器则改成用is not None判断,而不是看真假。hermes_cli/tools_config_mcp.py:hermes tools选择器的预勾选状态,以及列表行显示[include only: (none)]。hermes_cli/mcp_catalog.py:交互式重装时的预检查会尊重先前的[],而不是退回清单(catalog)里的默认值。apps/desktop/src/lib/mcp-tool-filter.ts:isToolEnabled把“存在 include”当作白名单——这一点在下一节细说。
源码注释把这件事说得很直白:
一个显式的
include: []是真正的(全禁)白名单,运行时不会注册任何工具,所以它不能在这里被折叠成“没有过滤”。
另外要交代的是,运行时那一侧其实更早就在另一个提交(fb1ec36a4b86)里修好了;本 PR 收拾的是仍然和它分叉的那几个读取方。所以这不是“新增一个功能”,而是让所有界面重新对齐运行时早就确立的语义。
桌面端最隐形的那个坑:删掉键就变成全开
桌面端 MCP 面板里,apps/desktop/src/lib/mcp-tool-filter.ts 的 isToolEnabled 现在把“存在 include”当作白名单来理解。这个改动看着不起眼,但它顺手堵住了一个比 CLI 更隐蔽的坑:当你把最后一个 include 项关掉时,代码会保留 include: [],而不是删掉整个键。
为什么这点重要?因为在配置文件层面上,“空列表”和“键不存在”是完全不同的两件事:
- 保留
include: []= 显式全禁,一个工具都不注册。 - 删掉
include键 = 不过滤,服务器静默变成全开。
在 UI 上,两者的表现都只是“所有勾选框都没勾上”,肉眼完全分不出来。删键这个操作看起来像是在做清洁,实际上是把封锁悄悄拆掉了。所以如果你的团队里有脚本或习惯会“顺手删掉空字段”,那正是最需要提防的写法。
顺带交代一下来源:桌面那一段提交是从 #52874(@Bartok9,本身是对 #13096 @dingn42 的抢救)cherry-pick 过来的。
怎么正确地把一个 MCP 服务器锁死
推荐的做法是把意图显式写进 YAML,而不是依赖界面的默认值:
mcp_servers:
demo:
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "/srv/data"]
tools:
include: [] # 显式全禁:一个工具也不注册
demo2: {} # 没写 tools 键 = 不过滤,全部启用
写完之后,按这个顺序验证一遍:
- 运行
hermes mcp list,demo旁边应该显示0 selected,而不是all。 - 运行
hermes tools,对应那行应该显示[include only: (none)];进到选择器里,不应该有任何工具处于预勾选状态,直接确认也不会把它们打开。 - 之后需要放开某个工具时,把它的名字加进
include列表,而不是把整个键删掉。
如果你是从旧版本升上来的,建议顺便核对一遍现有配置:只要某台服务器属于“本来想全禁、后来一直没人动过”的状态,就检查它到底是写了 include: [],还是干脆没写 include——两者看起来几乎一样,效果却完全相反。
进一步阅读:想了解 MCP 配置里上下文变量怎么用,可以看 Hermes MCP config context variables;想系统理解 MCP 工具面的安全边界,可以看 Hermes MCP catalog security guide;如果你还在挑该给 agent 接哪些服务器,Hermes official remote MCP catalog 是一份不错的起点;还没装上 Hermes 的话,安装指南 讲清了依赖和第一步。
小结
tools.include: []从来就不是“没配置”,它是显式全禁。运行时一直这么理解,只是有多个界面曾经读错。- PR #109188(2026-09-12 合入
main)让hermes mcp list、hermes mcp configure、hermes tools、目录重装和桌面端 MCP 面板都与运行时对齐:显式空列表 = 0 个已选。 - 最容易踩的坑是“删掉键”:
include: []和“没有include”在界面上长得一样,语义却正好相反。 - 验证成本很低:
hermes mcp list显示0 selected,就说明你的封锁是站得住的。