Tools & Capabilities
dsh-mcp-manager
fishlikewater/dsh-mcp-manager · ★ 2
MCP server manager: adds a Settings → MCP Servers page to DSH to configure MCP servers in the UI — saved means live: the host mounts/unmounts @deepseek-ai/dsh-mcp-client instances and the model gains or loses mcp tools immediately.
README body is shown in Chinese.
安装
dsh plugin --profile web add github:fishlikewater/dsh-mcp-managerGitHub 源码安装:首次需按提示配置 allowBuilds 构建授权后重试
For install and environment setup, plus plugin development tutorials, see DSH community docs ↗
README
在 DeepSeek Harness 的设置页里管理 MCP 服务器。
DSH 原生要在 profile 的 cordis.patch.yml 里手写 @deepseek-ai/dsh-mcp-client 条目。这个插件把它搬到界面上:填表单、保存,实例随之挂载或卸载,不用改文件、不用重启。模型立刻拿到或失去 mcp__<名称>__<工具名> 形式的工具。
支持 DSH 0.1.7-rc.2 到 0.2.x,包含桌面版。0.3.0 起配置不再写进 settings.yaml,而是本插件 Loader 条目的 config.servers。
特性
- 在「设置 → MCP 服务器」里增删改查,
stdio与streamable-http都支持 - 每台服务器单独开关。关闭只是不挂载,配置保留
- 保存即生效。
servers是 volatile 字段,走就地更新,不重载条目 - 密钥不落盘。
env/headers的值可以写成env:NAME/cred:NAME,挂载时才解析 - 宿主侧不依赖
webServer,headless profile 一样能用,只是没有界面
安装
插件要同时出现在 profile 的依赖和 dsh.profile.bundles 里。本包声明了 dsh.bundle,所以 dsh plugin add 会一次做完两件事。
桌面版
桌面版用应用自带的 profile,路径是 $DSH_HOME/profiles/desktop($DSH_HOME 默认是 ~/.dsh,Windows 上是 C:\Users\<你>\.dsh)。它由应用直接加载,不在 CLI 的 profile 列表里,但 dsh plugin --profile desktop 照样认得 —— 那个 --profile 就是指向 $DSH_HOME/profiles/<名字>。
在侧栏的「插件」页里装(推荐)。打开「插件」→「添加插件」,填包名、tgz 的绝对路径或者 Git 地址,点「安装」,装完选「立即启用」。对话框里说的「包名」就是下面命令里 add 后面那一段;安装失败时页面会给出原因和重试。
或者命令行走一遍:
dsh plugin --profile desktop add @fishlikewater/dsh-mcp-manager
dsh plugin --profile desktop add D:\downloads\fishlikewater-dsh-mcp-manager-0.4.0.tgz
两条路做完的是同样两件事:在 profile 目录里跑 pnpm,再按已装状态把声明了 dsh.bundle 的包补进 dsh.profile.bundles。装完都要重启桌面版,bundle 层只在启动时组合。重启后打开「设置 → MCP 服务器」;条目没出现就是被版本门禁拦下了,见「版本兼容」。
PATH 上的 dsh 可能比桌面版自带的核心旧。检查一下:
dsh --version
如果低于桌面版的版本,改用应用目录里那个:
"D:\Program Files\DeepSeek Harness\resources\runtime\cli\bin\dsh.cmd" --version
两个都能装插件(都是往 profile 目录转发 pnpm),但只有和桌面版同版本的那个能拿 --dump-config 校验组合结果,也才能把版本门禁的判定还原成启动时的真实情况。
用 link: 指向本仓库时,Node 会从插件目录往上找 @deepseek-ai/*,可能命中仓库 node_modules 里的旧版本。用 tgz 或 npm 包安装可以避开。
CLI profile
dsh plugin --profile web add @fishlikewater/dsh-mcp-manager
dsh plugin --profile web add ./path/to/dsh-mcp-manager # 或本地目录
dsh --profile web --dump-config # 能看到 mcp-manager 即可
卸载:dsh plugin --profile web remove @fishlikewater/dsh-mcp-manager。
使用
表格里的字段都能在设置页里填,只有 toolCallTimeoutMs 例外:它是条目级的,不是 volatile 字段,只能写进 patch。
| 字段 | 适用 | 说明 |
|---|---|---|
serverName |
全部 | [A-Za-z0-9_-]{1,32},全局唯一,决定工具名前缀 |
enabled |
全部 | 默认 true |
transport |
全部 | stdio 或 streamable-http,默认 stdio |
command |
stdio | 可执行文件,建议绝对路径 |
args |
stdio | 数组,每行一个 |
env |
stdio | 每行 KEY=VALUE,值可以是数字或布尔,也可以写成 env:NAME / cred:NAME |
cwd |
stdio | 可选 |
url |
http | 例如 http://localhost:3000/mcp |
headers |
http | 每行 KEY: VALUE,值同样支持 env:NAME / cred:NAME |
toolCallTimeoutMs |
条目 | 单次工具调用超时,默认 60000 |
serverName 全局唯一是 mcp-client 的要求。重名时本插件只挂第一台,日志里记一条。
改任意一台都会重建全部实例,其余 MCP 连接会重连一次。设置页在可见时每 5 秒对一次 revision,外部改了 patch 会自动反映到界面上。
密钥引用
env 和 headers 的值可以指向别处存着的密钥,配置里只留引用文本:
| 写法 | 取值来源 |
|---|---|
env:NAME |
宿主进程的环境变量 |
cred:NAME |
DSH 的 credentials 服务,即 resolve(NAME) |
引用名限 [A-Za-z_][A-Za-z0-9_]*,带连字符的名字不会被当成引用。解析发生在挂载前,交给 mcp-client 的配置里是真实值,而 patch 文件和设置页里始终只有引用文本。解析不出来时保留字面值,并在宿主日志里记一条;credentials 服务不在时同样如此,不影响其它服务器挂载。
examples/mcp-servers.example.yaml 是一份可以直接抄的配置,stdio 和 streamable-http 各一。
配置写在哪一层
mcp-manager 条目由插件自带的 bundle patch 插入。要保存服务器清单,就在 profile 的 cordis.patch.yml 里对它做 id 定向覆盖:
- id: mcp-manager
config:
servers:
- serverName: demo
transport: stdio
command: node
两个不要:
- 不要用
insert:。bundle 里已经有同名条目,会撞 id。 - 不要写进
$DSH_HOME/cordis.patch.yml。home 级 patch 在 profile patch 之后应用、优先级更高,而 DSH 会拒绝任何会被更外层覆盖的表单写入 —— 清单一旦落在 home 层,设置页就变成只读。
代价是每个 profile 各存一份清单,改的时候要改多处。这是「设置写入落在当前 profile 的 patch」带来的结果。
版本兼容
DSH 在导入插件前会拿运行中的版本核对它的要求。判定规则在 0.2.0 前后换过一次:
- 0.2.0 起:manifest 声明了
engines.dsh,就只看这一个范围,peerDependencies里@deepseek-ai/dsh-*的部分一律忽略 - 0.1.x:没有
engines.dsh,逐个核对peerDependencies里@deepseek-ai/dsh和@deepseek-ai/dsh-*的范围
本包两边都声明,且指向同一个真实支持区间:
"engines": { "dsh": ">=0.1.7-rc.2 <0.3.0" },
"peerDependencies": {
"@deepseek-ai/cordis": "^4.0.2",
"@deepseek-ai/dsh-mcp-client": ">=0.1.0-rc.6 <0.3.0"
}
下界 0.1.7-rc.2 是硬要求:settings 命名空间从这一版才有,再早的 DSH 里插件能加载,设置页却是空的。上界 0.3.0 留给下一个可能改配置契约的大版本。
@deepseek-ai/dsh-mcp-client 单独放宽到 >=0.1.0-rc.6 <0.3.0。它跟着 DSH 在 0.2.0 跳到 0.2.1-rc.2,但 Config 契约没变 —— serverName、transport、command、args、env、cwd、url、headers、failOnStartupError 都还在,所以一条范围能盖住两条线。
别写版本枚举,也别用 >=0.1.0-rc.6 <0.2.0。DSH 用 includePrerelease 匹配,枚举会漏掉没列出的版本,写死的上界则会把 0.2.0-rc.1 这类预发布放行。
被门禁拦下的表现是插件根本不出现在组合里:桌面版启动日志把它列进 skippedBundles,dsh --profile <名字> --dump-config 的输出里也找不到 mcp-manager 这一行。可以在 profiles/<profile>/compatibility.json 里为「包名@版本 × 运行时版本」写一条豁免,由插件管理器的授权流程管理;豁免按精确版本记录,插件或 DSH 升级都不继承。
工作原理
设置页 client.js(注册在 settings.section 插槽)
│ ctx.remote.settings.describe() / mutate(ns, ops, expectedRevision)
▼
mcp-manager 条目的 config.servers ←→ profiles/<profile>/cordis.patch.yml
│ servers 是 volatile 字段,就地更新引用并发 loader/volatile-update
▼
宿主半 index.js,逐台 ctx.plugin(mcp-client),工具注册到 ctx.tools
命名空间就是 Loader 条目 id。 DSH 0.1.7 起用户设置统一成「Loader 条目的 config」,$DSH_HOME/settings.yaml 只剩一次性导入的作用,插件自注册的命名空间也不再保证对浏览器可见。所以这里既没有 ctx.settings.register(),也没有自建的 HTTP 路由。
servers 必须是 volatile 字段。 settings 服务只把 schema 里带 volatile 字段的活跃条目投影成命名空间:describe() 对每个条目求一次 volatileForm(schema),返回 undefined 就整条跳过。没有 volatile 字段的插件,命名空间永远不存在。写入也只接受落在 volatile 子树内的路径。
volatile 只能标在固定对象路径上,不能嵌套,所以要整列标在 servers 上。标进 ServerSchema 内部会被 schemastery 拒绝:
volatile fields require a fixed object path without an enclosing volatile field
volatile 变更不会重载条目。 Loader 只把新快照提交进运行中的引用,然后发 loader/volatile-update,apply 不重跑。宿主半自己记录已挂载的实例,收到事件先 dispose() 再按新值重新挂载。值是 cosmokit 的 Volatile<T>,读取要走 .get()。
值按 any 接收。 args、env、headers 的值在挂载前统一字符串化。早期版本把 env 的值约束成字符串,写一个 PORT: 8080 就会让整份 config 校验失败,所有服务器一起挂不上。现在单台写错只影响那一台,日志里记一条 [dsh-mcp-manager]。
mcp-client 惰性加载。 首次需要挂载时才解析模块:先试裸导入,profile 进程里的 runtime 解析拦截会把插件的裸导入路由到宿主自带的那份包;失败则用 ctx.pluginPackages.packageOf() 定位包目录,再按绝对路径导入。两条路都落在宿主那一份,避免出现第二个实例抢占 serverName。配置为空时完全不加载。
@deepseek-ai/schemastery 是普通依赖而不是 peer,静态导入,保证 schema 一定能加载。最低 ^3.18.4:.volatile() 在 3.18.1 还不存在,装到旧版本会在加载时抛 Schema.array(...).default(...).volatile is not a function。
故障排查
| 现象 | 排查方向 |
|---|---|
| 设置里没有「MCP 服务器」 | 插件不在该 profile 的 bundles 里,或被版本门禁拒绝。看侧栏「插件」页和启动时的 stderr |
| 「没有找到 mcp-manager 配置命名空间」 | 条目 id 不是 mcp-manager、条目没被加载,或宿主半的 Config 里没有 volatile 字段(0.4.0 之前) |
| 保存被拒绝,页面像只读 | config.servers 被更外层的 patch 覆盖,通常是写在了 $DSH_HOME/cordis.patch.yml |
| 「配置已被其他写入方改动」 | revision 冲突。页面每 5 秒会自己刷新,重试即可 |
| 换了新版但行为没变 | index.js 和 client.js 的改动要重启 DSH;config 和 patch 的改动会热生效 |
工具列表里没有 mcp__* |
该台 enabled: false、config 校验失败,或 MCP 服务器自己连不上。在宿主日志里搜 [dsh-mcp-manager] 和 mcp-client( |
提示无法加载 @deepseek-ai/dsh-mcp-client |
该 profile 的 runtime 解析拦截没生效,或这个包不在运行时里 |
| 保存后 patch 文件被重排、注释消失 | 设置页按 schema 重写文件,属正常行为 |
| 服务器连不上但配置看着没错 | 值可能写成了 env:NAME / cred:NAME,而对应的环境变量或凭据没配。宿主日志里搜「引用保留字面值」 |
| 安装命令跑不通 | 手工兜底:在 profile 的 package.json 里加依赖、把包名加进 dsh.profile.bundles,再 pnpm install |
开发
package.json dsh.bundle.patch 与 dsh.client 双 manifest
cordis.patch.yml bundle 层,插入 mcp-manager 条目
index.js 宿主半:Config schema,逐台挂载 mcp-client
client.js 客户端 bundle,手写维护,不需要打包器
examples/ 配置示例
scripts/check-pack.mjs npm pack 白名单校验
test/ 自测与迁移校验
npm test # 宿主半 + 客户端半自测
npm run check:pack # 发布内容白名单
python test/verify-profile-patch.py # 校验两个 profile 的 patch,要能读到 $DSH_HOME
client.js 是给浏览器模块加载器用的产物,格式是 window.__ModuleLoader__.load({ id, factory }),id 必须是完整包名 @fishlikewater/dsh-mcp-manager:client-modules 按包名匹配 Loader 条目。
改完代码必须重启 DSH。Node 的 ESM 模块缓存不随文件变化失效,浏览器侧的 bundle 同理。
发布
npm publish
prepublishOnly 会先跑 npm test 和 npm run check:pack:自测不过,或者 files 白名单漏了 examples/、混进了 test/,发布都会中止。
包名用 scope 版 @fishlikewater/dsh-mcp-manager,无 scope 的 dsh-mcp-manager 已被他人占用。发布者账号必须是 fishlikewater。publishConfig.access: public 已经配好,不用带 --access public。
改包名要同步三处:cordis.patch.yml 的 name、client.js 的 id,以及各 profile 的 package.json。
License
MIT