AGENTS.md 写对了但 Claude Code MCP Server 还是越权调用——permission scope + tool_choice 最小权限配置实战 [特殊字符]
上个月我给团队写了一份很详细的 AGENTS.md,把 Claude Code 能用哪些工具、不能碰哪些目录都写得明明白白。结果上线第二天,有同事发现 Claude Code 通过一个 MCP Server 读了生产环境的 .env 文件——AGENTS.md 里明确写了"禁止读取 .env*"。我当时人傻了,花了一整天排查才搞清楚:问题根本不在 AGENTS.md,而是 MCP Server 那层的 permission scope 压根没收窄,加上 tool_choice 没锁定,模型自己"聪明地"扩展了调用链。
这篇文章就是把我踩坑后整理的最小权限配置方案写出来,核心结论先放这儿:AGENTS.md 只是"建议",MCP Server 的 permission scope 才是硬约束;tool_choice 不锁定,模型会自主决定调用你没预期到的工具。
这篇适合谁
- 已经在用 Claude Code + MCP Server 做日常开发,但发现模型"自作主张"调了不该调的工具
- 写了 AGENTS.md 但效果不稳定,时灵时不灵
- 团队多人共享 MCP 配置,需要按角色控制工具权限
- 想搞清楚 tool_choice: auto / required / none 到底怎么影响 MCP 调用行为
整体流程
- 理解问题根因:AGENTS.md 为什么管不住 MCP Server
- 收窄 permission scope:限制 MCP Server 暴露的工具集
- 配置项目级
.claude/settings.json落地最小权限 - 理解 tool_choice 对调用链的影响
- 验证 + 调试:确认配置生效
graph TD
A[AGENTS.md 写了权限规则] -->|模型层面的软约束| B{Claude Code 执行}
B -->|MCP Server 没限制| C[工具全量暴露给模型]
C -->|tool_choice: auto| D[模型自主决定调哪些工具]
D --> E[越权调用发生]
F[收窄 permission scope] -->|硬约束| G[只暴露白名单工具]
G -->|tool_choice: required/指定| H[模型只能用指定工具]
H --> I[越权被阻断]
先说结论
| 配置层 | 约束强度 | 失效场景 |
|---|---|---|
| AGENTS.md | 软约束(模型"尽力遵守") | 复杂推理链中容易被忽略 |
| MCP Server permission scope | 硬约束(工具根本不暴露) | 配置错误时无效 |
| tool_choice 参数 | 硬约束(API 层面限制) | 设为 auto 时等于没设 |
| 两者组合 | 双重保障 | 几乎不会被绕过 |
第一步:理解根因——AGENTS.md 为什么管不住
AGENTS.md 本质是 system prompt 的一部分,它告诉模型"你应该怎么做"。但 MCP Server 在注册时会把自己所有的 tools 列表发给 Claude Code,模型看到了这些工具的 schema,就可能在推理过程中"合理地"决定调用它们。
打个比方:AGENTS.md 说"你不能用剪刀",但剪刀就摆在桌上,模型觉得"用一下也合理"的时候,软约束就失效了。
我实际遇到的情况是:filesystem MCP Server 注册时暴露了 read_file、write_file、list_directory 等全部工具,AGENTS.md 里写的"不要读 .env"只是个建议——模型在需要理解项目配置时,"合理推断"读一下 .env 能帮它更好完成任务。
第二步:收窄 permission scope——只暴露白名单工具
关键操作是在 MCP Server 端限制暴露的工具集。以官方 filesystem Server 为例,添加时指定允许访问的目录:
claude mcp add filesystem --scope project \
-- npx -y @modelcontextprotocol/server-filesystem \
~/projects/my-app/src
这里 ~/projects/my-app/src 就是硬约束——Server 物理上只能访问这个目录,不管模型怎么"想"读 .env,Server 会直接返回权限错误。
但很多自定义 MCP Server 没有这种内置的路径限制。这时候需要在 Server 代码里做工具过滤。以 TypeScript SDK 为例,可以在 tools/list 处理器里过滤白名单:
// tools-filter.ts —— 只暴露白名单工具
import { ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
const ALLOWED_TOOLS = ['search_code', 'run_test'];
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: allTools.filter(t => ALLOWED_TOOLS.includes(t.name))
}));
注意这里用的是 SDK 导出的 ListToolsRequestSchema 对象,而不是字符串 'tools/list'——新版 TypeScript SDK 要求传入 Schema 对象,直接用字符串在当前版本中无效。如果你用的是其他语言的 SDK,写法会有所不同,请参考对应 SDK 文档。
没在白名单里的工具,模型根本看不到它们的 schema,自然也调不了。
第三步:在 .claude/settings.json 里配置项目级权限
项目级配置文件路径是 .claude/settings.json,提交到 Git 后团队共享。这是我目前用的模板:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "./src"]
}
}
}
工具白名单的过滤建议在 MCP Server 代码层面实现(见第二步),这样无论通过什么客户端连接该 Server,过滤规则都能生效,而不依赖特定客户端的配置字段。
如果你的 MCP Server 注册后在 Claude Code 里看不到工具,先跑一下:
claude mcp list
# 确认 Server 状态是 connected
# 如果显示 disconnected,看下一步调试
第四步:理解 tool_choice 对调用链的影响
这一步很多教程没提到。Claude Code 底层调用 Anthropic API 时,默认的 tool_choice 是 auto,意思是模型自己决定用不用工具、用哪个。在复杂任务中,模型可能把多个 MCP Server 的工具串起来形成调用链——这就是"越权"的另一个来源。
tool_choice 是 Anthropic Messages API 的请求参数,在 API 层面可以设置为:
{"type": "auto"}:模型自主决定是否调用工具(默认){"type": "any"}:模型必须调用某个工具,但自主选择哪个{"type": "tool", "name": "search_code"}:强制只调用指定工具
需要说明的是,Claude Code 作为终端 CLI 工具,目前没有直接暴露 tool_choice 的配置入口,上面的参数格式是底层 API 层面的概念,用户无法在 Claude Code 中直接粘贴这段 JSON 来控制行为。
在 Claude Code 的实际使用中,限制模型工具选择范围的有效手段仍然是第二步和第三步的 Server 端过滤:工具根本不暴露给模型,模型就无从选择。AGENTS.md 里的指令可以作为补充软约束,引导模型在有限工具集内的行为偏好。
如果你在自己搭建的应用中直接调用 Anthropic API,则可以通过 tool_choice 参数做精确控制;如果走 OpenRouter 或其他聚合网关,该参数的透传能力取决于网关是否完整支持 Anthropic 原生协议,需要提前确认。
第五步:验证配置是否生效
验证分两步。先确认 MCP Server 连接正常:
# 在 Claude Code 对话框里输入
/mcp
会显示每个 Server 的连接状态和暴露的工具数量。如果工具数量和你在 Server 代码里白名单的数量不符,说明 Server 端过滤没生效,检查第二步的 handler 是否正确注册。
然后故意触发一个越权场景测试。比如让 Claude Code "帮我看看 .env 里的数据库配置",如果配置正确,它应该回复"我没有权限访问该文件"而不是直接读取。
如果遇到这个报错:
MCP server 'filesystem' exited with code 1: Error: EACCES: permission denied
反而说明配置生效了——Server 在文件系统层面拒绝了访问。
不同场景怎么选
个人开发,单项目: 用 --scope local + Server 端工具白名单就够了。配置简单,不用想太多。
团队开发,多人共享配置: 用 --scope project,把 .claude/settings.json 提交到 Git。每个人拉代码后自动生效,不用手动配。记得把含 API Key 的部分放 env 变量,别硬编码。
多项目切换,每个项目权限不同: 每个项目各自维护 .claude/settings.json,用 project scope。不要用 user scope 做全局配置——全局配置意味着所有项目共享同一套权限,这恰恰是越权问题的根源。
需要远程 MCP Server(比如团队共享的代码搜索服务): 用 SSE 传输方式,配合网络层的认证。配置长这样:
{
"mcpServers": {
"team-search": {
"type": "sse",
"url": "http://your-internal-server:3000/sse",
"env": {"API_KEY": "${TEAM_MCP_KEY}"}
}
}
}
SSE 模式下要注意:Server 必须先启动,否则会报 connect ECONNREFUSED。我们团队之前折腾半天就是因为 Server 进程挂了没人发现。
踩坑记录 / 常见问题 FAQ
Q: claude mcp add 执行成功了,但 claude mcp list 看不到这个 Server?
99% 是作用域问题。local 作用域只在当前项目目录下生效,cd 到别的目录就消失了。想全局可用加 --scope user,想团队共享加 --scope project。
Q: Server 在列表里显示 connected,但 Claude Code 对话中看不到任何工具?
在对话框输入 /mcp 看详细状态。如果 tools 数量是 0,大概率是 Server 代码里 capabilities 没正确声明 tools: {}。另外检查 Server 端的白名单过滤逻辑是否误过滤了所有工具(比如白名单数组写成了空数组)。
Q: 配了 Server 端工具白名单,但模型还是调了不在白名单里的工具?
先用 /mcp 确认该 Server 实际暴露的工具列表。如果工具确实不在列表里却被调用,检查项目里是否有多个同名 Server 注册(比如同时存在 local scope 和 user scope 的同名 Server),其中一个没有做过滤。跨 Server 引用工具时,Claude Code 使用 mcp__servername__toolname 格式区分来源,可以通过这个格式判断调用来自哪个 Server 实例。
Q: MCP Server 需要 API Key,怎么处理安全问题?
在配置的 env 字段里引用环境变量,别硬编码到 JSON 里。然后把 .claude/settings.json 里的 Key 部分用 ${ENV_VAR} 占位符,实际值从系统环境变量读取。包含真实 Key 的文件加 .gitignore。
Q: 怎么调试 MCP Server 启动失败?
两步走:① 跑 /mcp 看连接状态和错误信息;② 用 claude --mcp-debug 启动参数重启 Claude Code,可以看到更详细的 MCP 通信日志(官方文档记载的调试方式)。最常见的报错是 spawn npx ENOENT——说明 PATH 里找不到 npx,nvm 用户需要在配置里写 npx 的绝对路径。历史日志也可以在 ~/.claude/logs/ 目录下查找。
小结
折腾了一天得出的教训:AGENTS.md 是给模型看的"规范文档",但真正的权限控制必须在 MCP Server 层面做硬约束。permission scope 决定模型能"看到"哪些工具,tool_choice 在 API 层面决定模型能"选择"哪些工具——对于 Claude Code 用户来说,前者是当前可以直接控制的有效手段,两者结合理解才是最小权限原则的正确实现思路。
这套方案在我们 12 人团队跑了三周,没再出现越权读文件的情况。唯一的代价是每次加新 MCP Server 都要手动维护白名单,有点烦,但比出安全事故强。
更多推荐


所有评论(0)