🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

技能与 MCP 管理 j62268781-alt/dsh-skills-mcp-manager

DeepSeek Harnessv0.5.6
j62268781-alt15 天前有提交已提供中文文档扫描:含敏感能力

DSH 设置面板:一个页面管理技能SKILL.md与 MCP 服务器,全局 / 项目级分层,改完即生效。Manage…

36.5/100
综合分
2
GitHub 星标
472
npm 周下载
—/5
用户评分

数据截至 2026/10/20 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add @j62268781-alt/dsh-skills-mcp-manager
npm 源 · @j62268781-alt/dsh-skills-mcp-manager 的发布者账号已校验与本仓库一致

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:已验证
是什么
dsh 原生插件 · market
装得上吗
本站已在 dsh 0.2.1-rc.5 下真实安装成功
非静态推断
L5 · 运行时冒烟等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 15 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查✓ 自动检查通过
展开查看检查明细 ↓

以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。

✓npm 包@j62268781-alt/dsh-skills-mcp-manager @ 0.5.6
✓Node 引擎要求 >=20 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明

验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/11/3 01:02:20

安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

可执行外部命令读写/删除本地文件发起外部网络请求
查看扫描证据(共 64 处,展示前 62 条)
  • 读取本地文件
    dist/index.js:340
    mtimeMs = statSync(file).mtimeMs;
  • 读取本地文件
    dist/index.js:1966
    const text = await readFile(path, 'utf8');
  • 发起网络请求
    lib/client.js:1052
    const response = await fetch(`/${NAME}/runtime`, {
  • 发起网络请求
    lib/client.js:1249
    const response = await fetch(`/${NAME}/rows`, {
  • 发起网络请求
    lib/client.js:1283
    const response = await fetch(`/${NAME}/skills`, {
  • 发起网络请求
    lib/client.js:1316
    const response = await fetch(`/${NAME}/skill?path=${encodeURIComponent(path)}`, {
  • 发起网络请求
    lib/client.js:1332
    const response = await fetch(`/${NAME}/reconnect`, {
  • 发起网络请求
    lib/client.js:1354
    const response = await fetch(`/${NAME}/import`, {
  • 读取本地文件
    dist/runtime/env.js:35
    if (shell.startsWith('/') && existsSync(shell))
  • 执行外部命令来自配置/变量
    dist/runtime/env.js:53
    const found = execFileSync(loginShell(), ['-lc', `command -v ${JSON.stringify(raw)}`], { encoding: 'utf8', timeout: 5000 }).trim();
  • 执行外部命令来自配置/变量
    dist/runtime/env.js:71
    const out = execFileSync(loginShell(), ['-lc', 'printf %s "$PATH"'], { encoding: 'utf8', timeout: 5000 }).trim();
  • 读取本地文件
    dist/runtime/env.js:114
    for (const id of readdirSync(root)) {
  • 读取本地文件
    dist/runtime/env.js:117
    if (existsSync(node) && existsSync(pnpx)) {
  • 写入本地文件
    dist/patch/apply.js:79
    await writeFile(path, text, 'utf8');
  • 写入本地文件
    dist/patch/apply.js:105
    await mkdir(backupDir(), { recursive: true });
  • 写入本地文件
    dist/patch/apply.js:106
    await writeFile(backupPath(), before);
  • 读取本地文件
    dist/patch/apply.js:210
    const before = await readFile(path, 'utf8');
  • 读取本地文件
    dist/patch/apply.js:294
    await readFile(target, 'utf8');
  • 写入本地文件
    dist/patch/apply.js:302
    await mkdir(dirname(target), { recursive: true });
  • 写入本地文件
    dist/patch/apply.js:303
    await writeFile(target, renderSkillMarkdown(entry), 'utf8');
  • 读取本地文件
    dist/patch/apply.js:347
    const before = await readFile(path, 'utf8');
  • 读取本地文件
    dist/patch/apply.js:557
    const before = await readFile(path, 'utf8');
  • 读取本地文件
    dist/import/scan.js:17
    return { files: paths.map((path) => ({ path, exists: existsSync(importFilePath(path, project)), servers: [], unsupported: 0, error: '' })), servers: [
  • 读取本地文件
    dist/import/scan.js:23
    const report = { path: relative, exists: existsSync(file), servers: [], unsupported: 0, error: '' };
  • 读取本地文件
    dist/import/scan.js:26
    const document = parseJsonLoose(readFileSync(file, 'utf8'));
  • 写入本地文件
    dist/diagnostics/heartbeat.js:11
    await mkdir(dirname(beatFile()), { recursive: true });
  • 写入本地文件
    dist/diagnostics/heartbeat.js:12
    await writeFile(beatFile(), `${JSON.stringify(payload, null, 2)}\n`);
  • 读取本地文件
    dist/skills/reconcile.js:18
    if (existsSync(join(current, '.git')))
  • 读取本地文件
    dist/skills/reconcile.js:57
    text = await readFile(stateFile(), 'utf8');
  • 读取本地文件
    dist/skills/reconcile.js:62
    text = await readFile(join(dshHome(), `${LEGACY_NAME}.state.json`), 'utf8');
  • 写入本地文件
    dist/skills/reconcile.js:66
    await writeFile(stateFile(), text, { flag: 'wx' });
  • 读取本地文件
    dist/skills/reconcile.js:71
    text = await readFile(stateFile(), 'utf8');
  • 读取本地文件
    dist/skills/reconcile.js:102
    const current = await readFile(path, 'utf8').catch(() => undefined);
  • 写入本地文件
    dist/skills/reconcile.js:104
    await mkdir(dirname(path), { recursive: true });
  • 写入本地文件
    dist/skills/reconcile.js:105
    await writeFile(path, text);
  • 删除本地文件递归
    dist/skills/reconcile.js:124
    await rm(dirname(path), { recursive: true, force: true });
  • 写入本地文件
    dist/skills/reconcile.js:134
    await mkdir(dirname(stateFile()), { recursive: true });
  • 写入本地文件
    dist/skills/reconcile.js:135
    await writeFile(stateFile(), `${JSON.stringify({ files: owned }, null, 2)}\n`);
  • 读取本地文件
    dist/skills/discover.js:79
    const text = await readFile(file, 'utf8');
  • 读取本地文件
    dist/skills/discover.js:93
    entries = await readdir(root.path, { withFileTypes: true });
  • 读取本地文件
    dist/skills/write.js:61
    if (existsSync(target))
  • 写入本地文件
    dist/skills/write.js:63
    await mkdir(target, { recursive: true });
  • 写入本地文件
    dist/skills/write.js:64
    await writeFile(join(target, 'SKILL.md'), renderSkillMarkdown({ name: checked.value, description, body }), 'utf8');
  • 读取本地文件
    dist/skills/write.js:73
    if (!existsSync(target))
  • 写入本地文件
    dist/skills/write.js:75
    await writeFile(join(target, 'SKILL.md'), renderSkillMarkdown({ name: checked.value, description, body }), 'utf8');
  • 读取本地文件
    dist/skills/write.js:89
    if (prev.value === next.value && existsSync(join(dir, prev.value))) {
  • 读取本地文件
    dist/skills/write.js:94
    if (!existsSync(source))
  • 读取本地文件
    dist/skills/write.js:96
    if (existsSync(target))
  • 写入本地文件
    dist/skills/write.js:100
    await rename(source, target);
  • 读取本地文件
    dist/skills/write.js:101
    const existing = await readFile(join(target, 'SKILL.md'), 'utf8').catch(() => '');
  • 写入本地文件
    dist/skills/write.js:103
    await writeFile(join(target, 'SKILL.md'), renderSkillMarkdown({ name: next.value, description, body: bodyText }), 'utf8');
  • 删除本地文件递归
    dist/skills/write.js:116
    await rm(target, { recursive: true, force: true });
  • 读取本地文件
    dist/patch/readRows.js:93
    const info = await stat(path);
  • 读取本地文件
    dist/patch/readRows.js:94
    const text = await readFile(path, 'utf8');
  • 发起网络请求
    dist/mcp/probe.js:76
    await service.request(serverName, { method: 'resources/list' }, { agent: options.agent, signal, timeout: timeoutMs });
  • 写入本地文件
    dist/mcp/oauth/store.js:112
    await mkdir(dir, { recursive: true, mode: 0o700 });
  • 写入本地文件
    dist/mcp/oauth/store.js:115
    await writeFile(temp, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
  • 写入本地文件
    dist/mcp/oauth/store.js:116
    await rename(temp, file);
  • 写入本地文件
    dist/mcp/oauth/store.js:120
    await chmod(file, 0o600);
  • 读取本地文件
    dist/mcp/oauth/store.js:138
    text = await readFile(file, 'utf8');
  • 删除本地文件
    dist/mcp/oauth/store.js:176
    await unlink(file);
  • 读取本地文件
    dist/mcp/oauth/sdk.js:101
    pkg = JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf8'));
扫描于 2026/11/3(1 天前) · 范围 入口闭包 + 全包扫描 · 来源 npm 发布包

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

依赖的 DSH / Cordis 模块
@deepseek-ai/dsh-mcp-client
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

界面预览

3 张 · 点击查看大图

功能亮点

  • 管理技能与MCP服务器
  • 全局与项目级分层
  • 修改即时生效
v0.5.62026-10-20
· 0.5.6 — 配置与文件分家:技能只剩磁盘,MCP 只剩文件 CRUD ⚠️ 破坏性变更:升级前请读 docs/MIGRATION-0.5.6.md。升级会自动迁移(写前备份),但 Host 侧改动需要重启一次 DSH。0.5.x 的面板与 0.5.6 的 Host 不兼容,请一起升级。 · 为什么 0.5.x 的 settings 文档同时承担了业务文件、命令队列和读模型三种东西,于是「一条命令」变成「重启后可能重放的配置」,「一个缓存」变成「每 5 秒重写一次的 yml 字段」。0.5.6 把它们拆开: | 数据 | 新的归属 | |---|---| | 技能 | 磁盘上的 //SKILL.md(唯一真源) | | MCP 配置 | cordis.patch.yml 里的 Loader 行(唯一真源) | | 列表 / 工具 / 连接 / 授权状态 | GET //runtime 现算 | | 命令 | 各自 HTTP 路由的请求/响应 | | 用户偏好 | 只剩 toolFilters | · 修复 · 明文 token 端点上「重新授权」必然失败 —— SDK 在存在 refresh_token 时无条件走自己的刷新,而那条路的 TLS 检查会抛错且不吞掉。现在明文端点下传 forceReauthorization 跳过它。 · 有凭据却仍显示「需要授权」 —— 鉴权挑战是缓存值;服务器不认这份令牌时,缓存的 401 会把状态钉住。现在有凭据时不再产出 needs-auth。 · 新增行要等约 4 秒才可见 —— 列表改为直接读 patch 文件(配置真源),phase 仍从 Loader 合并(运行态真源)。现在几十毫秒。 · stdio 服务器连不上 —— 默认走 pnpx,而 pnpx 在 pnpm store 里依赖没被链接进去(Cannot find package 'zod')。改为默认系统 npx,内置 runtime 降为兜底。 · 「运行时」下拉从没接到 Host 上 —— 值只活在面板 draft 里,commonFields / RowOp / applyRowOp / serverFromRow 全都没有这个字段,选什么都不生效。现在整条链路打通。 · 倒计时在页面 hidden 时冻住 —— 原先是「不可见就不更新」,倒计时永远停在某个数字上。改成降频而非停止。 · PATH 写进 profile —— profile 变得「只在这台机器上对」,面板还会把几百字符的宿主机路径显示给用户。改成运行时注入。 · 部署 pack-deploy 改用官方 dsh plugin remove + add,并校验 dsh.profile.bundles:依赖装上了、patch 里有条目,都不代表插件会被加载 —— 不在装配清单里时 DSH 静默跳过,设置页整块消失且零报错。现在这一项缺失会让部署当场失败。 · 验证 · 测试 551 全绿(1 跳过),本地与模拟 CI 环境(无内置 runtime)双跑 · Host 冒烟 121 条断言通过:加载 / 7 条路由 / 技能落盘 / 行文件 CRUD / 无需等 Loader / 零 settings 写盘 · 关键修复均做反向验证(把修复去掉,对应断言立刻失败)
v0.5.52026-10-15
· ⚠️ 这是一个破坏性版本 升级前请读 docs/MIGRATION-0.5.5.md。 三项改动都动了「数据存在哪里」: | 破坏性变更 | 不做迁移的后果 | 自动迁移做了什么 | |---|---|---| | 插件身份 skills-mcp-panel → dsh-skills-mcp-manager | 面板认不出自己的行 ⇒ 列表全空(而配置完好) | 改行 id + 删 3 个废弃字段,其余 13 个键原样保留 | | 项目级 MCP 从 config.servers[] 改为带 scope 的 profile 行 | 项目级服务器连不上 | 逐条转行(字节与手工添加一致),已存在则不覆盖 | | 行操作从 settings 队列改为 POST //rows | 命令没有响应 | 队列保留为回退通道,旧面板/旧 Host 三条路都可用 | 迁移全部自动完成,但需要重启一次 DSH(Host 侧改动无法热加载)。 --- · 主要变更 · 性能 · 技能增删改走 HTTP(新增 POST //skills):写盘实测只要 1ms,而队列通道要连等四个轮询(600ms 取请求 + 面板回读 + 2s 对账 + 5s 投影),最坏约 9.1 秒。现在新增技能点击即出现卡片。read 仍留在队列(有即时反馈、不是痛点),队列保留为回退通道。 · MCP 行操作早已走 HTTP:增删不再等 2–4 秒。 · 新功能 · toolCallTimeoutMs(单次工具调用超时)—— 来自 PR #1,感谢 @kids0cn。默认 60 秒,分钟级工具调用不再被拦腰打断。 · 技能对话框改为三个正交选择:层级(全局 / 项目级)× 工作区(注册表下拉 + 自定义路径,与 MCP 一致)× 目录(.dsh/skills / .agents/skills),并显示解析后的绝对路径。 · 连接与重连默认值改为显式写出:表单预填、落库写值,不再依赖读 dsh-mcp-client 的源码才知道这一行怎么重连。 · 修复 · 项目级挂载丢失 env / headers(issue #2):形状不匹配,parseEnv 收到对象返回 {},导致 JAVA_HOME 等静默消失、Authorization 静默丢失。 · 项目行状态问错权威源(issue #3):connected 只在全局档信任工具数,项目行必然被 probe 判成「已放弃重试」,而它其实工作正常。 · 技能弹窗与正文读写的五处修复(弹窗卡「保存中…」、Config 导出丢失导致列表空、编辑读不出正文、报「未知的写入区域」、正文多一行)。 · 其它 · 连接状态标签、一键重连、需要授权识别、工具清单可视化、Skills 与 MCP 布局统一。 --- · 验证 · 376 项测试(375 通过 / 1 可选跳过 / 0 失败)+ 真实 Host 冒烟 · CI 在 Node 20 / 22 / 24 上全绿 · 真机验证:创建 / 编辑 / 删除技能、项目级 MCP 的 env 传递与状态显示 完整变更见 CHANGELOG.md。
v0.5.42026-10-13
连接状态标签 + 一键重连 —— 让悄悄失效的 MCP 看得见、救得回。 版本说明:这一版的功能曾以 0.6.0 / 0.6.1 发布过,但两个版本都带着缺陷,已从 npm 撤回。 0.6.0 会误报「激活失败」;0.6.1 的连接观测在真机上从未捕获到任何实例。 0.5.4 是合并修复后的正式版本。 · 这一版修的是什么 如果 DSH 启动时某个 MCP 连不上,它再也不会恢复 —— 哪怕服务器随后完全正常,工具也不会回来,而面板上什么都没有。实测一个 deepwiki 就这样静默失效了 15 分钟以上(同时服务端一直健康,HTTP 200 且能返回完整工具列表)。 根因在 @deepseek-ai/dsh-mcp-client(DSH 的依赖,本插件不改它),有两条不同路径: · 勾了「连不上时拒绝激活」= 一次都不重试。apply 只 await 第一次尝试的 connection.ready,失败即抛错 → 插件实例失败 → 清理时 dispose() 把重试定时器 clearTimeout 掉,重试循环连同实例一起消失。 · 没勾,但重试预算用完就永久放弃。默认 maxAttempts: 10、initialDelayMs: 500,退避 500→1000→…→30000,总共只等 151.5 秒;之后日志是 *"giving up after 10 consecutive failed reconnect attempts — reload the plugin or restart the Host to reconnect"*,彻底停止。 两种情况都终结于「只能重启」,而状态在界面上完全不可见。 · 新增 · 🚦 每张 MCP 卡片带连接状态标签 | 标签 | 含义 | |---|---| | 已连接 · N 个工具 | 正常 | | 未连接 · 重试中 · 预计剩 Ns | 还在预算内,会自己恢复 | | 已放弃重试 | 预算已用完,不会自己恢复 | | 激活失败 | 权威信号(Loader / 挂载 fiber)说这个实例失败了 | | 连接中 | 首次连接仍在进行(客户端最久等到 toolCallTimeoutMs,默认 60 秒) | | 未挂载 | 该项目当前没有会话 —— 不是故障 | | 状态未知 | 观测机制不可用 —— 如实说明,而不是假装正常 | | 已停用 | 用户停用(此时不再显示连接状态,避免留一个过期的「已连接」) | · 🔁 一键「重连」(只在需要它的状态上出现):全局行让 Loader 重建该行(停用→启用),项目级条目失效挂载 key 重挂。两条路都不需要重启 DSH —— dsh-hmr 用 chokidar 精确监听 profile 的 patch 文件,写入即时生效。两次写入刻意跨 tick 分开并在中间等 Loader 确认,因为写入后立刻改回可能被合并成「无变化」而根本不触发重载。 · 修复(撤回 0.6.x 的原因) · 观测机制从未捕获到任何实例(真机发现)。原做法劫持 mcpResources.register 记录每个实例的连接句柄,但 profile 重新组装时会换成新的服务实例,一次性写下的影子留在旧实例上,之后所有注册都从旁边走过去 —— 而它仍然报告自己「已装上」,面板因此显示得像是正常。现在改为调用服务公开的 request(server, request, exec):每次探测都重新解析 provider,不受实例更替影响,也不再需要劫持任何东西。这同时带来第三种可区分状态 —— 「没有 provider」(挂载/激活问题)与「连不上」(网络问题),两者处置完全不同。 · 误报「激活失败」。原做法从「观测不到实例」反推激活失败,而注册回调要等 fiber 变成 ACTIVE 才执行;fiber 在 apply 里 await 首次连接期间一直是 LOADING(最久 60 秒),于是「还没连完」被当成了「已经失败」。现在「激活失败」只由权威的 phase === 'failed' 判定。告警误报比没有告警更糟:用户会去修一个根本没坏的东西。 · phase 一直是个死字段。原先读 entry.fiberPhase,但那是 dsh-host-plugin-inventory 另外算出来的派生字段,原始 Loader entry 上只有 entry.fiber —— 于是行状态永远是空串。现在按官方 FiberState 映射从 entry.fiber.state 推出。 · 构建隐患:build:host 只跑 tsc,重命名模块后旧产物会留在 dist/ 里继续被打包(这次 watch.js 就是如此
数据来自仓库的 GitHub Release,可能与插件实际版本不一致。

README

由 DeepSeek 最新模型翻译生成
dsh-skills-mcp-manager 在 DSH 的设置面板里管理「技能」与「MCP 服务器」——全局 / 项目级分层,改完即生效 Listed on dsh-plugin.org DSH 插件:把 Skills 与 MCP 的管理搬进 设置 → Skills & MCP。页面是「两个 tab × 两个层级」——MCP 与技能各自分「全局」和「项目级」;卡片、按钮、间距都对齐宿主内置的「内置插件」页,不 import 任何 Harness Client 包。 - 🗂️ 技能:列表 / 搜索 / 预览全文 / 新建 / 重命名 / 删除,落盘就是 <技能根>/<名字>/SKILL.md;删除先备份再删,可手动恢复 - 🔌 MCP:全局条目就是 profile cordis.patch.yml 里的真实 Loader 行,增删改停写回配置文件;项目级条目只挂到该项目里运行的 agent - 🎛️ 按服务器筛选工具:展开卡片就能看到这一行实际注册了哪些工具(名字 + 描述,来自 harness 自己的工具注册表),并按服务器选择「全部保留 / 只保留勾选 / 只挡勾选」。
展开完整文档(README 剩余 1 节)
dsh-skills-mcp-manager

在 DSH 的设置面板里管理「技能」与「MCP 服务器」——全局 / 项目级分层,改完即生效

Listed on dsh-plugin.org

DSH 插件:把 Skills 与 MCP 的管理搬进 设置 → Skills & MCP。页面是「两个 tab × 两个层级」——MCP 与技能各自分「全局」和「项目级」;卡片、按钮、间距都对齐宿主内置的「内置插件」页,不 import 任何 Harness Client 包。

- 🗂️ 技能:列表 / 搜索 / 预览全文 / 新建 / 重命名 / 删除,落盘就是 <技能根>/<名字>/SKILL.md;删除先备份再删,可手动恢复
- 🔌 MCP:全局条目就是 profile cordis.patch.yml 里的真实 Loader 行,增删改停写回配置文件;项目级条目只挂到该项目里运行的 agent
- 🎛️ 按服务器筛选工具:展开卡片就能看到这一行实际注册了哪些工具(名字 + 描述,来自 harness 自己的工具注册表),并按服务器选择「全部保留 / 只保留勾选 / 只挡勾选」。被挡下的工具不进工具注册表,所以也不占用每次组装工具面的 token——实测一个 chrome-devtools 就有 30 个工具、约 25 KB schema
- 🚦 连接状态圆点 + 一键重连:每张卡片用一个小圆点加一行小字说明状态 —— 🟢可用 · N 个工具 / 🟡连接中 · 约 Ns / 🔴已放弃重试 / 🔴异常 / 🟡需要授权 / ⚪未挂载,需要时在 … 菜单里点「重连」即可恢复——不用重启 DSH。它解决的是一个很坑的场景:DSH 启动时连不上的 MCP,等重试预算用完就永久失效(默认只等 151.5 秒),而以前界面上完全看不出来。「需要授权」是其中单列的一种:需要 OAuth 的服务器被拒绝连接不是故障(dsh-mcp-client 没有 OAuth 支持,401 会被当成网络故障一路重试到放弃),所以它用黄色而不是红色,并说明补授权的办法
- 🎛 每行一个开关 + … 菜单:启用/停用是小号开关(同时表达当前档位),重连/编辑/删除收在竖向菜单里。停用的行不显示操作,重连只在启用且确实异常时出现
- 📥 MCP 导入:一键扫描 Claude Code / Cursor / VS Code / Gemini / Codex 等 26 个来源的用户级或项目级配置,预览后导入
- 🗂 一份列表,跨工作区:全局在前、然后按工作区顺序;卡片上的标签说明它属于谁(「全局」或工作区名)
- 🧯 不动坏你的配置:插件里绝不把异常抛出去(DSH 会因此隔离整个 profile 配置),写 cordis.patch.yml 前自动备份(只留一个滚动 .bak)、写前重新校验
- 📦 零运行时依赖:react / yaml 都用宿主已有的那份,插件本体不带第三方运行时依赖

目录:界面预览 · 安装 · 使用 · 工作原理 · 已知边界 · 更多文档 · License

界面预览

两张图都是用本仓库构建出的客户端 bundle 离线渲染的(同一套 stub 加载器 + 示例数据),不是真机截图——真机截图会把作者自己的服务器地址与项目路径带进 README。
重新生成:node scripts/screenshots.mjs。

MCP 面板 · 全局(配置文件行 + 停用/编辑/删除)· 项目级 · 导入 MCP

展开卡片 · 这一行注册了哪些工具 + 只保留勾选(未勾的淡化,且不会注册给模型)

技能面板 · 与 MCP 页签同一套布局 · 全局在前、按工作区排序 · 预览 / 编辑 / 删除 收在 … 菜单

安装

本插件装进 DSH 的某个 profile($DSH_HOME/profiles/<name>),Host 半在 harness 进程里跑。

设置 → 插件市场(推荐)

打开 设置 → 插件市场,搜 dsh-skills-mcp-manager,点安装,刷新页面即可。

桌面端请走市场或内置插件安装器:desktop 配置档由桌面应用独占,宿主命令行的
dsh plugin --profile desktop 会被直接拒绝。

命令行

npm 包(预构建,安装时不需要编译)
dsh plugin --profile web add @j62268781-alt/dsh-skills-mcp-manager

或 GitHub Release 上的预构建 tarball(链接恒定指向最新版)
dsh plugin --profile web add https://github.com/j62268781-alt/dsh-skills-mcp-manager/releases/latest/download/j62268781-alt-dsh-skills-mcp-manager.tgz

重启 DSH,再刷新页面:设置 → Skills & MCP。

卸载:

dsh plugin --profile web remove @j62268781-alt/dsh-skills-mcp-manager

从源码(开发)

git clone https://github.com/j62268781-alt/dsh-skills-mcp-manager.git
cd dsh-skills-mcp-manager
npm install
npm run build # dist/(Host,tsc)+ lib/client.js(Client,esbuild 单文件)
npm run deploy # 拷贝进 profile;部署前先跑加载冒烟,失败则 profile 一字不动

npm run deploy 的目标是 $DSH_HOME/profiles/desktop/node_modules/@j62268781-alt/dsh-skills-mcp-manager,旧版本备份成同级的 .backup-<时间戳>(回滚只需改名回来)。仓库不提交构建产物,所以克隆后必须先 build。

之后把包名挂进 profile 的 bundles 并重启:

// $DSH_HOME/profiles/desktop/package.json
{ "dsh": { "profile": { "bundles": [ /* … / "@j62268781-alt/dsh-skills-mcp-manager" ] } } }

使用

入口:设置 → Skills & MCP(与内置插件页同一套设置槽位)。

- 搜索只匹配名称,不匹配地址 / 参数 / 环境变量
- 一份列表,没有分区标题:全局的排前面,然后按工作区顺序;卡片上的标签说明它属于谁
- 「添加」按钮与搜索框同一行,表单紧贴列表上方(点完就能看到它要追加到的位置)
- 没有项目筛选器:列表跨工作区展示(全局在前,然后按工作区顺序),卡片上的标签说明归属。
新条目要写到哪个工作区,在添加对话框里选
- 卡片标签统一:全局显示「全局」,项目级显示工作区名 —— MCP 与 Skills 同一套词表
- 卡片操作收进 … 菜单(预览 / 编辑 / 删除);技能没有启用开关——磁盘上有文件就是启用

MCP

| | 全局 MCP | 项目级 MCP |
| 存在哪 | profile cordis.patch.yml 的 Loader 行 | profile cordis.patch.yml 里带 scope 的 Loader 行 |
| 谁去连接 | 官方 @deepseek-ai/dsh-mcp-client 直接加载这些行 | 插件在 agent/created 时按项目路径挂载 |
| 生效范围 | 所有会话(含子代理) | 只有该项目里运行的会话智能体 |
| 增删改 | 写回配置文件(加锁 + 原子写 + 写前校验 + 自动备份) | 同一个行命令路由,写回带 scope 的行 |

- 卡片上的「配置文件」标签表示这一行来自 cordis.patch.yml;这种行可以编辑 / 停用 / 删除,改动通过带鉴权的 POST /dsh-skills-mcp-manager/rows 立即写回(新增/删除在点击瞬间就生效,不等轮询)
- 展开卡片能看传输方式、地址或命令、参数、环境变量(值打码)、范围与启停状态
- 展开卡片的「工具」一栏可以只保留你需要的工具。每个服务器的工具清单下面有三档:
| 档位 | 含义 |
| 全部保留(默认) | 不做任何干预,该服务器列出的工具全部注册 |
| 只保留勾选 | 勾选的才注册,没勾的不会进入工具注册表(也不占 token) |
| 只挡勾选 | 勾选的被挡住,其余照常注册 |

被挡下的工具不只是「不显示」——它们从未注册,所以也不占用每次组装工具面的 JSON schema 体积。
实测本机 chrome-devtools 一个服务器就占 30 个工具、约 25 KB(≈5.8k tokens),只留常用的几个收益很明显。

两点如实说明:
- 改动要等该服务器重新注册工具时才完全生效。规则本身是即时读取的(改完保存即可,不用重启),但已经注册进注册表的工具无法被插件注销,所以已存在的工具不会当场消失。
想立刻看到效果:把这张卡片「停用」再「启用」(几秒即可)——停用会销毁该服务器的桥接、连带注销它的工具,启用时重新连接并重新注册,于是全部工具重新过一遍过滤;或者重启 DSH。
- 过滤靠劫持工具服务的 register 实现——因为 dsh-mcp-client 自身没有白名单字段,而 ctx.tools.restrict() 要求 scoped 上下文(全局调用会抛错)、且只遮罩不注销。
这是唯一能真正把工具挡在注册表外的接缝。装不上时面板会显示原因,而不是假装在工作。
- 停用只是把该行的 enabled 置 false,不删你的配置
- 连接与重连(添加/编辑 MCP 时的最后一组输入,全局与项目级都支持):直接对应 @deepseek-ai/dsh-mcp-client 自己的配置字段
| 表单里填的 | 落到 config 的 | 默认值 | 作用 |
| 连不上时拒绝激活 | failOnStartupError | false | 勾上后:首次连接或工具同步失败会让插件激活失败(不勾则照常启动,只是没有该服务器的工具) |
| 工具调用超时(ms) | toolCallTimeoutMs | 60000 | 单次 tools/call 的超时。导入/分析这类分钟级操作会被默认 60 秒打断(本机实测一次 Ghidra 导入+全量分析约 158 秒),按需调大 |
| 重连首次延迟(ms) | reconnect.initialDelayMs | 500 | 第一次重连等待多久,之后逐次翻倍 |
| 重连最大延迟(ms) | reconnect.maxDelayMs | 30000 | 退避延迟的上限 |
| 重连最大次数 | reconnect.maxAttempts | 10 | 单次断线最多连续尝试几次,超出则放弃 |
输入框预填真实默认值,落库也写成显式值(500 / 30000 / 10):表单显示的就是实际生效的,不用去翻 dsh-mcp-client 的源码才知道这一行怎么重连。清空输入框会回到默认值,而不是删掉键。
failOnStartupError 例外——false 与「键不存在」完全等价,写它只是噪音,所以只在勾选时写入。
填了非法值(非整数、超范围)会被拒绝并提示——因为写进去会让整行加载失败
- 导入 MCP:选来源 → 扫描 → 预览「哪些文件存在、识别出几个、几个形态不支持」→ 确认。全局与项目级都写成配置文件行(项目行带 scope)

技能

- 列表 = 磁盘扫描结果:$DSH_HOME/skills、~/.agents/skills、以及各项目的 .agents/skills / .dsh/skills。磁盘文件是唯一真源,config.skills[] / 对账镜像已删除(旧条目会在升级时一次性写成文件)
- 「添加技能」写入 <根>/<名字>/SKILL.md:区域选全局或项目,名字同时是目录名
- 预览/编辑按需通过 GET /<name>/skill?path= 读取正文(只允许读扫描到的文件;正文不进 profile patch)
- 重命名会同时移动目录并改写 frontmatter 里的 name(否则面板和文件夹会对不上)
- 删除是两步确认:先把目录备份到同级的 .smp-backup-<名字>-<毫秒>,再删除;备份会保留,需要时手动改回原名即可

状态与失败

在途操作在卡片上显示进度(保存中… / 删除中… / 切换中…),期间该卡片的其它按钮不可点;操作失败会就地显示原因(例如「该技能不在当前扫描结果里」)。删除若 15 秒没有回执,会停止转圈并提示重试。

工作原理

插件分两半,装在同一个包里:

src/* Host 半(tsc → dist/index.js) 跑在 harness 进程 lib/src/** Client 半(esbuild → lib/client.js) 跑在浏览器,单文件、CSS 内联 cordis.patch.yml 声明插件行(本包自己的 bundle patch)

写通道:所有修改都是 HTTP 请求/响应,没有 settings 队列,也没有持久化的命令字段。

| 路由 | 作用 |
| POST /dsh-skills-mcp-manager/rows | MCP 行增删改;Host 加锁、校验、原子写 cordis.patch.yml,并在响应里带回写后的行列表 |
| POST /dsh-skills-mcp-manager/skills | 技能增删改;直接写/删 <根>/<名字>/SKILL.md |
| GET /dsh-skills-mcp-manager/skill?path= | 读取一个已被扫描到的 SKILL.md 正文(路径必须来自扫描结果) |
| POST /dsh-skills-mcp-manager/reconnect | 重连一台服务器;请求返回时命令已经跑完 |
| POST /dsh-skills-mcp-manager/import | 扫描其它工具的 MCP 配置,直接返回预览 |

写路由先过 connection.requestRejection() 鉴权,只接受 POST + application/json,请求串行执行。为什么不用 settings:写一个文件只要 1ms,而队列通道要连等四个轮询(600ms 取请求 + 面板回读 + 2s 对账 + 5s 投影),实测最坏约 9 秒。旧 Host / 旧面板不再兼容,请一起升级。

数据通道:GET /dsh-skills-mcp-manager/runtime 一次返回整个读模型:MCP 行列表(Host 直接读 patch 文件,Loader 只补 phase)、工具清单(现读工具注册表)、连接/授权状态(探测缓存 + 重连预算)、磁盘技能扫描、工作区与过滤状态。响应禁用缓存;面板在页签打开/展开卡片/页面恢复可见时读取,可见期间每 5 秒刷新,写入成功后立刻重拉一次。读取失败会显示原因并保留上一次的行列表,只把观测状态标成「状态未知」。

命名与升级:插件行 id、settings 命名空间和运行时接口前缀统一为 dsh-skills-mcp-manager。升级前请读 docs/MIGRATION-0.5.6.md —— 0.5.6 删掉了 settings 里的技能、命令与读模型字段:config.skills[] 迁移成 SKILL.md,config.servers[] 迁移成带 scope 的 Loader 行,其余键删除。迁移全部自动完成,但需要重启一次 DSH。

配置持久化:插件的 settings 里只剩 toolFilters(用户偏好,改过滤规则时才写)。技能、MCP 行、工具清单、连接/授权状态、命令都不再进 settings;启动时用配置文件锁、备份和原子写一次性清掉旧字段,之后扫描时间变化也不会再改写 cordis.patch.yml。

技能:面板写的、导入的、手工放进根目录的,全部只以磁盘文件为准;Host 每 5 秒扫描一次四个根供 /runtime 使用,不做任何镜像或回收。升级时 config.skills[] 的旧条目会写成 SKILL.md(已存在的文件优先,不覆盖)。
项目级 MCP:不猜项目根——判定依据是「配置里的 scope 是否为会话 cwd 的目录前缀」,命中的 agent 才挂载,agent/disposed 时卸载。注意 subagent 是独立 agent,只继承 root 作用域,所以项目级工具对 subagent 不可见(全局工具两处都可见)。

全局 MCP:对 cordis.patch.yml 的写入照抄官方 config-editor 的做法——同一把文件锁、原子写、写前重新 parseDocument 校验、每次改写前自动备份,块外的用户内容逐字节保留(注释、!!js 表达式都不会被写坏)。

备份只留一个文件:改写前把当前内容写到 $DSH_HOME/dsh-skills-mcp-manager.backups/cordis.patch.yml.bak,每次覆盖。它保存的是「当前状态之前的那一份」,也就是撤销上一次操作——这也是它唯一的用途,所以不留带时间戳的历史副本(早先那样做,一天就堆了 135 个文件、其中 130 个是逐字节重复)。
需要长期历史请用版本控制。

安全边界:DSH 的 sanitizeProfile 会在插件加载或通道处理抛出未捕获异常时,把用户整份 cordis.patch.yml 改名隔离并禁用其它插件。所以本插件的每个通道处理都包了双层 try/catch:宁可回一条失败原因,也不让异常逃出去。

工具过滤:dsh-mcp-client 会把每个连接到的服务器列出的全部工具无条件注册,而它的配置里没有任何白名单字段(stdio / streamable-http 两种 schema 都没有),ctx.tools.restrict() 也不能替代——它要求 scoped 上下文(全局调用会抛错),而且只遮罩定义、定义仍留在注册表里可被内省。唯一能把工具挡在注册表外的接缝是 register 本身,所以面板在 apply() 里劫持工具服务实例上的 register:

- 打在实例上而不是原型:按名字查找的调用方(syncTools 正是如此)只看得见实例上的写入;原型属于恰好被导入的那份包副本。
- 顺序上正好占优:本插件是 bundle 层,而 MCP 行在 profile 的 cordis.patch.yml(用户补丁层),profile 组装时先应用 bundle 补丁、再应用用户补丁——所以劫持一定在 MCP 客户端注册第一批工具之前装好,且这个顺序不依赖用户手工排行。真机上 filterState.installed === true 且 preExisting 为空,证实了这一点。
- fail-open:判不了名字就放行。过滤器自身的 bug 不该表现成「工具凭空消失」。Reflect.set 在不可写属性上会返回 false 且可能静默失败,所以写入结果是被验证的——否则会出现「报告安装成功、实际一个都没过滤」这种最坏情况。
- 规则每次注册时实时读取,所以改完保存即对新注册生效;未配置的服务器与非 MCP 的内置工具一律放行;归属按最长前缀判定(github 与 github__enterprise 并存时不会挂错)。

已知边界

- 面板列出磁盘上的技能(其它工具放进去的也在),但只对其中「本插件能定位到的根」执行写操作
- 项目级 MCP 对 subagent 不可见:subagent 只继承 root 作用域;需要所有 agent 都用就配成全局
- 工具过滤对「已注册」的工具无能为力:插件只能在注册那一刻放行或拦下,无法注销别的插件已经注册的工具。所以刚开启的档位要等该服务器重新注册工具才完全见效——把该卡片「停用」再「启用」即可立即触发,或重启 DSH
- 工具过滤依赖工具服务的 register 可被写入:若某个版本的 DSH 把它设成不可写,面板会显示「过滤未生效」并给出原因,而不是静默失效
- 过滤看的是公开工具名:MCP 客户端把工具注册为 mcp__<serverName>__<原始名>,名字过长时会规范化并加哈希后缀——勾选界面里的短名就是按这个公开名切的,与服务器端的原始名可能不同
- stdio MCP 首次启动要等:npx 首次下载包约 1–2 分钟,这期间挂载已建立但工具还没注册
- MCP 连不上之后不会永远重试:dsh-mcp-client 的重试预算用完就永久放弃,只能重连或重启。
默认预算是 151.5 秒(initialDelayMs: 500、maxDelayMs: 30000、maxAttempts: 10 的退避之和)。想让它长时间自己等,就把「重连最大次数」设大(如 10000)、「重连最大延迟」设 60000;不要勾「连不上时拒绝激活」——勾上等于一次都不重试(首次尝试失败即让插件实例失败并清掉重试定时器)。面板上的状态标签与「重连」按钮就是为这件事准备的
- 连接状态里的时间是推导值:dsh-mcp-client 不对外暴露重试计数(failedAttempts 在闭包里),所以「重试中 / 已放弃」是按该行配置算出的预算与已耗时推出来的,界面写「预计」
- 需要 OAuth 的 MCP 服务器会显示「需要授权」,而不是「坏了」:这类服务器在未授权时拒绝连接是协议规定的正常行为。但 dsh-mcp-client 完全没有 OAuth 支持(它不向 MCP SDK 传 authProvider),于是 401 被当成普通网络故障一路重试到放弃,界面上只会说「已放弃重试」——把「你去补一次授权就好」说成了「这台服务器坏了」。面板会用一次轻量 HTTP 探测读 401/403 的 WWW-Authenticate 挑战,把它单独标成「需要授权」(不是红色,因为不是故障)。去授权的办法只能靠凭据:给这一行加一个 Authorization 请求头,或换成服务器提供的带 token 的地址(DSH 不会弹出 OAuth 登录窗——那是客户端缺的能力,不是面板能补的)
- command: npx 走用户自己的 npm 缓存;~/.npm 里若有 root 所有的文件会 EACCES,一次性修复:sudo chown -R $(whoami):staff ~/.npm
- 装/更新之后:Client 半刷新页面即可,Host 半的改动需要重启 DSH(安装器与市场都会提示)

更多文档

| 文档 | 给谁看 |
| CHANGELOG.md | 每个版本改了什么 |
| docs/MIGRATION-0.5.5.md | 从 0.5.4 升级的人:这一版的破坏性变更、自动迁移覆盖了什么、怎么回退 |
| docs/DEVELOPMENT.md | 改这个插件的人:自测命令、诊断文件、故障恢复 |
| docs/PUBLISHING.md | 维护者:npm 发包、GitHub Release、上架插件市场 |
| docs/ICON.md | 改图标的人:插件的绘制规范(画布 / 尺寸 / 配色)与两个易踩的坑 |
| docs/ENGINEERING.md | 开发手记(真机验证记录 / 事故复盘 / 踩坑清单,历史归档) |
| docs/oauth.md | 用到 OAuth 保护的 MCP 服务器的人:怎么点授权、安全边界在哪、失败了怎么排查、怎么在本机验证整条链路 |
| docs/oauth-feasibility.md | 想弄清 OAuth 为什么必须打补丁的人:每条结论的源码出处与实测证据(调研;0.5.6 已实现,见上一条) |
| docs/tools-persistence.md | 工具清单为什么不该落盘:实测体积占比、每 5 秒重写的根因、三条可选路线 |

License

MIT
预览分享卡片长什么样 ↓j62268781-alt/dsh-skills-mcp-manager
分享 · DeepSeek Harness Hub
j62268781-alt/dsh-skills-mcp-manager

DSH 设置面板:一个页面管理技能SKILL.md与 MCP 服务器,全局 / 项目级分层,改完即生效。Manage…

这是你的仓库?认领条目

回到顶部