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 工具建構名稱:伺服器 a -> mcp__a__whoami;伺服器 a__b -> mcp__a__b__whoami;伺服器 a_ -> mcp__a___whoami。
分隔符號是 __,而伺服器名稱被允許包含 __。從左邊讀取第二行,並在前綴後的第一個 __ 處停止:您會得到伺服器 a,工具 b__whoami。對第三行執行相同的操作,您會得到伺服器 a,工具 _whoami。
這兩者現在看起來都像是伺服器 a 的工具,而單純的 mcp__a 規則表示「伺服器 a 的任何工具」。兩個不同的伺服器,卻有相同的前綴:單憑工具名稱無法判斷伺服器名稱的結尾。變更日誌並未發布舊的匹配器,因此請將上述段落視為一種重構。這是我能找到的最簡單的解讀方式,它能產生變更日誌中的兩個範例 (a__b 和 a_)。它也解釋了同一版本中拒絕側的修正。
一條旨在阻止伺服器 a__b 的拒絕規則 mcp__a__b,在相同的分割邏輯下會被解讀為「伺服器 a,工具 b」,然而伺服器 a__b 並沒有一個工具真的叫做 b。發布說明指出,當「伺服器的標準化名稱包含雙底線」時,針對「MCP 伺服器 (mcp__<server>)」的拒絕和詢問規則並未生效。
相同的分割邏輯,但方向相反。伺服器 a 帶有一個名為 b__whoami 的工具,以及伺服器 a__b 帶有一個名為 whoami 的工具,兩者會產生相同的字串 mcp__a__b__whoami。單憑名稱,任何解析器都無法區分這兩者。匹配器必須知道哪些伺服器已配置,這正是變更日誌中「伺服器的完整標準化名稱」所指的。
MCP 規範從客戶端角度對此發出警告:從多個伺服器聚合的工具「可能會遇到命名衝突」,客戶端「應實施一種消歧策略,例如在工具名稱前加上伺服器識別符」。
允許規則會跳過權限提示。一條涵蓋到第二個伺服器的規則,會跳過您從未審查過的伺服器的提示。誰來選擇伺服器名稱?您可以使用 claude mcp add 來設定。任何您開啟的、包含 .mcp.json 檔案的儲存庫也會設定伺服器名稱。
在互動式會話中,Claude Code 會根據權限文件,在連接這些專案伺服器之前詢問您。在您從未信任的資料夾中使用 claude -p 或 SDK 時,相同的文件指出它們「無需詢問即可連接,無論是否批准」,這在您的 CI 對於已簽出的儲存庫執行 claude -p 時很重要。
伺服器連接是一個獨立的決定,與讓其每個工具呼叫都無需提示地執行不同。想像一個使用者層級的 mcp__github 規則和一個名為 github__mirror 的專案伺服器:在舊的匹配邏輯下,一旦該伺服器連接,該規則很可能也涵蓋了其工具。這是一個假設情況,我尚未看到任何報告的事件。
Anthropic 的託管 MCP 文件在警告中提出了普遍的觀點:serverName 條目「不是安全控制」,因為「使用者可以呼叫任何伺服器 github」。對於管理員允許清單,他們建議使用 serverCommand 或 serverUrl。權限規則沒有這種選項,它們依賴於名稱,因此了解名稱的涵蓋範圍至關重要。
一條為一扇門寫的允許規則,卻也打開了旁邊的門。使用兩個虛擬伺服器重現此問題。您需要 Node 和 Claude Code。以下伺服器使用 v2 MCP TypeScript SDK,其套件為 @modelcontextprotocol/server,並公開一個工具,用於說明哪個伺服器回應了請求。
```javascript
// 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());
```
在一個暫存資料夾中,以兩個名稱註冊相同的檔案兩次:
```bash
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 預設為本地範圍,僅適用於此資料夾。現在,在該資料夾的本地設定中,只允許伺服器 a:
```bash
mkdir -p .claude
cat > .claude/settings.local.json << 'EOF'
{
"permissions": {
"allow": ["mcp__a"]
}
}
EOF
```
在 dontAsk 模式下執行這兩個呼叫。權限模式文件將其定義為:預先批准的工具會執行,而「任何會觸發提示的動作都會被拒絕」。這將允許規則變成明確的是或否,無需點擊提示:
```bash
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 可以清除。一個時間點的注意事項:設定文件指出穩定發布通道「通常約為一週前」,而 Homebrew 的 claude-code cask 追蹤此通道。
2.1.299 於 10 月 14 日發布,因此如果您在該日期後的一週內使用穩定版,很有可能仍然使用舊的匹配邏輯。
規則存在於您的使用者設定 (~/.claude/settings.json)、專案的 .claude/settings.json 和您的 .claude/settings.local.json 中,所有這些都列在權限文件中。伺服器名稱存在於 ~/.claude.json (使用者和本地範圍) 和專案的 .mcp.json 中,根據 MCP 文件。
這個從專案根目錄執行的腳本,從設定檔中提取裸露的 mcp__ 規則,並將每個規則與每個已配置的伺服器名稱進行比較。它需要 jq:
```bash
#!/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 這樣的允許規則,當存在名為 a__… 或 a_ 的伺服器時:該規則現在涵蓋的範圍比以前小了。第二個檢查會標記一個精確命名包含 __ 的伺服器的規則。在舊版本上,這種形式的拒絕和詢問規則並未觸發。
grep 會在這些檔案中看到每個 mcp__ 字串,無論是允許、詢問還是拒絕,因此請根據其所在的清單來解讀每個匹配項。還有兩個地方儲存 MCP 規則。子代理檔案包含工具清單,2.1.299 也縮小了其範圍,因此請執行 grep -rn 'mcp__' .claude/agents ~/.claude/agents。
CI 腳本也經常透過 --allowedTools 在命令列上傳遞規則。來自外掛程式和 claude.ai 連接器的伺服器不在這兩個檔案中,因此也請檢查 claude mcp list 以獲取這些名稱。
變更日誌明確說明了形式:mcp__<server>__*,其中包含伺服器的完整標準化名稱。對於兩個虛擬伺服器:
```json
{
"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,這無法以兩種方式分割。
移除伺服器,以新名稱重新添加,並更新指向它的規則。根據權限文件,規則會依「拒絕、詢問、然後允許」的順序檢查,以第一個匹配項為準,因此廣泛的拒絕規則仍然會勝過您編寫的任何允許規則。
我開發 Hermes IDE,因此請在閱讀本節時考慮這一點。Hermes 在真實終端機中執行每個代理程式自己的介面,因此權限規則、提示和此修正都屬於 Claude Code。Hermes 絕不會為您回答權限提示。
其中有兩點在此處有所幫助。按下 ⌘N (Windows 和 Linux 上為 Ctrl+Shift+N),代理程式選擇器會列出已安裝的代理程式及其版本,因此您可以查看是否為 2.1.299 版本。
⌘N 新任務表單開啟代理程式選擇器:Claude Code (已安裝,2.1.299) 已選取,Codex 已安裝 (0.145.0),其他支援的代理程式標記為未安裝,加上自訂代理程式。
Hermes 會儲存每個代理程式的回合,因此審查台 (⌘G) 會按回合或按檔案顯示變更。如果代理程式在任務中途編輯了 .claude/settings.json 或 .mcp.json,該檔案會顯示在變更它的回合中,您可以恢復該回合。Hermes 是免費的,在 BSL 1.1 下開源,並可在 macOS、Windows 和 Linux 上執行。
它不隸屬於 Anthropic、OpenAI、Nous Research 或任何代理程式供應商。下載它,如果它有用,請在 GitHub 上給它一個星。
如果這篇文章對您有幫助。這個錯誤存在於一個邊界:一個扁平的字串攜帶兩個識別符,而兩者都允許包含分隔符號。如果您喜歡推論系統在何處出現問題,我的《系統設計袖珍指南:基礎知識》以簡短的篇幅涵蓋了可擴展系統的核心組成部分。它是《開發者袖珍指南》系列中的八本短書之一,內容涵蓋代理程式、RAG、提示詞、可觀察性、事件、資料庫和系統設計。