mcp-ui-dsh
Verified@gxx-labs/mcp-ui-dsh · v0.2.4 · MIT · Web UI
DSH 插件:设置 →「MCP」页签,紧凑可折叠列表 + 表单/JSON 配置 MCP 服务,保存即生效,并展示每个服务已注册的工具。UI 复用 DSH 设计令牌与 primitives。
Install
dsh plugin add @gxx-labs/mcp-ui-dsh Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Published to npm without a public repository. Inspect the package contents before installing.
Tags
Readme
@gxx-labs/mcp-ui-dsh
DSH 插件:在设置 →「MCP」页签里用表单或 JSON 配置 MCP(Model Context Protocol)服务,
保存即生效(写入 profile 的 cordis.patch.yml 并经 Loader 热重载,无需重启 DSH)。
界面复用 DSH 的设计令牌与 primitives,与官方设置页观感一致。
功能
| 能力 | 说明 |
|---|---|
| 设置页签 | 设置 → MCP(settings.section,order 26,排在官方页签之后) |
| 表单配置 | 逐服务卡片式编辑:服务名 / 传输方式 / 命令 / 参数 / 工作目录 / 环境变量 / 请求头 / 超时 / 启动失败即报错 |
| JSON 配置 | 直接编辑整份配置;支持三种格式(见下),可一键格式化 |
| 即时生效 | 保存 → 写盘 → DSH 自带的 HMR 监听 cordis.patch.yml 并热重载,MCP 工具立刻可用 |
| 两种传输 | stdio(本地进程)与 streamable-http(远程端点) |
| 密钥保护 | env / headers 的值不下发浏览器;留空即保持原值 |
| 双语 | 中文 / English,跟随 DSH 语言设置 |
安装
npm install @gxx-labs/mcp-ui-dsh
或在 DSH 插件管理器里「添加插件」→ 填 @gxx-labs/mcp-ui-dsh。
装完完全重启一次 DSH(托盘退出后重新打开),设置里就会出现「MCP」页签。
首次安装后重启是必须的:客户端 bundle 由宿主在启动时扫描
dsh.client装载。
使用
表单模式
- 设置 → MCP → 「添加服务」
- 填
serverName(工具会以mcp__<serverName>__<工具名>暴露) - 选传输方式:
- stdio:填
command(如npx)、args(如-y @modelcontextprotocol/server-filesystem)、可选cwd与env - streamable-http:填
url(如http://127.0.0.1:3000/mcp)、可选headers
- stdio:填
- 「保存并生效」
保存成功后页面会提示已应用的服务数;对应工具立即出现在 Agent 的可用工具里。
JSON 模式
点顶部「JSON」切换。接受三种形状,保存时整体替换本插件管理的服务:
// A. 本插件导出格式(服务名 → 配置)
{
"fs": {
"transport": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "D:\\work"],
"env": {},
"cwd": "",
"toolCallTimeoutMs": 60000,
"failOnStartupError": false
}
}
// B. Claude Desktop / Cursor 习惯(mcpServers 包裹)
{
"mcpServers": {
"demo": { "command": "uvx", "args": ["mcp-server-demo"] }
}
}
// C. patch 行数组
[{ "id": "mcp-ui-demo", "name": "@deepseek-ai/dsh-mcp-client",
"config": { "serverName": "demo", "transport": "streamable-http", "url": "http://127.0.0.1:3000/mcp" } }]
未知字段(如 reconnect、maxInstructionBytes)原样保留——用 JSON 配的高级选项不会因为
在表单里保存一次就消失。
它到底做了什么
MCP 服务在 DSH 里不是某个服务的可写数据,而是 Loader 行:
# ~/.dsh/profiles/<profile>/cordis.patch.yml
- insert:
- id: mcp-ui-fs
name: '@deepseek-ai/dsh-mcp-client'
config:
serverName: fs
transport: stdio
command: npx
args: ['-y', '@modelcontextprotocol/server-filesystem']
官方 @deepseek-ai/dsh-mcp-client 的 apply() 只建立连接,没有任何 CRUD API;
mcpResources 也只有 register。所以「新增 / 修改 MCP 服务」唯一正规做法就是写 Loader 行。
即时生效的实现
保存时本插件只做两件事:
- 用 YAML Document 编辑
cordis.patch.yml(保留注释、!!js标签、字段顺序) - 原子写盘(同目录临时文件 + rename)
然后什么都不做 —— DSH 自带 @deepseek-ai/dsh-hmr,用 chokidar 监听
profile.patchPath(就是 cordis.patch.yml),文件一变即执行:
const patches = readProfilePatches("dsh", profile);
await reconcileProfilePatches(this.ownerContext.root, patches, "dsh");
也就是官方已经替我们做了正确的热重载。写盘后本插件只会只读地
loader.await() 等一下让状态收敛(超时不算失败),再回一个
pending 提示,让页面能显示「已保存,正在生效」。
🔴 千万不要自己去驱动 Loader(0.1.1 就是这么把整个 DSH 搞挂的)。 当时的做法是拿根 Include 条目再
entry.update({ config: { ...cfg, patches: <裸 patch 列表> } }), 这绕过了官方prepareProfilePatches:function prepareProfilePatches(ctx, patches, parentURL, binName) { if (ctx.get("profileContext") === void 0) return patches; const rows = prepareProfileEntries( ctx, applyEntryPatches([], patches, patchWarning(ctx)), parentURL, binName); return rows.length === 0 ? [] : [{ insert: rows }]; }
applyEntryPatches([], patches, warn)以空数组为基底,把 patch 物化成真正的条目行 (insert:展开成扁平行,- id:覆盖式 patch 因无基底被丢弃并告警),再包成 单个{ insert: rows }。塞未转换的裸 patch 进去 → 同一批 patch 被应用两遍 → 所有条目被重复/错误重建 → 大量服务失联 → 整个 DSH 崩。测试里加了硬约束:假 ctx 的
loader.update/create/remove一旦被调用即抛错。 以后谁再"顺手"加一次 update,测试立刻红。另:
findRootIncludeEntry()保留但只用于诊断。它的判定要注意根 Include 的options.name是cordis:include(ctx.loader.create({ id:"include", name:"cordis:include" })创建,属 Loader builtin 语法),不是 npm 包名@deepseek-ai/cordis-plugin-include。
只动自己的行
profile 里可能有用户手写的 MCP 行或其他插件的行。本插件只增删改 id 带 mcp-ui- 前缀
且 name 为 @deepseek-ai/dsh-mcp-client 的行,其余内容逐字保留(含注释)。
删完最后一个服务时,会把因此变空的 insert: 块一并清掉,不留空壳。
安全模型
- 读:同源页面即可。但
env/headers的值不下发,只回传键名与数量。 - 写:仅限本机 loopback(
127.0.0.1/::1),因为写的是 profile 配置文件。 - 密钥往返:表单里已有键显示为空,留空提交 = 保持原值(宿主按空串还原)。 想真正删除某个键,就在表单里删掉那一行。
HTTP 路由
| 方法 | 路径 | 用途 |
|---|---|---|
| GET | /api/mcp-ui/state |
当前已配置的服务(env/headers 已脱敏)+ 写入文件路径 |
| POST | /api/mcp-ui/save |
保存并生效;体为 { servers: [...] } 或 { json: "..." } |
| POST | /api/mcp-ui/json |
取当前配置的 JSON 文本 |
开发
npm install
npm run build # 构建 dist/client.js(esbuild 产物 + ModuleLoader 包装 + CSS 注入)
npm run verify # 产物契约校验(15 项:id/导出/CSS 令牌/类名对齐/无宿主模块泄漏…)
npm test # 77 个测试
npm run check # build + verify + test
目录
lib/mcp.js 服务领域模型:规范化、校验、与 patch 行互转(无 DSH 依赖,可单测)
lib/store.js profile patch 读写 + Loader 热重载(即时生效)
lib/paths.js 定位 profile 的 cordis.patch.yml
lib/index.js 宿主半:三条同源 HTTP 路由
client/index.jsx 浏览器半:设置页签 UI
client/model.js 表单 ↔ 配置的纯逻辑
client/Mui.module.css 样式(只用 --dsw-* 令牌)
cordis.patch.yml 本插件自己的 bundle patch(声明 Loader 行)
测试覆盖
| 文件 | 覆盖 |
|---|---|
test/mcp.test.mjs |
规范化、校验、serverName 唯一性、JSON 三形状、patch 行往返 |
test/store.test.mjs |
YAML 文档编辑:增删改只落在自己的行、用户内容逐字保留、幂等 |
test/e2e.test.mjs |
真实 writeServers:落盘/读回、校验失败不改文件、原子写不留临时文件、「绝不碰 Loader」硬约束 |
test/routes.test.mjs |
脱敏(密钥不外发)、密钥合并、loopback 限制、方法/请求体错误映射 |
test/model.test.mjs |
表单字段收拾(切换传输不留残字段)、参数切分、密钥占位往返 |
已知限制
- 服务名唯一:
serverName全局唯一。重复会让第二个mcp-client实例启动即抛错 (官方activeServerNames保留表),因此保存前会拦截。 - 一个服务一行:官方
mcp-client一个实例连一个服务器,所以 N 个服务 = N 个 Loader 行。 - 生效是异步的:保存成功后 DSH 的 HMR 会自行装载,通常瞬间完成; 若页面提示「正在生效」,稍等即可(文件已经写好,不会丢)。
- 连接结果要看工具:保存成功只代表配置已写对、行已装载。
failOnStartupError关闭时,服务器连不上不会让保存失败——请在对话里试mcp__<serverName>__<工具名>确认连通性。 cwd/command路径在 Windows 上请用双反斜杠或正斜杠(JSON 转义)。
版本历史
| 版本 | 变更 |
|---|---|
| 0.1.2 | 修复把整个 DSH 搞挂的严重 bug:不再自己驱动 Loader,改为只写文件(HMR 自动重载)。加硬约束测试。 |
| 0.1.1 | 修复根 Include 识别名(cordis:include,非 npm 包名)。 |
| 0.1.0 | 首个版本。 |
许可
MIT