FEATURED · 精选文章

DeepSeek Harness 架构解析:MCP 和 Skill 如何被统一为 Cordis 插件

发布时间 / 2026/8/15 8:50:39
来源 / 创域科博编辑部
栏目 / 资讯中心
DeepSeek Harness 架构解析:MCP 和 Skill 如何被统一为 Cordis 插件 本文是 DeepSeek Harness 系列博文的第三篇。面向已有 Agent 框架开发经验的读者深入源码分析 Harness 如何用统一的插件抽象消解 MCP、Skill 等独立概念。问题协议碎片化带来的工程复杂度主流 Agent 框架的一个结构性问题是每引入一种外部协议就引入一套独立的生命周期管理。以 MCP 为例Claude Code 的处理方式是独立的mcp.json配置文件独立的连接状态机connecting → connected → reconnecting → failed独立的 tool 注册/注销路径独立的错误处理和重试逻辑再加上 Skill/Rules、LLM Adapter、File System 各自一套框架内部变成了多套并行的生命周期管理器行为模式各异。DeepSeek Harness 的解法是让所有能力走同一条注册路径复用同一个生命周期引擎——Cordis。Cordis 的核心机制Effect-scoped Registration在进入 MCP/Skill 的具体实现之前先理解 Cordis 提供的基础设施。Cordis 的注册模型只有一个规则通过ctx发起的任何注册在该 ctx 对应的插件 fiber 卸载时自动撤销。exportfunctionapply(ctx:Context){// 这个 register 调用返回一个 disposer// 但你不需要手动管理它——Cordis 在插件卸载时自动调用ctx.tools.register(defineTool({/* ... */}))// 对于非 Cordis-native 的资源用 ctx.effect() 显式绑定ctx.effect((){constconnectionconnectExternal()return()connection.close()// 卸载时执行})}服务依赖通过inject声明exportconstinject[tools]// Cordis 保证在 apply 执行时 ctx.tools 已就绪// 如果 ctx.tools 的 provider 被热替换本插件自动 dispose 并重新 apply这两个机制组合起来意味着任何外部协议的集成只需要做一件事把协议的资源映射到 ctx 上的注册调用。MCP Bridge 源码分析一个 170 行的applydeepseek-ai/dsh-mcp-client的入口packages/mcp/mcp-client/src/index.ts是一个标准的 Cordis 函数插件exportconstnamemcp-clientexportconstinject[tools]exportasyncfunctionapply(ctx:Context,config:Config):Promisevoid{// 1. 校验 reconnect 配置constreconnectresolveReconnectPolicy(config.reconnect,...)// 2. 抢占 serverName 命名空间effect-scoped卸载自动释放ctx.effect((){names.add(config.serverName)return()voidnames.delete(config.serverName)},mcp-client.serverName)// 3. 启动连接 supervisorconstconnectionstartConnection(ctx,config,reconnect)// 4. 绑定连接到 fiber 生命周期ctx.effect((){return()connection.dispose()},mcp-client.connection)// 5. 阻塞 fiber 激活直到首次 tool 发现完成constoutcomeawaitconnection.readyif(outcome.errorconfig.failOnStartupError)throw...}关键设计决策serverName命名空间是 effect-scoped 的。通过ctx.effect()注册如果两个实例用了相同的serverName后加载的那个在apply阶段就会 throw——不是运行时静默覆盖。而当插件被 HMR 热替换时旧的 namespace reservation 自动释放新实例可以用相同的名字。连接 supervisor 是 Cordis-oblivious 的。startConnection()返回一个普通对象{ ready, dispose() }——它不知道 Cordis 的存在。Cordis 集成只发生在ctx.effect(() () connection.dispose())这一行——把 supervisor 的生命周期绑到 fiber 上。Tool 注册从 MCP 协议到 ctx.toolstools.ts中的syncTools函数执行两阶段切换exportasyncfunctionsyncTools(client:Client,ctx:Context,opts:ToolBridgeOptions,previous:ToolDisposers,// 上一代 tool 的 disposer 集合):PromiseToolDisposers{// Phase 1: Fetch — 分页遍历 MCP 的 tools/list构建 ToolDefinitionconstdefinitionsnewMapstring,ToolDefinition()letcursor:string|undefineddo{constresponseawaitlistToolsUncached(client,cursor)for(consttoolofresponse.tools){constpublicNamepublicToolName(opts.serverName,tool.name)definitions.set(publicName,{name:publicName,description:tool.description??,parameters:tool.inputSchema,output:createOutput(tool.name,supportedOutputSchema(tool.outputSchema)),execute:createExecutor(client,tool.name,...),})}cursorresponse.nextCursor}while(cursor)// Phase 2: Swap — 原子替换 tool 注册for(constdisposeofprevious.values())dispose()constdisposers:ToolDisposersnewMap()try{for(const[publicName,definition]ofdefinitions){disposers.set(publicName,ctx.tools.register(definition))}}catch(error){// 冲突回滚全部注销不留 partial setfor(constdisposeofdisposers.values())dispose()returnnewMap()}returndisposers}这段代码的核心洞察MCP tool 注册和原生 tool 注册走完全相同的ctx.tools.register()路径。这意味着模型看到的 tool schema 格式完全一致tools/pre-execute→tools/execute→tools/post-execute事件管道完全一致权限策略、timeout、compaction 行为完全一致不存在MCP tool 是特殊的这种概念命名规范化确定性函数公共名mcp__serverName__rawName的生成是纯函数exportfunctionpublicToolName(serverName:string,rawName:string):string{constjoinedmcp__${serverName}__${rawName}constnormalizedjoined.replace(INVALID_NAME_CHARS,_)if(normalizedjoinednormalized.lengthMAX_PUBLIC_NAME_LENGTH)returnnormalized// 有损规范化时附加 12 字符 SHA-256 hash 防碰撞consthashcreateHash(sha256).update(${serverName}\0${rawName}).digest(hex).slice(0,HASH_LENGTH)return${normalized.slice(0,MAX_PUBLIC_NAME_LENGTH-HASH_LENGTH-1)}_${hash}}不依赖连接顺序、不依赖其他 server 的存在——相同的(serverName, rawName)输入永远产生相同的公共名。Reconnect Supervisor有限状态机连接管理实现了一个标准的断线重连状态机核心语义connecting → connected → (transport close) → backoff → connecting → ... └→ (budget exhausted) → disabled关键参数initialDelayMs首次重连延迟默认 500ms指数递增maxDelayMs延迟上限默认 30s同时作为稳定连接的判定阈值maxAttempts连续失败次数预算默认 10一个连接存活超过maxDelayMs会重置预算——这意味着偶尔崩溃的 server 可以无限恢复但 crash-loop 会被正确终止。整个 reconnect 逻辑是通用的连接管理——和 MCP 协议本身无关。如果未来出现另一种需要 stdio/HTTP 连接的协议这段逻辑可以被提取复用。Skill 系统源码分析Scoped Layered RegistrySkill 的架构比 MCP bridge 更复杂因为它不只是桥接一个外部协议而是一个完整的Capability Seam。Service DefinitionSkillRegistrydeclaremoduledeepseek-ai/cordis{interfaceContext{skills:SkillRegistry}interfaceEvents{skills/change():void}}exportclassSkillRegistryextendsService{// 分层注册表全局层 每个 scopeagent preset一层privatereadonlylayersnewScopedLayersSkillLayer(...)registerProvider(create:(control:SkillProviderControl)SkillProvider):()voidregister(skill:SkillRegistration):()voidasynclist(options:SkillViewOptions{}):PromiseSkillSummary[]asyncsnapshot(options:SkillViewOptions{}):PromiseSkillCatalogSnapshotasyncget(name:string,options:SkillViewOptions{}):PromiseSkillDefinition|undefined}核心数据结构是ScopedLayersSkillLayer——和ctx.tools使用同一种分层模型。一个注册根据调用者的 scope 落入对应的层宿主行和仓库插件 → 全局层Agent preset 的 standing composition → 该 preset 的 scope 层读取时全局层 查看者 scope 链逐层合并近层同名 entry 直接覆盖远层Provider 接口exportinterfaceSkillProvider{readonlyname:stringreadonlylist:(options:SkillLookupOptions)PromisereadonlySkillCandidate[]|SkillProviderObservationreadonlyget:(candidate:SkillCandidate,options:SkillLookupOptions)PromiseSkillDefinition|undefined}exportinterfaceSkillProviderControl{readonlysignal:AbortSignal// 注册被销毁时 abortreadonlyinvalidate:()void// 通知 registry 清除缓存}Provider 注册时拿到一个SkillProviderControlsignal在精确的这个注册被 dispose 时 abort——provider 可以用它取消进行中的发现工作invalidate()通知 registry 刷新缓存——只在注册仍然存活时生效这个设计让 provider 是无状态可替换的一个文件系统 provider、一个 Redis provider、一个 HTTP registry provider 都实现同一个接口。Consumerdsh-tool-skillConsumer 做的事在每个agent/pre-step注入初始 skill catalogname description 列表每步之前检查 catalog 是否变化变了就注入更新提供skill({ name })tool 给模型主动加载完整 skill bodySkill body 是按需加载的——catalog 只暴露 name 和 description控制 token 开销模型决定是否调用skilltool 获取完整内容。运行时注册除了 provider 发现的 skill任何插件也可以直接注册 runtime skillexportconstinject[skills]exportfunctionapply(ctx:Context){ctx.skills.register({name:my-runtime-skill,description:Dynamic skill contributed by a plugin,source:runtime,content:# Instructions\n...,})// 插件卸载时自动从 registry 移除}和 provider 发现的 skill 在同一个 registry 中合并——优先级由 rank 决定runtime rank 250介于 project 和 user 之间。对比三种能力的注册路径MCP Tool原生 ToolSkill注册目标ctx.tools.register()ctx.tools.register()ctx.skills.register()或registerProvider()生命周期fiber dispose →syncTools回滚fiber dispose → autofiber dispose → auto配置入口cordis.yml 行cordis.yml 行cordis.yml 行隔离机制isolate: { tools: true }isolate: { tools: true }scope chainHMRdispose old → re-apply → re-syncdispose old → re-applydispose old → re-apply → re-discover模型可见性tool schema in prompttool schema in promptcatalog message skilltool三者共享的关键路径插件加载Cordis fiber 激活 →apply()执行注册调用ctx.service.register()→ 返回 disposer依赖等待inject声明 → 框架保证服务就绪卸载清理fiber dispose → 所有 effect 逆序执行热替换配置变更 → dispose old fiber → create new fiber → re-applyCapability Seam 模式以 Shell 为参照MCP 和 Skill 都不是最完整的 Capability Seam 示例。最经典的是 ShellBash 执行dsh-shell (Service Definition) ├── dsh-bash-local (Provider: 本机执行) ├── dsh-bash-e2b (Provider: E2B 沙箱执行) └── dsh-tool-bash (Consumer: 模型可见 tool)在cordis.yml中切换执行环境# 本地执行-name:deepseek-ai/dsh-bash-local# 换成 E2B 沙箱——只改这一行# - name: deepseek-ai/dsh-bash-e2b# config:# sandboxId: ...Consumer (dsh-tool-bash) 完全不变。它只inject: [shell]不知道也不关心后面是本机进程还是远程沙箱。MCP 在这个框架中的位置它是ctx.tools的一种 Provider和defineTool手写的 tool、和 Skill tool consumer 注册的 tool 共享同一个 Service Definition。Skill 的位置ctx.skills是独立的 Service Definition有自己的 Providerfilesystem/runtime/badgeConsumer 通过ctx.tools.register()暴露skilltool 给模型。实际影响开发一个自定义 Skill Provider假设你要从公司内部 Git 仓库发现 skill实现如下importtype{Context}fromdeepseek-ai/cordisimporttype{SkillProvider,SkillProviderControl}fromdeepseek-ai/dsh-skillexportconstnameskill-git-remoteexportconstinject[skills]exportfunctionapply(ctx:Context){ctx.skills.registerProvider((control:SkillProviderControl)({name:git-remote,asynclist(options){// control.signal 在注册被 dispose 时 abortconstresponseawaitfetch(https://internal.git/api/skills,{signal:options.signal??control.signal,})constskillsawaitresponse.json()returnskills.map(s({name:s.name,description:s.description,invocation:{modelInvocable:true,userInvocable:true},source:customasconst,provider:git-remote,rank:350,// 介于 custom dirs(300) 和 user-dsh(400) 之间locator:s.url,// 不透明句柄get() 时回传}))},asyncget(candidate,options){constresponseawaitfetch(candidate.locatorasstring,{signal:options.signal??control.signal,})return{...candidate,content:awaitresponse.text(),}},}))}cordis.yml中挂载-id:skill-git-remotename:./plugins/skill-git-remote.ts完成。不需要修改框架代码不需要了解 skill catalog 的渲染逻辑不需要处理 HMR——registerProvider的 effect-scoped 语义保证了热替换时旧 provider 自动注销、缓存自动失效。总结统一抽象的工程价值Harness 的做法不是弱化MCP 和 Skill——MCP bridge 有完整的 reconnect 状态机和两阶段原子 swapSkill registry 有分层合并和增量 catalog 推送——功能一点不少。区别在于这些复杂性被封装在各自的插件内部对外只暴露统一的ctx.tools.register()或ctx.skills.registerProvider()。框架层面不存在MCP 连接管理器和Skill 发现引擎这种并行的基础设施——有的只是 Cordis fiber 的 effect-scoped lifecycle。工程价值一套 HMR 规则覆盖所有子系统——改cordis.yml配置对应 fiber 热替换一套隔离机制覆盖所有服务——isolate: { tools: true }同时隔离原生 tool 和 MCP tool一套错误传播语义——inject 的服务消失 → 依赖它的 fiber 自动 dispose一套测试基础设施——mockctx.tools就能测所有 tool 注册者不区分来源这是 Cordis 作为 plugin orchestration 框架的核心卖点把连接管理、“发现机制”、注册/注销这些每个子系统都要做的事情收敛到一个统一的 effect 模型中。参考MCP Client 源码Skill 子系统文档能力的三种角色设计Cordis 框架教程服务与依赖DeepSeek Harness 系列文章第一篇DeepSeek Harness 框架介绍第二篇插件开发实战第三篇MCP 和 Skill 如何被统一为 Cordis 插件本文
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻