dsh-mcpserver-manager
IdentifiedDSH (DeepSeek Harness) 插件:在设置 → MCP 管理页面里用一段 JSON 添加 MCP,校验后写入 DSH 官方 MCP 维护配置文件 cordis.patch.yml 并回显列表
dsh-mcpserver-manager
在 DeepSeek Harness 的「设置 → MCP 管理」里管理 MCP 服务器:粘贴一段 JSON → 双重校验 → 写入 DSH 官方的 MCP 维护配置文件,立刻在列表里回显;每个条目还能开关、体检、看它到底注册了哪些工具。
配置写在 cordis.patch.yml(DSH 原生 MCP 配置的同一个文件),不是插件私有的 JSON——卸载插件后配置依然在,与宿主原生 MCP 配置同源。
它能做什么
- 粘贴一段 JSON 就能接入:弹窗里贴配置,输入即时校验,通过后才写盘,写完列表立刻回显并高亮 1.5 秒
- 认 Claude / Cursor 的
{ "mcpServers": { … } }形状:集合键自动当serverName,transportType/type自动映射成transport,sse自动落成streamable-http(DSH 没有 SSE 实现)并给出提示——从别家文档复制过来的配置不用手改 - 写入官方配置文件,且形状是对的:写进当前宿主 profile 的
cordis.patch.yml(例如 desktop 宿主即~/.dsh/profiles/desktop/cordis.patch.yml),按官方补丁格式- insert:插入(cordis.patch.yml是补丁列表,顶层裸条目只会被当成"改已存在条目",找不到目标就被静默跳过——0.4.2 及之前本插件踩的就是这个坑) - 绝不破坏你的配置文件:受管段落 + 文本级行编辑 + 原子写 +
.bak备份 + 失败自动回滚,段外一个字节都不动 - 前后端同一套校验规则:前后端共用同一个
src/validate.js,前端判过的一定能过后端;JSON 语法错能报出「第 N 行第 M 列」 - 秘密脱敏:列表与回显里只出现
env/headers的键名(GITHUB_TOKEN ••••、Authorization ••••),明文永不越过 Host 边界 - 停用/启用是一个开关:用 cordis 原生
disabled: true,开关即时反映状态(比"停用/启用"文字按钮少一次阅读),停用后卡片置灰标「已停用」,再启用完整恢复(含env与!!js表达式) - 已添加的条目可以直接改:卡片 ⋯ → 编辑 打开同一个弹窗,预填这一条的 JSON(
env/headers的值由 Host 置成空串,留空 = 沿用原值,想换就填新的、想删就把整个键删掉),改完不用删了重加;连serverName都能改,改名即替换原条目 - 每个条目都能体检:一键健康检查,结论用一条 toast 说出来——「正常 / 需留意 / 连不上」三档 + 原因 + 耗时,卡片里不留常驻文案(那块长提示只在点下去的那一刻有用,留在列表里只是占三行)。判据从硬到软:已注册的工具数(连不上就一个都不会注册)→
streamable-http走完整握手(initialize→tools/list,带着你配的请求头)→stdio命令能否在 PATH 里解析。握手那一步顺带把服务端自己有几个工具问出来,于是「服务端有 42 个、DSH 注册了 0 个」能直接摆在一条提示里——问题在宿主侧还是服务端侧,一眼就分得清 - 看得到每个服务器有哪些工具:行上显示「共 N 个工具」,点开才拉详情(工具名、参数个数、描述),收起不重查。工具名取自宿主
ctx.tools的mcp__<serverName>__命名空间,只读宿主真实注册结果,不额外连一次服务器 - 看得见别人写的 MCP:用户手写或其它插件写入的条目也会列出来,归到「非本插件管理 · 只读」分组,只展示不改写(健康检查与工具列表照旧可用——那是读操作)
- 有新版会提示,能一键原地更新:启动 15 秒后查一次 GitHub Releases,之后每 6 小时一次;发现新版本在页头给一条横幅(可忽略此版本、可点「更新」),点版本号也能立刻手查。安装走宿主
pluginManager.installBundle('github:…#vX.Y.Z'),tag 钉死,不会装到检查之后新推的提交 - 零运行时依赖:
dependencies为空,构建期只用一个esbuild,客户端 bundle 约 55KB
界面
列表——状态点、serverName、传输方式 chip、命令行,一行「环境 / 请求头 / 超时」,行尾「共 N 个工具」。停用的条目置灰并标「已停用」。
展开工具——点开才去拉详情(工具名、参数个数、描述),收起不重查。工具名取自宿主 ctx.tools 里的 mcp__<serverName>__ 命名空间,所以看到的就是宿主真实注册的结果。
添加——粘贴 JSON,边打字边校验;底部一行直接告诉你这条会写到哪、按什么传输方式:将写入 sentry · stdio。
- 列表页:标题 + 一句副标题(后面跟着版本号,点它手动检查更新);列表为空时右上角什么都不渲染(空态卡片里已经有一个「添加 MCP」,刷新一个空列表也不会带来任何变化)。每张卡片显示状态点、serverName、传输方式 chip、命令行(过长单行截断、悬浮看全量)、环境/请求头 key 与超时
- 刷新有反馈:图标在加载时转起来(至少 450ms,否则本地文件几毫秒读完,动画根本来不及出现)并
aria-busy标记,结束后弹一条「已刷新 · 共 N 个服务器」——不会出现"点了不知道有没有发生"的静默 - 操作结果都有提示(新增 / 编辑 / 删除 / 启停 / 健康检查 / 更新),页面上方居中的 toast,3.2 秒自动消失(贴着上半屏的操作入口出现,视线不用走一整屏),不需要手动关(toast 设了
pointer-events: none,绝不挡下面的按钮) - ⋯ 菜单点外面就关:菜单下面铺一块透明垫层接管点击,不用去点同一个按钮第二下
- 空态:图标 + 主副文案 + 「添加 MCP」按钮——目标文件通常一条 MCP 都没有,空态是默认路径而不是异常
- 添加 / 编辑弹窗:等宽 JSON 编辑器 + 实时错误区 + 一行「将写入
<serverName>·<transport>」解析结果。添加时多一个「填入示例 ▾」(Claude / Cursor 本地命令、Claude / Cursor 远程服务、DSH 原生写法三套);编辑时不渲染示例下拉(点一下就把用户自己的配置整段覆盖了),改成提示「正在编辑 X」与「Authorization的值已隐藏,留空表示沿用原值」 - 遮罩层:跟 DSH 原生 Modal 同一套(
--dsw-alias-bg-mask-1+--dsw-mask-blur),弹窗后面的页面看得见且被虚化,不是一块不透明的黑 - 三态齐备:加载中骨架屏、空态、失败可重试 banner,不会出现「不知道是没数据还是没加载完」
- 主题原生:全部色值走
--dsw-alias-*设计变量并带 fallback,卡片用设置页语言(settings-card-fill+settings-card-stroke),没有任何 JS 明暗判断
安装
方式一:插件页直接添加(推荐)
-
左侧导航点 插件
-
右上角点 添加插件
-
输入框粘贴本仓库地址:
https://github.com/liuleiyu121520/dsh-mcpserver-manager -
点 安装,完成后在 设置 → MCP 管理 里使用
方式二:从 DSH Hub 安装
在 DSH Hub 插件页 点安装,按页面给出的命令执行。
方式三:命令行
dsh plugin --profile <你的 profile> add github:liuleiyu121520/dsh-mcpserver-manager
需要固定版本时加 # 后缀:
dsh plugin --profile <你的 profile> add github:liuleiyu121520/dsh-mcpserver-manager#v0.1.0
卸载
dsh plugin --profile <你的 profile> remove dsh-mcpserver-manager
卸载后本页消失,但已写入的 MCP 条目仍然留在 cordis.patch.yml 里(它们是 DSH 原生配置,不属于插件)。
用法
- 打开 设置 → MCP 管理
- 点 添加 MCP,在编辑器里粘贴一段配置(或点「填入示例 ▾」选一套模板改)
- 错误区为空后点 确定 → 配置写入 → 列表刷新 → 新条目高亮
- 卡片上可直接 停用 / 启用、点心跳图标 健康检查、点「共 N 个工具」展开工具列表
- 点 ⋯ 可 编辑(改完保存,密钥留空即沿用)或 删除(二次确认)
支持哪些格式
Claude Desktop / Claude Code / Cursor / VS Code 的写法(推荐,从别家文档复制过来的就是它):
{
"mcpServers": {
"creis_mcp_server": {
"transportType": "sse",
"url": "https://example.com/mcp/creis_mcp_server",
"headers": { "x-litellm-api-key": "Bearer sk-xxx" },
"disabled": false
}
}
}
粘贴后会自动做这几件事(结果行会显示「将写入 creis_mcp_server · streamable-http」):
| 外来写法 | 落到 DSH 的什么 |
|---|---|
mcpServers / servers 这层外壳 | 拆掉;集合键就是 serverName |
transportType / type | → transport |
sse | → streamable-http(附一条提示:DSH 没有 SSE 实现,连不上就换该服务的 streamable-http 地址) |
http / streamableHttp / streamable_http | → streamable-http |
| 没写传输方式 | 有 command 按 stdio、有 url 按 streamable-http,附一条提示 |
disabled | 保留:true 写成条目级 disabled: true,false 直接不写(默认就是启用) |
一次只接受一个服务器:mcpServers 里多于一个会直接拦下并列出名字——替用户猜要加哪个,猜错就是往配置里塞他不要的东西。
示例配置:stdio(本地命令)
{
"serverName": "github",
"transport": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": { "GITHUB_TOKEN": "!!js process.env.GITHUB_TOKEN" }
}
示例配置:streamable-http(远程服务)
{
"serverName": "remote",
"transport": "streamable-http",
"url": "https://example.com/mcp",
"headers": { "Authorization": "Bearer your-token" }
}
env的值写成!!js process.env.X时会被原样写入(cordis 的私有 tag,运行时才求值),这样 token 不必以明文躺在配置文件里。普通字符串会被加上单引号。
字段说明
| 字段 | 必填 | 说明 |
|---|---|---|
serverName | ✅ | 字母/数字/下划线/连字符,1–32 字符;是工具 namespace,全局唯一 |
transport | ✅ | stdio 或 streamable-http(也接受 transportType / type 与它们的常见别名,见上表) |
command | stdio 必填 | 可执行文件 |
args | — | 字符串数组 |
env | — | 「字符串: 字符串」字典 |
cwd | — | 工作目录 |
url | http 必填 | http:// 或 https:// 且能被解析 |
headers | — | 「字符串: 字符串」字典 |
toolCallTimeoutMs | — | 1–600000 整数,默认 60000 |
maxInstructionBytes | — | 正整数,默认 32768 |
failOnStartupError | — | 布尔,默认 false |
disabled | — | 布尔;cordis 原生条目级停用键 |
不在白名单里的字段不会丢弃,而是给出黄色警告后原样写入——丢弃会静默吃掉未来官方新增的字段。
关于 SSE:
@deepseek-ai/dsh-mcp-client的 schema 只有stdio与streamable-http两个值(z.const),没有 SSE 实现。所以 SSE 配置不会被硬拒,而是映射成streamable-http并明确告知——能跑起来比拒绝更有用。若目标服务只暴露 SSE,请改用它的 streamable-http 地址(很多网关两个路径都提供)。
写入位置与安全
-
写入文件:当前宿主 profile 的 patch 层。插件从宿主拿
profileContext.patchPath(cordisContext上的权威字段,由 dsh 在启动 profile 时提供),所以 desktop 宿主写~/.dsh/profiles/desktop/cordis.patch.yml、web 宿主写~/.dsh/profiles/web/cordis.patch.yml——加完就用得上,不会再出现「加了但工具没出现」。 -
核对写在哪个文件:页面不再铺一条「写入位置」条,改由 Host 启动日志显式报告一行:正常为
mounted profile=desktop file=~/.dsh/profiles/desktop/cordis.patch.yml source=profileContext;用patchFile显式指定路径时 profile 一栏写(未指定,由 patchFile 决定),另有一条使用配置中显式指定的 patchFile:<path>。日志里不会出现与本机 profile 不符的 profile 名(排障时要贴的就是这一行)。同一份路径在GET /api/dsh-mcpserver-manager/servers的file/fileDisplay字段里也有。 -
路径解析优先级(第一个命中的胜出):
顺序 来源 说明 1 插件配置 patchFile(绝对路径)精确指定到文件,测试与高级用户的逃生门;相对路径被忽略 2 宿主 profileContext.patchPath正常运行走这条,宿主说哪个 profile 就是哪个 3 插件配置 dshHome+profiles/<profile>/宿主未提供 profileContext时(测试 / 独立进程)4 $DSH_HOME+profiles/<profile>/同上 5 ~/.dsh+profiles/<profile>/兜底 空字符串或纯空白的
$DSH_HOME/dshHome视为未设置(不会解析成当前工作目录)。 -
受管段:本插件只维护下面这段注释之间的内容,段外内容一个字节都不改:
# >>> dsh-mcpserver-manager:managed:begin (由「设置 → MCP 管理」维护,请勿手改) - id: 'mcp-github' name: '@deepseek-ai/dsh-mcp-client' config: serverName: 'github' transport: 'stdio' command: 'npx' args: - '-y' - '@modelcontextprotocol/server-github' toolCallTimeoutMs: 60000 maxInstructionBytes: 32768 failOnStartupError: false # <<< dsh-mcpserver-manager:managed:end请勿手改这两个锚点之间、以及锚点本身。锚点缺失一半、顺序颠倒或重复出现时,页面会切成只读并提示手工修复;你把条目挪到段外它就会变成「非本插件管理 · 只读」。
-
写入流程:写前落
cordis.patch.yml.bak→ 写cordis.patch.yml.tmp→fsync→rename原子替换;任一步失败自动把.bak拷回并提示「文件已回滚」 -
手工恢复:
# desktop 宿主的配置在 ~/.dsh/profiles/desktop/(宿主日志的 mounted 行里有准确路径) cp ~/.dsh/profiles/desktop/cordis.patch.yml.bak ~/.dsh/profiles/desktop/cordis.patch.yml只有一份
.bak(覆盖式),所以误操作两次后要尽快恢复。 -
并发与竞态:写操作在 Host 进程内串行化,且唯一性判断发生在落盘前的最后一次扫描上;文件在读后写前被外部改动会被 checksum 拦下并返回「配置文件已被外部修改,请刷新后重试」
-
秘密脱敏:
GET /servers与页面永不回显env/headers的明文——无论条目由谁写入(本插件、你手写、或其它工具生成的块/流/引号键/行尾注释等形态),这两个子树的值一律显示为••••,只有键名可见。headers一起打码是必须的:远程 MCP 的 token 按惯例就放在Authorization/x-api-key里(Claude / Cursor 的配置全是这个形状),只打码env等于把另一半密钥下发给浏览器。唯一例外是!!js process.env.TOKEN这类引用表达式:它本身不是明文,会原样显示,宿主在启动 MCP 子进程时才求值。明文只存在于落盘文件里(MCP 服务自己要读),不进浏览器 -
编辑时的密钥往返:
GET /servers/<name>/config下发的是可编辑 JSON,但env/headers的值一律是空串——浏览器只能知道"有哪些键",永远拿不到明文。保存时 Host 用restoreSecretValues()把「空串 + 原值非空字符串」的键还原成原值,所以留空 = 沿用原值;想换密钥就填新的,想删掉就把整个键删掉(键没了就不会被还原)。这份原始配置只挂在item.parsed这个不可枚举属性上,JSON.stringify与对象展开都跳过它——任何"顺手把 item 传出去"的代码都不会把明文带进快照 -
脱敏边界:对「非本插件管理 · 只读」的不可解析条目,页面会逐字展示原文(这样你才能看懂并手工处理它),而脱敏只作用于能识别的
env/headers子树——若你把 token 写在别的位置(比如塞进某段自由文本里),它会随原文显示。请把秘密只放在env或headers里
配置
插件自身的可选项写在 cordis.patch.yml 的条目 config 里:
| 参数 | 默认值 | 说明 |
|---|---|---|
profile | web | 宿主未提供 profileContext 时要维护的 profile(profiles/<profile>/);正常运行以宿主实际 profile 为准 |
backup | true | 写前是否落 .bak |
dshHome | 未设置 | 显式指定 DSH 根目录;优先级高于 $DSH_HOME(低于宿主 profileContext) |
patchFile | 未设置 | 显式指定到文件的绝对路径,连宿主 profileContext 都能压过;相对路径被忽略 |
probeHeaders | true | 健康检查探测时是否带上该条目配置的 headers(含 Authorization)。关掉它,需要鉴权的服务就只能报"需要鉴权" |
路径解析优先级:config.patchFile > 宿主 profileContext.patchPath > config.dshHome > $DSH_HOME > ~/.dsh。空字符串或纯空白的 $DSH_HOME / dshHome 视为未设置(不会解析成当前工作目录)。
想让某台机器强制维护另一个 profile(例如在 desktop 宿主里管理 web 的配置),用 patchFile 精确指到文件;只改 profile 在正常运行(宿主提供了 profileContext)时不生效——这是有意的:那正是「配置写到了宿主不读的文件里」这个 BUG 的成因。
示例:
- id: dsh-mcpserver-manager
name: 'dsh-mcpserver-manager'
config:
profile: web
backup: true
检查更新与原地升级
规则只有四条,每条都对应一个踩过的坑:
| 环节 | 做法 | 为什么 |
|---|---|---|
| 何时查 | 启动 15s 后一次,之后每 6h 一次(ctx.timer);点版本号可随时手查 | 别跟宿主启动抢网络;"自动查失败过一次"的用户得能重试 |
| 当前版本 | src/version.js 的 PLUGIN_VERSION | 升级后运行中的 Host 仍是旧代码,读 package.json 会得出"已是最新"的假结论 |
| 更新横幅 | latest > current 才显示;可「忽略此版本」(只压住这一个) | 忽略是临时动作,不是配置:出了更新的版本照旧提示 |
| 安装 | pluginManager.installBundle('github:liuleiyu121520/dsh-mcpserver-manager#vX.Y.Z', { enabled: true }) | 走和 dsh plugin 同一条 pnpm 路径,profile 的 package.json / lockfile 保持一致;tag 钉死,检查与安装之间新推的提交插不进来 |
安装完成后会立刻重新查一次版本:装成功横幅自己消失;装成功但需要重启时,提示会明确说「重启 DSH 后生效」(application: 'applied' 的热 profile 不用重启)。
相关端点:GET /update(读 Host 缓存的结论,不打网络)、POST /update/check(真的查一次)、POST /update(忽略当前版本)、GET|POST /upgrade(进度 / 开始装)。
完全离线部署时把 UPDATE_ENDPOINT 置空即可关闭(清单里 enabled: false,界面不会显示"检查失败")。
权限与副作用
- 只读写 profile 下的
cordis.patch.yml(及它的.bak/.tmp) - 不读取
env/headers的值用于展示(只解析键名,回显时打码),明文 token 不进日志、不进 HTTP 响应。唯一的例外是健康检查的探测:它会带上你配的请求头(否则对需要鉴权的服务来说,探测永远只能回一句"401 需要鉴权",等于没测)。这条风险被收窄到三处——只发往你自己写下的那个url、不跟随重定向(redirect: 'manual',否则 302 会把密钥带到Location指向的域名)、日志与快照里永不出现值。不想让它发就设probeHeaders: false - 两类外部网络请求,都只在必要时发生:
- 版本检查 —— 只打
api.github.com的 releases/latest,8 秒超时; - 健康检查 —— 只对你自己配置的
streamable-http地址走一次 MCP 握手(仅http/https,其它协议直接拒绝),整个探测共 6 秒超时。点「健康检查」才发,不点不发;已注册工具数 > 0 时连探测都不发(已经有硬证据了)
- 版本检查 —— 只打
- 不重启宿主。写盘后宿主的 HMR 会精确监听到这个文件(
packages/boot/hmr的watchConfig(),chokidar 只盯这一个路径,awaitWriteFinish消除写入中途的抖动),触发reconcileProfile()重新合成 loader 树,条目增删与disabled启停都是即时生效的(见下面的 FAQ)。「更新」按钮例外:它会调pluginManager.installBundle(),那会改 profile 的依赖和模块代码,属于需要重启的情形
工作原理
┌──────── Client 半(esbuild → lib/client.js 单文件 CJS)────────┐
│ launchpad(slots.inject) → McpManager → Toolbar / EmptyState / │
│ ServerList → ServerCard(Switch / 工具列表 / 健康检查按钮) │
│ AddDialog / ConfirmDialog │
│ 共享 ../validate.js(同源校验)· api.js · styles.js(CSS) │
└──────────────────────────┬─────────────────────────────────────┘
HTTP /api/dsh-mcpserver-manager/*
┌──────────────────────────┴──────── Host 半(Node ESM)─────────┐
│ index.js → routes.js(守卫 + 11 端点) │
│ ├→ patch-store.js ─┬→ paths.js(路径解析) │
│ │ ├→ serialize.js(→ YAML) │
│ │ └→ validate.js(同源校验) │
│ ├→ tool-catalog.js(读 ctx.tools 的工具命名空间) │
│ ├→ health.js(MCP 握手探测 / 命令解析) │
│ └→ update.js(release 检查 + installBundle) │
└────────────────────────────────────────────────────────────────┘
为什么工具数是最硬的健康证据:@deepseek-ai/dsh-mcp-client 只有握手成功才会把工具注册进 ctx.tools,公开名固定是 mcp__<serverName>__<rawName>,而 serverName 的约束是 ^[A-Za-z0-9_-]{1,32}$——不含任何会被 INVALID_NAME_CHARS 改写的字符,所以按前缀过滤永远精确。这等于免费拿到一份"当时到底连上没有"的一手证据,不必再为了体检去连一遍服务器。
为什么健康检查要带密钥探测:headers 里通常就是 API key,而早期版本刻意只发 content-type / accept——代价是整个探测对需要鉴权的服务失去意义:它永远只能回一句"401 需要鉴权",用户明明配好了 header。真正该回答的问题是"我配的这个 header 到底生不生效",所以现在照配置发,并把 401 拆成三句(没配 / 配了但被我们关掉了发送 / 配了却被拒)。风险靠三件事收住:只发往用户自己写的那个 url、redirect: 'manual' 不跟随跳转(否则 302 会把密钥带到别的域名)、日志与快照里永不出现值。同理,stdio 只能确认"命令在 PATH 里",所以那种情况判 需留意 而不是 正常——命令存在但一个工具都没注册,正是启动失败的样子。
为什么还要多问一次 tools/list:toolCount === 0 时,"服务端本来没工具"和"DSH 没连上"在界面上长得一模一样,而这两种情况用户要做的操作完全相反。多走两步(notifications/initialized + tools/list)就能把服务端的数字拿回来,于是可以说"服务端有 42 个、DSH 注册了 0 个:重启 DSH"——而不是让用户去改一个本来就正确的配置。这一步只在工具数为 0 时发生,且任何一步失败都只降级成"不知道",不把自己变成新的失败源。
为什么条目必须包在 - insert: 里(0.4.3 的根因修复):cordis.patch.yml 是补丁列表,不是条目列表。宿主合成配置时跑的是 @deepseek-ai/cordis-plugin-include 的 applyEntryPatches():
const { id, insert, name, ...overrides } = patch;
if (insert) { data.push(...insert); continue; } // 建行
const target = entryMap.get(id);
if (!target) { warn('patch: entry %C not found'); continue; } // 改行:找不到就吞掉
而 profile 的补丁是 apply 在空根上的,所以"顶层直接写一条 - id: mcp-x"永远找不到目标:它只换来一条 logger warning,插件根本没被挂载。症状极具迷惑性——配置文件看着完全正确、健康检查 200、服务端 42 个工具,而 DSH 里 0 个,并且一声不响。正确形状是:
- insert:
- id: mcp-creis_mcp_server
name: '@deepseek-ai/dsh-mcp-client'
config: { … }
0.4.3 起新增的条目都按这个形状写;已存在的旧形状条目会在列表页顶部被点名,点「修复」一次性迁移(POST /repair,只碰受管段内、id 正好是 mcp-<serverName> 的条目,走同一套备份 + 原子写)。编辑一条旧形状条目也会顺手把它修正。
为什么不用 YAML 库:目标文件里可能有用户注释和 cordis 私有的 !!js tag,任何「解析 → 改对象 → 全量 dump」都会把 !!js process.env.TOKEN 静默写成字面量——那是不可逆的用户资产损坏。所以本插件自研了一个受管段感知的行编辑器:只在自己的锚点段内做字符串拼接式增删,段外字节不变(这一点由单测逐断言覆盖)。
为什么编辑要"摘掉自己"再判重名:唯一性校验用的是"文件里已占用的名字集合",而编辑时那条条目自己就在集合里。不摘的话,只改一个 url、名字没动,会被自己的旧名字判成 duplicate-server——用户会以为编辑功能坏了。validateOrThrow(raw, scanned, self) 就是干这件事的;改名撞到别人的名字照样拦。
为什么 disabled 没在新配置里就沿用旧状态:编辑弹窗里下发的是 config,而 disabled 是条目级键、不在 config 里。如果按"新配置说了算"处理,改个 url 会顺手把一条停用的服务器悄悄启用——用户根本没碰那个开关。所以规则是:新配置里出现了 disabled 就以用户写的为准,没出现就沿用原状态。
为什么停用用 disabled: true:已核实 cordis 的 @deepseek-ai/cordis-plugin-loader 在启动前会检查 fiber.entry.disabled,原生键可用,比注释搬运更"官方"、也更可逆。
开发
npm install # 只装 esbuild(devDependency),运行时依赖为 0
npm run build # 生成 lib/index.js 与 lib/client.js
npm run check # 构建到临时目录,并与仓库里的 lib/ 逐字节比对(有漂移即非零退出)
npm test # node:test,不需要任何测试框架
lib/ 是构建产物,但必须入库(DSH 从 github:owner/repo 安装插件时不执行构建,仓库里没有 lib/ 就装不起来)。所以 npm run check 不只"能构建成功":它会把本次构建的产物与仓库里现有的 lib/ 逐字节比对,缺文件、多文件、内容不同都会非零退出并点名漂移文件——「改了代码忘了 build,带着陈旧产物发版」正是它拦的情况。
本地试用:
npm run build
ln -s "$(pwd)" ~/.dsh/profiles/web/node_modules/dsh-mcpserver-manager
发版流程
# 1. 改 package.json 的 version,重新构建
npm run build && npm run check && npm test
# 2. 提交推送(lib/ 是构建产物,但**必须随仓库分发**——DSH 从 github:owner/repo
# 安装插件时不执行构建,仓库里没有 lib/ 就装不起来)
# 3. 在 GitHub 上基于该提交创建 Release,tag 用 vX.Y.Z
常见问题
升级后页面提示「页面 v0.4.0,运行中的 Host 是 v0.3.0」 → 重启 DSH。客户端 bundle 每次打开页面都会从磁盘重新取,而 Host 半的模块只在进程启动时加载一次——升级后不重启,页面是新的、Host 是旧的,两边跑同一份校验规则的两个版本。
弹窗里本地没报错,点「确定」却出现一条「服务端未通过:…」 → 同上:Host 还是旧进程,它的校验规则不认识新格式。重启 DSH 后重试;这种提示现在会带一句「(Host 是旧版本,重启 DSH 后重试)」。
列表为空时右上角什么都没有 → 故意的:空列表刷新不会带来任何变化,而「添加 MCP」在空态卡片里已经有一个。加完第一条 MCP 之后刷新与添加就都出现了。
健康检查说「服务端有 42 个工具,但 DSH 注册了 0 个」
→ 先说结论:你的配置和密钥都是对的(探测带着它们走完了 initialize 和 tools/list)。剩下的可能是两件事——
- 条目形状是旧版的(≤0.4.2 写入的条目在顶层,宿主不会加载)。看列表页顶部有没有那条黄色提示;有就点「修复」,不用重启——改完一两秒 HMR 就会重新加载。
- 形状已对但连接没建立。
@deepseek-ai/dsh-mcp-client连接失败时只写日志、不弹窗(failOnStartupError默认false),所以界面上一片安静。去对话里让它真的调一次这个服务器的工具,那次失败会把真正的原因带出来。
工具数是「共 0 个工具」或「工具数未知」,是插件坏了吗
→ 都不是。「共 0 个工具」= 宿主此刻确实一个都没注册(点一下健康检查能分清是服务端没工具、还是 DSH 没连上);「工具数未知」= 宿主没提供 tools 服务(多半是 Host 还是旧版本)。
健康检查说「需要鉴权(HTTP 401)」
→ 分三种,看它具体说的是哪句:「该条目没有配置请求头」 = 这条真没配,补上服务要求的键;「探测按配置没有发送请求头」 = 你配了,但 probeHeaders 被关掉了;「鉴权失败:请求头 X 被拒绝」 = 配了也发了,服务端不认——密钥过期或写错了,这时会点名是哪个键。
改 MCP 配置(新增 / 编辑 / 启停 / 删除)需要重启 DSH 吗
→ 不需要。 宿主的 HMR 精确监听 cordis.patch.yml(packages/boot/hmr/src/watch-config.ts:chokidar 只盯这一个路径,awaitWriteFinish 等写稳再触发,避免读到写了一半的文件),变更后跑 reconcileProfile() 重新合成 loader 树。条目级的 disabled 则由 cordis-plugin-loader 的 Entry.update() 处理——为真时 fiber.dispose(),为假时 init()。所以开关一拨,对应的 MCP 客户端当场停/启,工具也当场注册/注销:刷新一下列表就能看到工具数变化。
前提是 HMR 在跑。
dsh-base里id: hmr那条的disabled: !!js "!ctx.get('profileContext')"——只要有 profile 上下文(desktop / web 宿主)它就是开的;headless 之类没有 profile 的运行才需要手动重启。
→ 需要重启的只有一种:插件自身的模块代码变了(升级本插件、装新包)。dsh-hmr 的 root: [] 表示不监听模块目录,已在跑的模块会继续跑旧代码,它自己会在日志里留一句 changed modules keep running their previous code until a restart。页面顶部那条「Host 是旧版本」横幅说的就是这件事。
点了「更新」但版本号没变
→ 看提示:已更新到 vX,已生效 说明装好了;重启 DSH 后生效 说明这次是冷更新,重启即可。若提示「宿主没有提供插件管理器」,请在「设置 → 插件」里手动装一次。
已知限制
- 编辑已存在的条目是 P1(后端
replaceEntry()已实现并单测,路由与 UI 未暴露) - 搜索与状态筛选是 P1(Toolbar 未渲染占位控件)
- 健康检查是"轻体检":
stdio只能确认命令与工作目录存在(进程由宿主拉起,插件手里没有句柄,探活是另一套进程管理);streamable-http会走完整握手(带上你配的请求头),能判断密钥对错,也能问出服务端工具数,但不会真的调用任何工具——工具能不能跑通,仍要看它被调用的那次结果 - 工具数是"曾经连上"的证据:MCP 客户端断线后工具可能仍留在注册表里,所以「正常」意味着"宿主侧确实装上了这些工具",不严格等于"此刻网络一定通"
- 不认
-rc.1这类预发布后缀:版本比较只做点分数字(发布用稳定 tag) - 只做中文文案
- 只维护一个 profile(默认
web,正常运行以宿主profileContext为准) - 一次只添加一个 MCP:
mcpServers里多于一个会被拦下并列名,不做批量导入 - 不支持 SSE:DSH 的 MCP 客户端只有
stdio/streamable-http,SSE 配置会被映射成streamable-http并提示 - 解析能力边界:只认顶层序列项 + 2 空格键 + 更深的
config子块 + 流样式。块标量、锚点引用、多文档等形状会降级为「无法解析 · 只读」并展示原文,不报错、不写、不猜
许可
MIT — 见 LICENSE。
Comments
Loading…