runcat-tommy/dsh-mcp-mini-manager ↗★ 0

dsh-mcp-mini-manager

在设置中提供极简MCP服务器管理页面 适合需要直观查看、管理和移除当前DSH中已装载MCP服务器的用户。

包名
dsh-mcp-mini-manager
兼容性
待验证
版本
1.5.3
许可证
MIT
最近更新
2026年10月13日

安装

$npx -p @deepseek-ai/dsh dsh plugin --profile web add github:runcat-tommy/dsh-mcp-mini-manager

dsh-mcp-mini-manager

English | 中文

DSH 的 MCP 极简管理:在「设置」里加一页,与「插件」同级,列出当前 DSH 实际装载的每个 MCP 服务器及其简介与状态,并且可以逐个移除。

  • 插件 id:dsh-mcp-mini-manager
  • 中文名:MCP 极简管理
  • 英文名:MCP Mini Manager
  • 设置页 id:mcp-mini-manager(settings.section,order 16,紧接「插件」)

MCP 极简管理设置页(中文界面)

它解决什么

DSH 里没有一处能看到「我现在装了哪些 MCP」。要知道这件事,只能去翻 cordis.patch.yml、再看各个包的 bundle 补丁,还要自己判断哪一行是 MCP;移除一个 MCP 更没有任何界面入口。这一页把这两件事做成一步。

它为什么能知道有哪些 MCP

这是本插件唯一需要解释的设计点。

在 DSH 里,一个 MCP 服务器就是 cordis 装载树里的一行,模块是 @deepseek-ai/dsh-mcp-client。这一点写在该包自己的类型文档里:

MCP client bridge plugin: connects to an external MCP server and registers its tools on ctx.tools … Each plugin instance connects to one MCP server; load multiple instances in cordis.yml for multiple servers.

所以「装了哪些 MCP」= 「ctx.loader 的条目里哪些是 dsh-mcp-client」。本插件从两处读,并合并:

来源提供什么局限
运行中的 Loader(ctx.loader.entries())权威的 config(serverName / transport / command / url …)与真实 fiber 状态看不见被停用的行——停用的行根本不在树里
补丁层(各 bundle 的 cordis.patch.yml + profile 的 cordis.patch.yml + ~/.dsh/cordis.patch.yml)被停用的行、以及从哪个包来的只有静态配置,没有运行状态

两边合并后,一个「昨天被移除」的 MCP 仍然会出现在页面上,带着「已移除」状态和它的原始配置(来自移除记录)。合并规则:运行中的树在它能看见的事情上优先,补丁层补上树结构上看不见的那部分。

页面严格遵守一条边界:浏览器从不发送路径,也不发送 YAML。每个请求只带一个行 id,而这个 id 是本插件的目录接口自己报出去的;host 端会重新确认「它现在确实是一个 MCP 客户端行」之后,才推导要写哪个文件、写哪一段。

移除是怎么实现的

「移除」= 往 profile 自己的 cordis.patch.yml 追加一段按 id 定位的停用补丁:

# >>> dsh-mcp-mini-manager: disabled mcp row mcp-github
# server: github
# removed:  by dsh-mcp-mini-manager
# restore this server from the MCP 极简管理 settings page, or delete
# these lines to re-enable the row by hand.
- id: mcp-github
  disabled: true
# /.mcp-mini-manager/removed/.json`。页面上的「恢复」只做一件事:删掉那段停用块。停用再恢复是**逐字节还原**——文件里你原本的注释、`!!js` 表达式、其他插件的补丁都原样不动,因为写入是**行级拼接**,不是 YAML 解析后重写。

> **为什么不整份重新序列化 YAML。** 那个文件是用户手写的、被所有插件共用。任何「解析 → 修改 → 重新导出」都会把注释和 `!!js` 表达式重排甚至改义,从而悄悄弄坏别的插件。所以本插件只做行级插入与整段删除,且每段都有自己的起止标记。

### 会被拒绝的情况(都是刻意的)

| 情况 | 行为 |
|---|---|
| 这一行已经是停用状态 | 拒绝(`already-disabled`),文件不动 |
| 起止标记不配对(手改过、或写入被打断) | 拒绝(`marker-unpaired`)——此时这段的边界不可知,拼接或删除都不能诚实完成 |
| 补丁文件末尾没有换行 | 拒绝(`no-trailing-newline`)——会让恢复不再精确 |
| 行 id 的形状像路径(`a/b`、`../x`、`C:\x`) | 在碰文件系统之前就按文法拒绝(`bad-row-id`) |
| 请求来自其他来源(`Origin` 与 `Host` 不一致) | 403 拒绝 |

**用户层里已经有同一行的补丁时,照常写入。** 这一点值得说明,因为直觉上会以为"同 id 重复"要拒绝:`applyEntryPatches` 对每个补丁**独立、按键**应用,所以一条只带 `id` + `disabled` 的补丁和一条带 `name` + `config` 的补丁可以安全共存——DSH 自己就是这样分层的。连"用 `insert` 声明这个 MCP 行"这种最常见的形态也因此照样能移除,否则页面上大部分行都会变成不可操作。

「清空移除记录」是本插件唯一不可逆的操作,它**只删记录文件,不恢复也不删除任何 MCP**,并且需要二次确认。

## 页面上还有什么

- **简介**:每个服务器一行——stdio 显示命令行,streamable-http 显示 endpoint;配置里若含 `!!js` 表达式会标出是哪几个字段(**只报字段名,永不显示值**)。
- **状态**:`在用` / `已移除` / `启动失败` / `启动中` / `未观测`,由 Loader 的 fiber 状态与补丁层共同推导,只出一个结论。
- **工具**:该服务器通过 `mcp____` 注册给模型的工具及描述。
- **疑似自带 MCP 能力的插件(只读)**:有些插件自己用 MCP 连远端、工具命名自己决定(不经过 `dsh-mcp-client`)。移除它们等于卸载插件,语义不同,所以这里**只列出、不提供操作**。
- **诊断信息**:profile 目录、写入目标、home 层补丁、读取了哪些补丁层、以及每一层里无法解析的内容。

### 一条要说明的局限

fiber 处于 `active` **只说明插件装载成功,不代表远端连上了**。连接与重连状态只写进日志(`dsh-mcp-client` 用 logger 汇报 reconnecting / recovered / 最终失败),本插件不去猜。页面底部对此有明示。

## 安装

本插件是一个标准的 DSH bundle(声明了 `dsh.bundle.patch` 与 `dsh.client`),把包装进 profile 并**重启一次** `dsh web` 即可。三条路都可用,区别只在"要不要本机有 git / 会不会触发构建审批 / 以后怎么更新"。

### ① 从 GitHub 直接装

```bash
dsh plugin --profile web add github:runcat-tommy/dsh-mcp-mini-manager
  • 适合:只是想用,不想 clone。
  • 要注意:需要本机有 git。仓库里已经提交了全部运行产物(lib/ 三个文件)且没有任何 preinstall/install/postinstall/prepare 脚本,所以通常不会触发构建审批;若 DSH 仍然要求批准,按提示同意即可。
  • 更新:重跑同一条命令。

② 下载下来再装

不用 git、不用 npm,把仓库当一个压缩包取下来:

  1. 打开 https://github.com/runcat-tommy/dsh-mcp-mini-manager
  2. 点绿色的 Code → Download ZIP
  3. 解压到一个固定目录,例如 D:\plugins\dsh-mcp-mini-manager(别放临时目录,路径会写进 profile 依赖)
  4. 指向解压出来的目录:
dsh plugin --profile web add D:\plugins\dsh-mcp-mini-manager

习惯用命令行的话,等价于:

git clone https://github.com/runcat-tommy/dsh-mcp-mini-manager.git
dsh plugin --profile web add D:\path\to\dsh-mcp-mini-manager
  • 适合:想改代码、想锁死某个版本、本机没有 git、或访问不了 npm。

  • 要注意:无需构建审批,仓库里已带全部运行产物。dsh plugin 实际是 pnpm 的转发壳——它在 profile 目录里跑 pnpm add,然后按装载后的真实状态对齐 dsh.profile.bundles。所以也可以用 pnpm add link: 手工挂载,此时要自己把包名加进 dsh.profile.bundles(pnpm add 不会自动加):

    {
      "dsh": {
        "profile": {
          "bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app", "dsh-mcp-mini-manager"]
        }
      }
    }
    
  • 更新:下载新一份覆盖旧目录(或 git pull),重跑 add,再重启。

③ 从 npm 装

dsh plugin --profile web add dsh-mcp-mini-manager

包地址:https://www.npmjs.com/package/dsh-mcp-mini-manager

  • 适合:最省事。包内已带完整运行产物(lib/ 三个文件),安装时不执行 prepare,也不触发构建审批。
  • 要注意:npm 上发布的是打过标签的稳定版本;想跟着 main 上的最新提交跑就走 ① 或 ②。当前 npm latest 与仓库 main 是同一个版本。
  • 更新:dsh plugin --profile web up dsh-mcp-mini-manager。

三路对照

命令需要什么构建审批更新方式
① GitHub 直装… add github:runcat-tommy/dsh-mcp-mini-manager本机有 git通常不需要(产物已提交、无 install 类脚本)重跑同一命令
② 下载再装下载 ZIP 解压后 … add D:\plugins\dsh-mcp-mini-manager无不需要覆盖目录 + 重跑 add + 重启
③ npm… add dsh-mcp-mini-manager无不需要… up dsh-mcp-mini-manager

三条路装的是同一个包、同一份代码,区别只在"怎么拿到"和"以后怎么更新"。

三条路都必须重启一次 dsh web——新 bundle 是在启动时由 profile manifest 读取的,只刷新页面不够(页面会明确提示「插件的主机半侧没有响应」)。装好之后,停用/恢复 MCP 本身都不需要重启:写入的是 profile 自己的补丁层,DSH 会热加载它。

查看与卸载:

dsh plugin --profile web list                        # 看已装了什么
dsh plugin --profile web remove dsh-mcp-mini-manager # 卸载插件(与"停用某个 MCP"是两回事)

装箱内容

文件作用
lib/index.js宿主半:五条 HTTP 路由、读取 Loader 与各补丁层、停用/恢复的写入与守卫
lib/catalog.js纯函数核心:行分类、配置投影、补丁层解析、planDisable / planRestore
lib/client.js浏览器半:手写的 __ModuleLoader__ bundle,注册 settings.section
cordis.patch.ymlbundle 补丁:在本 profile 的装载树里插入一行
test/81 个测试:纯规则、宿主集成(真实临时目录 + 分层补丁)、浏览器半结构
scripts/verify-e2e.mjs端到端冒烟:一个进程里加载两侧真入口,跑完整周期(npm run smoke)

浏览器半不需要构建步骤:它只 require("react") 这一颗平台种子,其余全部用同源 fetch 与宿主半对话。文案通过 locale 服务注册中英双语,跟随 DSH 界面语言。

宿主接口

这些路由挂在 DSH 的 webServer 上,不在 /api 网关的同源防护之内——Host 是恶意页面唯一无法伪造的头,所以每个带 Origin 的请求都要 Origin 与 Host 一致才放行,不一致直接 403。刻意不带 Origin 的请求(curl、脚本)会放行,因为它们不是浏览器发起的跨站请求,这类调用方本来就有本机文件权限。

方法路径作用
GET/dsh-mcp-mini-manager/api/catalog目录:全部 MCP 行 + 移除记录 + 诊断信息
GET/dsh-mcp-mini-manager/api/rows原始行(调试用)
POST/dsh-mcp-mini-manager/api/disable停用一个 MCP(body:{rowId})
POST/dsh-mcp-mini-manager/api/restore恢复一个 MCP(body:{rowId})
POST/dsh-mcp-mini-manager/api/purge清空移除记录(唯一不可逆,只删记录,不恢复也不删除任何 MCP)

开发

npm test          # 81 个测试:catalog 纯逻辑 + 宿主集成 + 浏览器半结构
npm run smoke     # 端到端冒烟:在一个进程里加载两侧真入口,跑完整目录/停用/恢复/清空
  • lib/catalog.js — 纯函数:行分类、配置投影、补丁层解析、停用/恢复计划。
  • lib/index.js — 宿主半:挂 webServer 路由,读 Loader 与各补丁层。
  • lib/client.js — 浏览器半:手写 ModuleLoader bundle,只依赖平台提供的 react。

npm test 里有两条值得单独说:

  • 客户端测试会把样式表里出现的每一个 CSS 自定义属性,逐个对着已安装的 DSH 主题文件校验。令牌名写错不会报错、只会静默变成无样式,所以这条断言是必要的。
  • scripts/verify-e2e.mjs 里的假 Loader 按 applyEntryPatches 的真实语义合成补丁层(逐补丁、逐键覆盖),所以"停用 → 恢复"的往返是在真实的合成语义上验证的,而不是在猜测的语义上。

跑冒烟脚本时请确保 $DSH_HOME 指向沙箱:移除记录写在 harness home 下,脚本默认会把它指到临时目录。

已知边界

  • 必须有 loader 服务才能列出运行中的行;读不到时页面会明说,而不是显示成「没有 MCP」。
  • 只桥接工具:dsh-mcp-client 本身不桥接 resources / prompts,所以这里也看不到。
  • 补丁层解析是子集:支持块映射/序列、引号标量、!!js、注释、以及 profile 模板里的空列表哨兵 [];不支持跨行 flow collection、锚点/别名、多行块标量、merge key——遇到不支持的构造会记进诊断信息,而不是猜。
  • 多 profile:写入目标永远是当前运行 profile 的用户层(由 Loader 的 baseUrl 推导)。

许可

MIT

变更记录见 CHANGELOG.md。