DeepSeek Harness · 模型與外部接入

MCP 與 Extensions:外部工具接入的兩條路

橋接生態標準與原生擴充怎麼分工。核心原始碼:packages/mcp/mcp-client/packages/extensions/

課程目標讀完你能說清三件事:DSH 接外部能力有兩條路,MCP 橋負責接協議生態裡現成的工具伺服器,Extensions 負責讓模型在 harness 裡現寫現跑外掛;MCP 橋為什麼刻意只橋 tools、工具名怎麼用 hash 防碰撞、伺服器斷線時模型手裡的工具會經歷什麼;以及兩條路的信任模型差在哪,一邊把風險擋在行程外,一邊靠審批和沙箱看管。
互動演示 · 接入方式對照臺

先玩再講。同一個外部能力「查天氣」,左邊走 MCP 橋,右邊走原生 Extension,兩條路同時接入。看三件事:工具名怎麼生成、伺服器斷線時模型視角發生什麼、兩條路的能力面差多少。點「播放」自動走完,或用「單步」逐幀看。

路 A · MCP 橋(外部行程)世代 G1
外部世界
weather server(尚未啟動)
原始工具名 get_forecast(只在網線上出現)
harness 裡的 ctx.tools 註冊表
公開名 mcp__weather__get_forecast
工具 事件 服務 介面
路 B · 原生 Extension(行程內)
模型的動作
呼叫 cordis_define,提交外掛原始碼
等待使用者審批:允許這個外掛執行嗎?
執行中的兩半
Host 半:node:vm 沙箱裡跑邏輯
Browser 半:頁面裡渲染天氣面板
工具 事件 服務 介面
點「播放」,看同一個能力分別從兩條路接進 harness。
邏輯拆解 · 路 A:MCP 橋,把別人的伺服器接進來

先解釋名詞。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) 的純函式:連線順序、重新同步、別的伺服器,都改不了一個工具的名字。

packages/mcp/mcp-client/src/tools.ts第 96 至 102 行
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}`
}
原始碼快照說明:依據本地倉庫 deepseek-harness-master,核對檔案 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 的「已知限制與暫緩事項」一節寫得很坦白:

「只橋接 MCP 的工具能力:資源和提示詞沒有 harness 消費介面,暫緩實現。」 出處:packages/mcp/mcp-client/README.zh.md 第 111 行,核對日期 2026-08-13

邏輯不難還原:harness 內部沒有誰會消費一個外部 resource 或外部 prompt,先造橋墩沒有意義。工具有明確的消費方(agent loop 的工具呼叫),所以先橋工具。這屬於按需造橋。圖片、音訊這類非文字結果也做了有損投影,在模型上下文裡變成佔位符,二進位載荷不進上下文。

邏輯拆解 · 路 B:Extensions,讓模型給自己長外掛

第二條路完全不同。Cordis 是 DSH 的外掛框架,整個 harness 就是一棵 Cordis 外掛樹。Extensions 子系統讓模型在會話裡現寫一個 Cordis 外掛、當場跑起來:寫程式碼前先用 cordis_inspect 查詢當前執行時裡有哪些服務和介面可用,然後 cordis_define 提交原始碼,cordis_run 啟動,不要了就 cordis_stopcordis_undefine。這五個工具由 packages/extensions/tool-cordis 註冊。

一個動態外掛分兩半。Host 半在 Node 側的 node:vm 沙箱裡跑邏輯,Browser 半在頁面裡渲染 UI,兩半的生命週期由 ctx.dynamicCordisRunnerpackages/extensions/cordis-host-runner/src/index.ts 第 124 行起)統一管。帶 Browser 半的啟動要走審批:cordis/request-run 事件把請求送到頁面,使用者點了允許才繼續,還可以勾選一併放行這個外掛的後續版本(runHostHalfapproveFutureVersions 參數)。每個 Package 版本不可變,改程式碼就是追加新版本。

把兩條路放一起看,它們是正交的,各管一頭。MCP 橋面對的是行程外的現成能力,信任模型是隔離:伺服器崩了、返回垃圾、斷線,都被世代和錯誤路徑擋在橋外,但它能給模型的只有工具這一種東西。Extension 面對的是模型現場生成的程式碼,跑在自己行程裡,能力面大得多:能加工具、能發事件、能註冊服務、能畫介面,代價是每次執行都在審批和沙箱的看管之下。一個是接外面的電,一個是自己發電。

名字是純函式

公開名只由 (serverName, rawName) 決定。兩個伺服器都叫 search 的工具在各自名稱空間下共存;連線順序和重新同步永遠不會重新命名工具。

世代要麼全有要麼全無

拉取失敗不碰註冊表,註冊衝突整代回滾。模型看到的永遠是完整的一套工具,絕不會是半套。斷線期間舊世代保持註冊,呼叫會失敗但名字還在。

橋只橋 tools

resources 和 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 把兩條路拆開,各配各的信任模型:橋外隔離,橋內審批。

課堂練習
01

推演一次斷線重連的完整時間線

weather 伺服器在模型剛拿到工具清單後崩潰,8 秒後被 supervisor 拉起來,這次它的工具清單多了一個 get_alerts。請按時間順序推演:崩潰瞬間註冊表裡有什麼?模型在中斷期間呼叫 mcp__weather__get_forecast 會得到什麼?重連成功後註冊表經歷了什麼操作,get_forecast 的公開名變了嗎?再回答:如果兩個不同的伺服器 weatherweather2 都暴露 get_forecast,它們會衝突嗎,為什麼?(提示:世代替換、名字是 (serverName, rawName) 的純函式。)

Takeaway:MCP 橋接的是別人的能力,Extensions 擴充的是自己的執行時,兩條路正交,各配各的信任模型。橋只橋 tools 是刻意的:沒有消費方就不造橋墩。工具名是 (serverName, rawName) 的純函式,世代替換保證模型手裡的工具集要麼完整要麼為空,永遠沒有中間態。