DSH Plugins Marketplace

DSH Plugins

Plugins

/

dsh-mcpserver-manager

d

dsh-mcpserver-manager

Identified

DSH (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

界面

MCP 管理列表:三张卡片分别是 deepwiki(streamable-http、4 个工具)、github(stdio、26 个工具)与已停用的 postgres;每张卡片右侧是健康检查图标、启停开关与⋯菜单

列表——状态点、serverName、传输方式 chip、命令行,一行「环境 / 请求头 / 超时」,行尾「共 N 个工具」。停用的条目置灰并标「已停用」。

展开 deepwiki 后列出 4 个工具:ask_question、read_wiki_structure、read_wiki_contents、search,各带参数个数与描述

展开工具——点开才去拉详情(工具名、参数个数、描述),收起不重查。工具名取自宿主 ctx.tools 里的 mcp__<serverName>__ 命名空间,所以看到的就是宿主真实注册的结果。

添加 MCP 服务器弹窗:等宽 JSON 编辑器里是 mcpServers 配置,下方一条黄色提示说未写传输方式、按 command 推断为 stdio,再下面一行说将写入 sentry · stdio

添加——粘贴 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 明暗判断

安装

方式一:插件页直接添加(推荐)

  1. 左侧导航点 插件

  2. 右上角点 添加插件

  3. 输入框粘贴本仓库地址:

    https://github.com/liuleiyu121520/dsh-mcpserver-manager
    
  4. 点 安装,完成后在 设置 → 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 原生配置,不属于插件)。

用法

  1. 打开 设置 → MCP 管理
  2. 点 添加 MCP,在编辑器里粘贴一段配置(或点「填入示例 ▾」选一套模板改)
  3. 错误区为空后点 确定 → 配置写入 → 列表刷新 → 新条目高亮
  4. 卡片上可直接 停用 / 启用、点心跳图标 健康检查、点「共 N 个工具」展开工具列表
  5. 点 ⋯ 可 编辑(改完保存,密钥留空即沿用)或 删除(二次确认)

支持哪些格式

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 与它们的常见别名,见上表)
commandstdio 必填可执行文件
args—字符串数组
env—「字符串: 字符串」字典
cwd—工作目录
urlhttp 必填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(cordis Context 上的权威字段,由 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 里:

参数默认值说明
profileweb宿主未提供 profileContext 时要维护的 profile(profiles/<profile>/);正常运行以宿主实际 profile 为准
backuptrue写前是否落 .bak
dshHome未设置显式指定 DSH 根目录;优先级高于 $DSH_HOME(低于宿主 profileContext)
patchFile未设置显式指定到文件的绝对路径,连宿主 profileContext 都能压过;相对路径被忽略
probeHeaderstrue健康检查探测时是否带上该条目配置的 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
  • 两类外部网络请求,都只在必要时发生:
    1. 版本检查 —— 只打 api.github.com 的 releases/latest,8 秒超时;
    2. 健康检查 —— 只对你自己配置的 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)。剩下的可能是两件事——

  1. 条目形状是旧版的(≤0.4.2 写入的条目在顶层,宿主不会加载)。看列表页顶部有没有那条黄色提示;有就点「修复」,不用重启——改完一两秒 HMR 就会重新加载。
  2. 形状已对但连接没建立。@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…