MCP 與 Extensions:外部工具接入的兩條路
橋接生態標準與原生擴展怎麼分工。核心源碼:packages/mcp/mcp-client/ 與 packages/extensions/。
先玩再講。同一個外部能力「查天氣」,左邊走 MCP 橋,右邊走原生 Extension,兩條路同時接入。看三件事:工具名怎麼生成、伺服器斷綫時模型視角發生什麼、兩條路的能力面差多少。點「播放」自動走完,或用「單步」逐幀看。
先解釋名詞。MCP(Model Context Protocol)是一個開放協議:任何人寫一個工具伺服器,任何支持 MCP 的客戶端都能連上去用它的工具。DSH 的 dsh-mcp-client 插件就是這個協議的客戶端,一個插件實例連一個伺服器,stdio 子進程和 streamable-http 兩種傳輸都支持。連接成功後它做的事很直白:listTools() 拉一遍工具清單,把每個工具用公開名註冊進 ctx.tools,模型從此把它們當原生工具用。
命名是第一個設計點。每個 MCP 工具有兩個名字:原始名只在網綫上出現(tools/call 用它),模型看到的公開名是 mcp__伺服器名__原始名。這個格式與 Claude Code 和 Codex 一致,mcp-client 的 README 自己點了這一句。名字必須滿足 DeepSeek 函式名約定:最長 64 字符、只允許字母數字下劃綫連字符。要是替換字符或截斷改動了名字,就在尾部追加一個 12 位十六進制的 SHA-256 hash,保證兩個不同的工具身份絕不會摺疊成同一個名字。整個函式是 (serverName, rawName) 的純函式:連接順序、重新同步、別的伺服器,都改不了一個工具的名字。
export function publicToolName(serverName: string, rawName: string): string {
const joined = `mcp__${serverName}__${rawName}`
const normalized = joined.replace(INVALID_NAME_CHARS, '_')
if (normalized === joined && normalized.length <= MAX_PUBLIC_NAME_LENGTH) return normalized
const hash = createHash('sha256').update(`${serverName}\0${rawName}`).digest('hex').slice(0, HASH_LENGTH)
return `${normalized.slice(0, MAX_PUBLIC_NAME_LENGTH - HASH_LENGTH - 1)}_${hash}`
}
packages/mcp/mcp-client/src/tools.ts,核對日期 2026-08-13。程式碼塊保留源碼原文。第二個設計點是世代(generation)。伺服器的工具清單會變,變了就要重新同步。同步分兩階段:先把下一世代的全部工具定義拉完建好,任何一步失敗都不碰註冊表,上一世代原樣活着;拉完了才做交換,先註銷舊世代、再註冊新世代。
交換階段的寫法值得講一下。註冊循環裏,每註冊成功一個工具就把它的註銷函式存進一張表。任何一次註冊拋了衝突(意味着有外來註冊霸佔了這台伺服器的命名空間),catch 分支就把這張表裏已註冊的全部註銷,一個工具都不留,然後記一條 error 日誌。註釋把意圖寫得很直白:回滾是為了讓模型看到的要麼是完整的一個世代,要麼什麼都沒有,絕不能是半套。
出處:packages/mcp/mcp-client/src/tools.ts 第 159 至 172 行的註冊與回滾分支,核對日期 2026-08-13。
斷綫重連也建在世代上。stdio 子進程崩了,supervisor 用指數退避重啓它:首次延遲預設 500 毫秒,逐次翻倍,上限 30 秒,一次中斷最多試 10 次(README.zh.md 配置表)。中斷期間最後一個正常世代保持註冊,模型這時調用會失敗,但工具名不會憑空消失;重連成功後重新發現,恢復的世代整體替換舊世代,工具既不重複也不泄漏。serverName 沒變的話,新世代的名字逐字相同,KV cache 前綴都保得住。預算也有講究:連接存活超過 30 秒就重置嘗試預算,所以偶爾崩一次的伺服器可以無限恢復,反覆崩潰循環的伺服器最終會耗盡預算被註銷,不會永遠重啓下去。
最後一個設計點最容易被忽略:這座橋刻意只橋了 MCP 的 tools 能力。MCP 協議裏還有 resources(資源)和 prompts(提示詞模板)兩類能力,DSH 一概沒接。README 的「已知限制與暫緩事項」一節寫得很坦白:
packages/mcp/mcp-client/README.zh.md 第 111 行,核對日期 2026-08-13
邏輯不難還原:harness 內部沒有誰會消費一個外部 resource 或外部 prompt,先造橋墩沒有意義。工具有明確的消費方(agent loop 的工具調用),所以先橋工具。這屬於按需造橋。圖片、音頻這類非文本結果也做了有損投影,在模型上下文裏變成佔位符,二進制載荷不進上下文。
第二條路完全不同。Cordis 是 DSH 的插件框架,整個 harness 就是一棵 Cordis 插件樹。Extensions 子系統讓模型在會話裏現寫一個 Cordis 插件、當場跑起來:寫程式碼前先用 cordis_inspect 查詢當前運行時裏有哪些服務和接口可用,然後 cordis_define 提交源碼,cordis_run 啓動,不要了就 cordis_stop 或 cordis_undefine。這五個工具由 packages/extensions/tool-cordis 註冊。
一個動態插件分兩半。Host 半在 Node 側的 node:vm 沙箱裏跑邏輯,Browser 半在頁面裏渲染 UI,兩半的生命週期由 ctx.dynamicCordisRunner(packages/extensions/cordis-host-runner/src/index.ts 第 124 行起)統一管。帶 Browser 半的啓動要走審批:cordis/request-run 事件把請求送到頁面,用戶點了允許才繼續,還可以勾選一併放行這個插件的後續版本(runHostHalf 的 approveFutureVersions 參數)。每個 Package 版本不可變,改程式碼就是追加新版本。
把兩條路放一起看,它們是正交的,各管一頭。MCP 橋面對的是進程外的現成能力,信任模型是隔離:伺服器崩了、返回垃圾、斷綫,都被世代和錯誤路徑擋在橋外,但它能給模型的只有工具這一種東西。Extension 面對的是模型現場生成的程式碼,跑在自己進程裏,能力面大得多:能加工具、能發事件、能註冊服務、能畫介面,代價是每次運行都在審批和沙箱的看管之下。一個是接外面的電,一個是自己發電。
名字是純函式公開名只由 (serverName, rawName) 決定。兩個伺服器都叫 search 的工具在各自命名空間下共存;連接順序和重新同步永遠不會重命名工具。
世代要麼全有要麼全無拉取失敗不碰註冊表,註冊衝突整代回滾。模型看到的永遠是完整的一套工具,絕不會是半套。斷綫期間舊世代保持註冊,調用會失敗但名字還在。
橋只橋 toolsresources 和 prompts 被有意擱置,理由是 harness 裏沒有它們的消費接口。能力面差距要靠 Extensions 補:工具、事件、服務、介面四樣都能加。
Claude Code:MCP 客戶端的滿配實現
還原源碼裏的 MCP 實現比 DSH 厚得多:六種傳輸方式(stdio、sse、sse-ide、http、ws、sdk,見 restored-src/src/services/mcp/types.ts 第 23 至 26 行)、七個配置來源層級(local、user、project、dynamic、enterprise、claudeai、managed)、OAuth 認證加 15 分鐘緩存。工具命名和 DSH 同形,mcp__server__tool,權限規則能精確到工具級或伺服器級。還有一個 DSH 沒有的防禦:工具描述截斷到 2048 字符,因為觀測到 OpenAPI 自動生成的伺服器往描述裏塞 15 到 60KB 的文檔(services/mcp/client.ts 第 217 至 219 行註釋)。資料來源:claude-code-sourcemap-main/study/chapters/08-mcp.md。
差異在取向。Claude Code 把 MCP 當唯一的官方擴展點做深做全;DSH 把 MCP 橋做薄(只橋 tools),把重能力留給原生 Extensions。前者的擴展跑在進程外,後者多給了一條跑在進程內的路。
Grok Build:插件市場路綫
Grok Build 倉庫裏 MCP 客戶端(crates/codegen/xai-grok-mcp/)與插件市場(crates/codegen/xai-grok-plugin-marketplace/)並存:MCP 負責協議相容,市場負責分發與信任,走的是集中審核的生態路綫。它的 MCP 連接、發現與恢復機制,站內 Grok 專題已經逐行核對過,見 MCP 連接、發現與恢復;市場的發現與信任模型見 Plugin Marketplace 的發現與信任,這裏不重複展開。
三家放一起,光譜就出來了:Grok 靠市場集中管信任,Claude Code 靠七層配置和權限規則分散管信任,DSH 把兩條路拆開,各配各的信任模型:橋外隔離,橋內審批。
推演一次斷綫重連的完整時間綫
weather 伺服器在模型剛拿到工具清單後崩潰,8 秒後被 supervisor 拉起來,這次它的工具清單多了一個 get_alerts。請按時間順序推演:崩潰瞬間註冊表裏面有咩?模型在中斷期間調用 mcp__weather__get_forecast 會得到什麼?重連成功後註冊表經歷咗咩操作,get_forecast 嘅公開名變咗未?再回答:如果兩個唔同嘅伺服器 weather 同 weather2 都暴露 get_forecast,佢哋會唔會衝突,點解?(提示:世代替換、名字是 (serverName, rawName) 的純函式。)
(serverName, rawName) 的純函式,世代替換保證模型手裏的工具集要麼完整要麼為空,永遠沒有中間態。