上个月我给团队写了一份很详细的 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 调用行为

整体流程

  1. 理解问题根因:AGENTS.md 为什么管不住 MCP Server
  2. 收窄 permission scope:限制 MCP Server 暴露的工具集
  3. 配置项目级 .claude/settings.json 落地最小权限
  4. 理解 tool_choice 对调用链的影响
  5. 验证 + 调试:确认配置生效
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_filewrite_filelist_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 都要手动维护白名单,有点烦,但比出安全事故强。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐