DeepSeek Harness 结合 MCP:给 dsh 的 Agent 接上 MCP 工具
生命不息,折腾不止。模型能换、插件能写,这一篇把 MCP 这扇门打开——一个 yaml 配置,让 dsh 的 Agent 直接调用 GitHub、数据库和第三方服务的现成工具。
这个系列走到现在,dsh 已经被我们玩明白了大半:装上了、接上了模型、会写自定义插件、能拉多 Agent 团队、还能按场景换模型。但还有一个关键缺口:外部工具协议。
你自己写工具,一次只能写一个;而 MCP(Model Context Protocol)背后是一个已经成型的工具生态——GitHub、文件系统、数据库、浏览器、各家 SaaS,几乎都有现成的 MCP server。把 MCP 接进 dsh,等于一夜之间给 Agent 装上一整个工具库。
一、先说清楚:dsh 怎么「理解」MCP
dsh 本身不原生说 MCP。记住这个设计:既然「一切皆插件」,MCP 支持自然也是一族插件,而不是内置协议。官方给的桥接插件是 @deepseek-ai/dsh-mcp-client,它的职责一句话说清:
一个插件实例 = 一个 MCP server。它去连接外部 MCP server,把对方的工具逐个注册进
ctx.tools,模型就能像调用本地工具一样调用它们。
所以接 MCP 的思路,跟前面「注册本地工具」完全同构——区别只是工具的实现跑在外部进程或远程服务里,dsh 负责桥接。
两个典型方向:
- 正向(本篇重点):dsh 作为 MCP 客户端,去消费外部 MCP server 的工具。
- 反向:把 dsh 自己暴露成 MCP server,给 Cursor、Codex 这类 IDE 用(社区有
dsh-cursor-codex等插件干这事),属于进阶玩法,今天不展开。
二、接一个本地 stdio 的 MCP server
最常见的场景是 stdio:dsh 拉起一个本地子进程当 MCP server,比如 GitHub 官方 server。在 cordis.yml(或你的 patch 覆盖层)里写:
1 | - id: mcp-github |
拆开看:
serverName:工具命名空间,就是后面工具名前缀里的那个段,必须是[A-Za-z0-9_-],长度 1~32,且活着的实例之间要唯一。transport: stdio:走本地子进程。command+args是启动命令,cwd可选(子进程工作目录)。env:额外环境变量,会在「清洗过的宿主环境」之上叠加。!!js process.env.GITHUB_TOKEN的意思是从你启动 dsh 的 shell 环境里取GITHUB_TOKEN传进去。
!!js这种写法只在插件config(和条目disabled)里允许,前面配置篇讲过,这里正好是合法用例。
装好后启动:
1 | pnpm dsh web --patch ./your-cordis.yml |
模型就能直接喊 mcp__github__create_issue 去开 issue 了。
三、接一个远程 streamable-http 的 MCP server
不想跑本地子进程、或者 MCP server 在另一台机器上,就上 streamable-http:
1 | - id: mcp-web |
url 填 server 地址,headers 带鉴权头(比如 Bearer token)。两种 transport 的完整字段见下一节。
四、工具怎么进注册表:两个名字,别搞混
MCP server 连上之后,它的工具就成了 ctx.tools 上的普通工具。但每个工具有两个名字:
- raw name:发给 server 的原名(
tools/call实际用的)。 - public name:模型看到、调用的名字,格式
mcp__<serverName>__<rawName>。
举个例子:GitHub server 的 create_issue,在 dsh 里对外叫 mcp__github__create_issue。
public name 会规范化到 DeepSeek 的函数名契约(≤64 字符、只含 [A-Za-z0-9_-]),重名时追加一段 12 位 hex hash 防止冲突;这个名字是 (serverName, rawName) 的纯函数,跟连接顺序、重连都没关系,所以很稳定。
关键保证:MCP 工具合并进来后,走的跟本地工具是同一条流水线——tools/pre-execute 的放行/拒绝策略、超时、结果改写全都生效。也就是说,你给本地工具设的 allow/deny 名单,对 MCP 工具一视同仁。
五、完整配置字段表
官方 @deepseek-ai/dsh-mcp-client 的字段就这些,收藏这张表够了:
| 字段 | 适用 transport | 必填 | 作用 |
|---|---|---|---|
transport |
两者 | 是 | "stdio" 或 "streamable-http" |
serverName |
两者 | 是 | 工具命名空间,[A-Za-z0-9_-]{1,32},实例间唯一 |
env |
stdio | 否 | 额外环境变量,叠加在清洗后的宿主环境上 |
cwd |
stdio | 否 | 子进程工作目录 |
url |
http | 是 | MCP server 地址 |
headers |
http | 否 | 额外请求头(如鉴权 token) |
toolCallTimeoutMs |
两者 | 否 | 单次 callTool 超时,默认 60000 |
failOnStartupError |
两者 | 否 | 初始连接/同步失败时是否拒绝启动,默认 false |
reconnect.enabled |
两者 | 否 | 掉线自动重连,默认 true |
reconnect.initialDelayMs |
两者 | 否 | 首次重连延迟,连续失败翻倍,默认 500 |
reconnect.maxDelayMs |
两者 | 否 | 退避上限,也是重置退避预算所需的上线时长,默认 30000 |
reconnect.maxAttempts |
两者 | 否 | 每次断连的连续失败上限,默认 10 |
六、几个容易踩的边界
- 只桥接 tools,不桥接 Resources 和 Prompts。MCP 协议里那两样 dsh 现在不接,别指望资源列表和提示词模板进模型上下文。
- 重连语义:断连后 supervisor 按原配置指数退避重连,成功后就地重新 discovery;重连成功前,上一代的工具一直注册着,不会凭空消失。连续失败达到
reconnect.maxAttempts后,工具会被注销、停止重连,直到 HMR 重载或重启宿主。 failOnStartupError默认是false:初始连接失败只会记日志,不会把整个 dsh 干崩。除非你要「MCP 挂了就坚决不启动」,否则不用改它。- 工具名冲突会自动兜底:同名工具会被规范化 + hash 处理,不会因为接了两个 server 就互相覆盖。但为了可读性,
serverName起得语义化一点(github、db-readonly、browser),模型理解起来也轻松。 - 安全性:MCP 工具进的是统一工具流水线,等于外部的工具要受你的 allow/deny 策略约束;反过来,别把 MCP server 绑到公网裸奔,远程 server 记得上鉴权头和 TLS。
七、总结
这一篇补上了 dsh 的最后一块能力拼图:通过 @deepseek-ai/dsh-mcp-client,一个 yaml 配置就接一个 MCP server,外部工具进 ctx.tools、和本地工具同等待遇。至此,模型、插件、多 Agent、配置切换、外部工具协议——dsh 的核心能力我们全部走了一遍。
下一篇就是收官:把这些能力串成一个完整的工作台——定时任务、子代理、工具链全上,看看 dsh 能不能把「一个想法」自动跑成「一份交付物」。
生命不息,折腾不止。下一篇《DeepSeek Harness 实战工作流:搭一个完整的自动化工作台》,把模型、插件、多 Agent、MCP 全串起来干一次真活,下回见。