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) 的纯函数,世代替换保证模型手里的工具集要么完整要么为空,永远没有中间态。