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) 的純函式,世代替換保證模型手裏的工具集要麼完整要麼為空,永遠沒有中間態。