生命不息,折腾不止。模型能换、插件能写,这一篇把 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
2
3
4
5
6
7
8
9
10
11
- id: mcp-github
name: '@deepseek-ai/dsh-mcp-client'
config:
serverName: github
transport: stdio
command: npx
args:
- '-y'
- '@modelcontextprotocol/server-github'
env:
GITHUB_TOKEN: !!js process.env.GITHUB_TOKEN

拆开看:

  • 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
2
3
4
5
6
7
8
- id: mcp-web
name: '@deepseek-ai/dsh-mcp-client'
config:
serverName: web
transport: streamable-http
url: http://localhost:3000/mcp
headers:
Authorization: !!js '`Bearer ${process.env.MCP_TOKEN}`'

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

六、几个容易踩的边界

  1. 只桥接 tools,不桥接 Resources 和 Prompts。MCP 协议里那两样 dsh 现在不接,别指望资源列表和提示词模板进模型上下文。
  2. 重连语义:断连后 supervisor 按原配置指数退避重连,成功后就地重新 discovery;重连成功前,上一代的工具一直注册着,不会凭空消失。连续失败达到 reconnect.maxAttempts 后,工具会被注销、停止重连,直到 HMR 重载或重启宿主。
  3. failOnStartupError 默认是 false:初始连接失败只会记日志,不会把整个 dsh 干崩。除非你要「MCP 挂了就坚决不启动」,否则不用改它。
  4. 工具名冲突会自动兜底:同名工具会被规范化 + hash 处理,不会因为接了两个 server 就互相覆盖。但为了可读性,serverName 起得语义化一点(githubdb-readonlybrowser),模型理解起来也轻松。
  5. 安全性: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 全串起来干一次真活,下回见。