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,紧接「插件」)

它解决什么
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 incordis.ymlfor 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: <ISO 时间戳> 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
# <<< dsh-mcp-mini-manager: disabled mcp row mcp-github
这条路径有三个刻意的性质:
- 不改任何包文件。 目标行可能来自
dsh-base、dsh-web-app或第三方插件的补丁层,那些目录是包自己的、重装即还原。停用写进的是用户层,也就是dsh-app-boot每次启动都会应用、并且用户在编辑的那一层。 - 不需要重启。 DSH 启动时会
watchUserPatches监听 profile 层与 home 层,写入后会被重新解析并应用。host 端在写完后会重新读一次运行树,据实告诉你是「已即时生效」还是「重启后生效」,而不是替你承诺。 - 可恢复。 写补丁之前,原始那一行会先存到
<harness home>/.mcp-mini-manager/removed/<rowId>.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__<serverName>__<rawName>注册给模型的工具及描述。 - 疑似自带 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 直接装
dsh plugin --profile web add github:runcat-tommy/dsh-mcp-mini-manager
- 适合:只是想用,不想 clone。
- 要注意:需要本机有
git。仓库里已经提交了全部运行产物(lib/三个文件)且没有任何preinstall/install/postinstall/prepare脚本,所以通常不会触发构建审批;若 DSH 仍然要求批准,按提示同意即可。 - 更新:重跑同一条命令。
② 下载下来再装
不用 git、不用 npm,把仓库当一个压缩包取下来:
- 打开 https://github.com/runcat-tommy/dsh-mcp-mini-manager
- 点绿色的 Code → Download ZIP
- 解压到一个固定目录,例如
D:\plugins\dsh-mcp-mini-manager(别放临时目录,路径会写进 profile 依赖) - 指向解压出来的目录:
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上的最新提交跑就走 ① 或 ②。当前 npmlatest与仓库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.yml |
bundle 补丁:在本 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。
No comments yet. Be the first to write one.