npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@zfdx123/dsh-mcp-manager

v1.0.13

Published

MCP 服务器管理器:在 DSH 设置页配置 MCP 服务器,宿主侧按配置热挂载/卸载 mcp-client 实例(支持内网自签名证书)

Downloads

2,729

Readme

@zfdx123/dsh-mcp-manager

给 DeepSeek Harness(DSH)加一个「设置 → MCP 服务器」页面:在界面上配置 MCP(Model Context Protocol)服务器,保存即生效——宿主侧自动挂载/卸载 @deepseek-ai/dsh-mcp-client 实例,模型随即拿到或失去 mcp__<名称>__<工具名> 形式的工具。无需再手写 cordis.patch.yml 里的 MCP 行,也无需重启进程。

安装

从 npm 装(dsh plugin 会把这条命令转发给 profile 的 pnpm):

dsh plugin --profile web add @zfdx123/dsh-mcp-manager

一次装齐整套插件用聚合包 @zfdx123/dsh-atelier:

dsh plugin --profile web add @zfdx123/dsh-atelier

dsh plugin add 会把包安装进 profile 的依赖,并因为本包声明了 dsh.bundle,自动把 @zfdx123/dsh-mcp-manager 追加到该 profile 的 dsh.profile.bundles(即应用本包的 cordis.patch.yml 层)。验证组合树:

dsh --profile web --dump-config   # 应能看到 mcp-manager 行

重启 dsh 进程后生效(bundle 层在启动时组合;插件代码改动也需要重启,见「开发」)。重启后刷新浏览器页面,打开 设置 → MCP 服务器 即可。

开发用:本地 link 安装

改代码直接生效,不必发版:

dsh plugin --profile web add link:E:/work/ai/dsh-atelier/packages/dsh-mcp-manager

link: 安装有一个必须知道的副作用(本地 dev 副本会盖住宿主副本),见「已知限制」与「开发」。

卸载

dsh plugin --profile web remove @zfdx123/dsh-mcp-manager

(同时移除 profile 依赖与 bundle 层。)

从旧包名 @fishlikewater/dsh-mcp-manager 升级

本分支从 0.3.x 起就不再沿用上游包名,当前包名是 @zfdx123/dsh-mcp-manager(版本 1.0.0)。若你的 profile 里记的还是旧名,必须重装一次——profile 里记的是旧名的依赖键与 bundle 行,只改包内文件会出现「宿主按旧名装载、包内自称新名」的错配(客户端 bundle 的注册 id 校验失败,设置页会消失):

dsh plugin --profile web remove @fishlikewater/dsh-mcp-manager
dsh plugin --profile web add @zfdx123/dsh-mcp-manager
# 重启 dsh 进程,刷新页面

配置文件(settings.yaml 的 mcp 命名空间)不受影响,重装后服务器列表与开关状态照旧。

快速上手

重启并刷新页面后,打开 设置 → MCP 服务器:列出 / 新增 / 编辑 / 删除服务器,每张卡片上带挂载状态与「开启 / 关闭」按钮。

设置页字段

| 字段 | 传输 | 说明 | |---|---|---| | 名称(serverName) | 两者 | [A-Za-z0-9_-]{1,32},全局唯一,决定工具名前缀 mcp__<名称>__… | | 传输方式 | 两者 | stdio / streamable-http | | 命令(command) | stdio | 可执行文件(支持绝对路径)或 npx 之类命令。只填可执行文件本身 | | 参数(args) | stdio | 每行一个参数;一行里写了多个词会按引号规则自动拆开(含空格的参数用双引号,如 "C:\my dir\a.py") | | 环境变量(env) | stdio | 每行 KEY=VALUE;值支持 env:NAME / cred:NAME 引用 | | 工作目录(cwd) | stdio | 可选 | | URL | http | 如 http://localhost:3000/mcp;内网自签名时用 https://… 并配合下面的 TLS 选项 | | 请求头(headers) | http | 每行 KEY: VALUE;值支持 env:NAME / cred:NAME 引用 | | 工具调用超时(toolCallTimeoutMs) | 两者 | 毫秒,默认 60000 | | 启动失败即报错(failOnStartupError) | 两者 | 是否在首次连接/工具同步失败时报错(默认否,自动重连) | | 断线自动重连(reconnectEnabled) | 两者 | 默认开启(退避重试);关闭后连接断开不再重试 | | 最大重连次数(reconnectMaxAttempts) | 两者 | 默认 10;连续失败达到该次数后放弃(日志与状态会提示需重载插件) | | 允许自签名证书(tlsInsecure) | http | 默认否。开启后只对该服务器的地址跳过证书校验 | | 自定义 CA 文件(tlsCaFile) | http | PEM 绝对路径。填写后用该 CA 严格校验该服务器证书(推荐,比关校验安全) |

保存后立即生效;删除后对应工具立即移除。

删除、关闭与开关

  • 删除与「关闭」都是先确认再执行:确认用外壳原生的 RiskConfirmation(弹窗 + 必须先勾选「我已了解…」,勾上之前确认动作一直不可用),拿不到这套原生组件的版本退回卡片里的内联确认条(按钮配色与行为不变)。「开启」无副作用,不问。
  • 每张服务器卡片上的「关闭 / 开启」按钮会立即停止 / 启动对应实例:关闭后工具马上移除、不再重连;开启后重新挂载并注册工具。
  • 开关状态(enabled,默认 true)随配置一起持久化在 settings.yaml,重启 dsh 后保持;只影响被切换的那一台,其余服务器不受影响。

示例

examples/mcp-servers.example.yaml 给出了 settings.yaml 中 mcp 命名空间的示例(stdio、streamable-http、内网自签名 HTTPS 各一)。可以直接把它合并进 $DSH_HOME/settings.yaml 作为初始配置,再从设置页调整。

它做什么

  • 设置页管理:列出 / 新增 / 编辑 / 删除 MCP 服务器,带挂载状态。
  • 每台服务器可单独开启/关闭(enabled 开关):关闭立即停止实例并移除工具,开启重新挂载并注册工具,状态持久化。
  • 两种传输:stdio(本地命令)与 streamable-http(远程 URL)。
  • 内网自签名证书可用:每台 streamable-http 服务器可开 tlsInsecure(只对该服务器地址跳过证书校验)或指定 tlsCaFile(用指定 CA 严格校验);其余出站请求(含模型 API)的证书校验完全不变。
  • 失败原因看得见:连接失败不再只显示「连接失败,重试中…」——插件把 fetch failed 背后的证书/网络原因展开并给出可操作提示(例如「证书不受信任:开启允许自签名证书或填写 CA 文件」)。
  • 真实挂载状态:连接失败、重连中、重连成功、放弃重连等异步事件实时反映到设置页(截获 mcp-client 日志驱动),不再只显示乐观的「已挂载」。
  • 不亮没有证据的绿:ctx.plugin() 返回只说明实例建好了,不说明连上了——而 mcp-client 只在失败时打日志,连接成功不打任何日志。所以挂载完成后先显示「连接中」,只有拿到正向证据才转「已挂载」:工具注册(mcp__<名>__<工具> 出现,管理器给注入的 tools 服务包了一层只转发不改行为的 register 观察器)或成功日志(重连成功 / 重新同步工具)。确认窗口内(HTTP 默认 20 秒、stdio 5 秒)仍无任何证据就转「错误」并说明「服务器没有回应、也没有注册任何工具」。

    网络超时本来就有反馈,不用等确认窗口。 实测黑洞地址(192.0.2.1,包被丢弃):undici 的 connect timeout 是 10 秒,之后 mcp-client 自己就会打出 connection attempt failed: … fetch failed ← UND_ERR_CONNECT_TIMEOUT Connect Timeout Error,插件把它翻成带原因链与可操作提示的错误状态。所以确认窗口(20 秒)刻意设得比它长:那 10 秒里卡片显示「连接中」而不是绿,10 秒后显示的是这条真实原因,而不是窗口那句更含糊的「未确认」。窗口只兜住「连不上也不报错」的路径。

  • 并发保存保护:GET/POST 携带 rev 修订号(乐观锁),配置被其他窗口改过时返回 409 并提示刷新,避免静默覆盖。
  • 密钥不入盘:env / headers 的值支持 env:NAME(进程环境变量)与 cred:NAME(DSH 凭据)引用,挂载时解析,settings.yaml 只存引用。
  • 配置持久化在 settings.yaml 的 mcp 命名空间(mcp.servers),改动热生效。
  • 宿主侧按配置动态挂载 @deepseek-ai/dsh-mcp-client 实例(断线自动重连、工具自动注册/注销由它负责)。
  • HTTP 接口不裸奔:优先注册在 DSH 0.1.5 起提供的 /api 共享通道精确 Fetch 路由上(宿主施加 Host/Origin 信任栅栏 + 浏览器鉴权),拿不到 connection 服务时才退化为裸 webServer 路由并自行施加同样的判定(退到只允许回环来源)。
  • headless profile 也能用:没有 webServer / connection 服务时跳过 HTTP 接口,只做实例管理。

内网自签名证书(HTTPS MCP 服务器)

典型的失败长相是设置页一直「连接失败,重试中…」,日志里只有一句 TypeError: fetch failed——真正的原因是 Node 默认校验证书时对自签名证书抛 DEPTH_ZERO_SELF_SIGNED_CERT。两种解法:

  1. 推荐:把服务器证书(或签发它的 CA)导出成 PEM,填进 tlsCaFile。证书仍然校验,只是多信任这一个 CA。
  2. 临时/应急:把 tlsInsecure 设为「是」。此时只对这台服务器的 origin 关闭校验,其它请求(模型 API、web 搜索、其它服务器)不受影响。

两种方式都由宿主侧按 origin 定向实施,并且:

  • 策略随服务器挂载生效、随卸载/删除释放;全部释放后 globalThis.fetch 还原成原函数(可逆,不会长期改变进程行为);
  • 因为 Node 内置 fetch 只接受内置 undici 的 dispatcher,定向分流走的是 npm undici 包自己的 fetch(本包已声明依赖);undici 不可用时挂载直接报错,不会静默降级成「连不上但没原因」;
  • CA 文件读不到、undici 缺失等失败都会写进日志并在设置页状态里显示。

也可以用系统级信任链代替:NODE_EXTRA_CA_CERTS=<pem> 或 node --use-system-ca 启动 dsh(需重启进程)。那样就不必逐台配置。

连不上时先跑诊断工具,它会分别打印 TLS 握手、证书内容和一次真实 initialize 的结果,用来区分「网络不通 / 证书不受信任 / 服务端拒绝」:

node scripts/probe-mcp-endpoint.mjs https://10.170.17.55:8091/mcp 你的token

stdio 服务器填错了会怎样

常见误用是把整条命令行粘进「命令」栏(...\python ...\server.py 一行),参数栏里又一行写多个词。这会让 spawn 的 executable 变成「带空格的一整串」→ ENOENT,而 MCP 客户端只报 McpError: MCP error -32000: Connection closed,完全看不出原因。插件对此做了两件事:

  1. 自动归一化:command 里混进来的参数、args 每行里的多个 token 都会按引号规则拆开(不解释反斜杠转义,Windows 路径安全;C:\Program Files\...\node.exe 这种真实存在的带空格路径不会被误拆,也可用引号显式声明为一个整体)。每次改写都会写一条宿主告警。

  2. 启动前置检查:可执行文件找不到、脚本文件(绝对路径)不存在、工作目录不存在、command 里仍含多个 token 时,设置页状态直接给出可照改的一句话,例如 启动前置检查未通过:找不到可执行文件:...(路径是否正确?或它在 PATH 里吗?)

    这句话是持久的主文案,不会被随后到来的真实异步失败顶掉(失败只作为第二行细节并入,例如 连接失败:SdkError: Connection closed)——你随时打开设置页看到的都是那句能照着改的话,同时看得到真实错误。唯一的例外是服务器真的连上了(重连成功):那说明这次启发式判断错了,判定会被清掉,不会对着一个已经可用的服务器一直报错。

    前置检查只是保守的启发式(只覆盖能静态判断、且几乎必然导致启动失败的情况),所以它不会阻止挂载:万一判断错了,实例照常挂载并继续重连。

Windows 上 venv 的可执行文件是 Scripts\python.exe;写 Scripts\python 也能用(Windows 会补 .exe,插件同样识别)。

密钥不入盘(env: / cred: 引用)

密钥引用(env:NAME / cred:NAME)在挂载时解析:env: 读进程环境变量;cred: 经 DSH 的 credentials 服务解析(resolve),服务不可用或未配置时保留字面值并打日志。settings.yaml 里始终只存引用文本。

HTTP 接口(供设置页使用)

| 方法 | 路径 | 说明 | |---|---|---| | GET | /api/mcp/servers | 返回 { rev, servers, status };rev 是命名空间修订号(乐观锁),status 是 serverName -> { state: 'ok'\|'error'\|'disabled', message, detail, preflight?, failure? }。message 是主文案(前置检查的判定优先),failure 是并入的异步失败细节(仅错误态),preflight 是那条判定的原文(存在即表示主文案受它保护) | | POST | /api/mcp/servers | 请求体 { rev?, servers: [...] },整体替换并触发重挂载;rev 与当前修订不符返回 409 { code: 'conflict', rev };校验失败返回 400 { error } |

接口的注册方式(DSH 0.1.5 起的兼容点):

  • 插件优先用 ctx.connection.fetch.register({ path: '/api/mcp/servers', methods: ['GET','HEAD','POST'], requestBody: 'buffered', fetch }) 注册到 /api 共享通道的精确 Fetch 路由上。物理载体会先做 Host/Origin 信任栅栏与浏览器鉴权(connection.requestRejection),未鉴权的请求在到达插件之前就被 401/403 挡下。
  • 只有在拿不到 connection 服务时(旧版 DSH 或非 Web 组装),才退回裸 webServer.register,并由插件自己调用 connection.requestRejection;连它也没有时只接受本机回环来源。
  • 这条约束很重要:该接口能写入任意 command,而宿主会立即执行它。裸注册 + 无鉴权 = 未授权的任意命令执行(dsh web --host 0.0.0.0 场景下尤其致命)。

配置

配置持久化在 settings.yaml 的 mcp 命名空间(mcp.servers,数组),设置页保存即写入,热生效。文件里的结构与各键默认值:

mcp:
  servers:
    - serverName: my-stdio-server
      enabled: true
      transport: stdio            # stdio | streamable-http
      command: npx
      args: ['-y', '@some/mcp-server']
      env:
        SOME_TOKEN: 'env:SOME_TOKEN'   # env:NAME / cred:NAME 引用,密钥不入盘
      cwd: ''
      url: ''
      headers: {}
      toolCallTimeoutMs: 60000
      failOnStartupError: false
      reconnectEnabled: true
      reconnectMaxAttempts: 10
      tlsInsecure: false          # 仅 streamable-http
      tlsCaFile: ''               # 仅 streamable-http,PEM 绝对路径(推荐)

| 键 | 默认 | 说明 | |---|---|---| | serverName | —(必填) | 唯一名,决定工具名前缀 mcp__<名称>__… | | enabled | true | 关闭后实例停止、工具移除,但配置保留 | | transport | stdio | stdio / streamable-http | | command / args / env / cwd | '' / [] / {} / '' | stdio 传输的连接参数 | | url / headers / tlsInsecure / tlsCaFile | '' / {} / false / '' | streamable-http 传输的连接参数 | | toolCallTimeoutMs | 60000 | 工具调用超时 | | failOnStartupError | false | 首次连接/工具同步失败是否直接报错(默认自动重连) | | reconnectEnabled / reconnectMaxAttempts | true / 10 | 断线重连;只写改过的子键,其余交回 mcp-client 默认值 |

完整示例(含内网自签名 HTTPS 的两种解法)见 examples/mcp-servers.example.yaml。

前置要求

| 组件 | 版本要求 | |---|---| | Node | ^22.19.0 \|\| >=24.0.0(undici 8.x 的下限是 22.19.0;@deepseek-ai/dsh-mcp-client 依赖 Promise.withResolvers) | | DSH | ^0.2.1-alpha.2(engines.dsh 与 peer 范围。本包在 1.0.5 那一版按 0.1.7-rc.2 的设置 API 重写过;1.0.13 起收窄到这一条,已实测;0.1.7-rc.2 / 0.2.0-rc.* 与更早的 0.1.5 / 0.1.6-alpha.* 都只是历史校对记录) | | @deepseek-ai/dsh-mcp-client | ^0.2.1-alpha.2(peerDependency,由运行时提供;reconnect 配置从 0.1.5 起可选,maxInstructionBytes 从 0.1.6 起可选、默认 32768) | | @deepseek-ai/dsh-settings | ^0.2.1-alpha.2(peerDependency,可选:ctx.settings 由宿主提供) | | @deepseek-ai/cordis | ~4.0.5-alpha.1(peerDependency;照抄 DSH 0.2.1-alpha.2 自己的声明) | | dsh CLI + pnpm | dsh plugin 命令转发给 pnpm;本包按 profile 的 pnpm 布局安装 |

普通依赖:@deepseek-ai/schemastery(配置 schema)与 undici(^7.0.0 || ^8.0.0,只用于按 origin 定制 TLS dispatcher)。客户端界面用外壳自带原生组件(@deepseek-ai/dsh-client-ui-primitives),拿不到时逐处降级,不会白屏。

已知限制

  • 日志文案契约是「软」的:设置页的真实挂载状态靠截获 mcp-client 的日志文案驱动,规则表以「前缀 + 关键词」为键。上游一旦改写文案,翻译就会落空、状态显示失真(历史上就发生过:failed generation did not close within … 被改写成 failed generation could not confirm transport closure …,导致「永久停止重连」的终态仍显示「已挂载」)。现在有三道防线:未识别的 error/warn 一律透传原文(未知 ≠ 看起来正常)、test/log-contract.test.js 扫描上游源码文案(改词即变红)、确认窗口——挂载后没有正向证据就不亮绿,所以文案漂移最多让状态停在「连接中 / 未确认」,不会退回「乐观的已挂载」。(1.0.10 起只声明 0.2.0-rc.1,规则表里就只留现行措辞了:那条更早的 rc.8 措辞已删,但它仍会走透传兜底,照旧报红带原文——见 test/host-logic.test.js 里那条「已移除的旧措辞仍然报错」。)
  • 确认窗口是启发式,不是握手:它靠「工具注册」这一个正向信号 + 超时兜底。一台连得上但确实没有工具的 MCP 服务器会在窗口结束后被标成「未确认连上」(它没连上的样子与没回应无法区分,因为上游成功时不打日志)。若上游哪天补上一条连接成功的日志,规则表加一条即可把这种情况收干净。
  • state: 'connecting' 是新增的第四种状态:外壳没有 warning 态令牌,所以它画成中性灰点 + outline 标签;老版本客户端拿到未知状态会显示「已挂载」,升级客户端后才是「连接中」。
  • ns → namespace 风险:修订号(乐观锁)依赖 settings 描述符的 ns 字段,而 dsh-settings 的 README 已把 ns→namespace 列为 TODO。字段一旦改名,currentRev() 会退回 0,表现为设置页每次保存都 409「已被其他窗口/页面修改」,保存链路整体不可用。现在用 settings/document-updated (ns, revision) 的推送值兜底(事件参数是位置参数,不受字段改名影响),但这条依赖仍在。
  • HTTP 接口依赖载体:需要 connection(首选)或 webServer 服务;headless 组装下两者都没有,此时没有 HTTP 接口,只剩实例管理(设置页也不存在)。
  • 安全边界不可放宽:接口能写入任意 command 并由宿主立即执行,所以必须走宿主鉴权/信任栅栏;插件退到裸 webServer 路由时会自行施加同样判定,最终退化为只允许本机回环来源。
  • 请求体上限 1 MiB:超过返回 413(BODY_TOO_LARGE);上限判定在两条载体上统一,边界值不误杀。
  • 导航图标不能自定义:设置页导航那一行用外壳默认图标——外壳按 section id 硬编码图标且没有第三方注册点(0.2.x 那套 CSS 覆盖依赖外壳类名哈希 VOzbGW_*,DSH 0.1.5 的外壳已不输出这些类名,遂删除)。
  • link: 安装时本地 dev 副本会盖住宿主副本:profile 用 link: 装本包时,Node 按真实路径解析依赖,插件 import '@deepseek-ai/dsh-mcp-client' 拿到的是本仓库 node_modules 里的那份(可能远旧于宿主实际加载的版本),而不是宿主 profile 里的那份。后果:本地跑得很正常,线上(正常的 npm 安装,peer 由宿主提供)才暴露版本差异。缓解办法见「开发」。
  • 工具注册观察器会就地包装注入进来的 tools 服务:包装只转发 register(原样返回 disposer)并在本插件 fiber 卸载时还原,但它是对宿主服务对象的一次写操作——若将来 tools 变成冻结对象或 Cordis 改用别的方式派发服务,这一步会装不上。装不上时不影响正确性:只损失「提前转绿」,确认窗口的超时判定照旧。

开发

结构

├── package.json       # dsh.bundle.patch + dsh.client 双 manifest
├── cordis.patch.yml   # bundle 层:插入 mcp-manager 行
├── index.js           # 宿主侧:settings 注册 + 实例挂载 + HTTP 接口注册(两套载体)
├── lib/logic.js       # 纯逻辑:settings schema、校验、配置构造、指纹、日志翻译、密钥引用
├── lib/api.js         # 纯逻辑:/api/mcp/servers 的请求处理(与载体无关)+ 回环判定
├── lib/tls.js         # 每服务器 TLS 策略 + 定向 fetch 分流 + 失败原因展开
├── client.js          # 客户端 bundle(手写 CJS factory,经 ./client 导出)
├── test/              # node:test 套件(见下)
├── fixtures/          # E2E 用的极简 MCP 服务器(stdio / 自签名 HTTPS),不被打包、不被当作测试执行
├── scripts/           # pack 内容白名单校验、MCP 端点诊断工具
└── examples/

注意

  • 改代码后必须重启 dsh 进程:Node 的 ESM 模块缓存不会随文件变化失效。配置数据不需要重启(设置页保存即热生效)。

  • client.js 是给浏览器模块加载器的 bundle(window.__ModuleLoader__.load 格式),直接手写维护,不需要打包器。 注册 id 必须是包名(@zfdx123/dsh-mcp-manager):宿主按 __DSH_BOOT__ 图里的 entry id(= 包名)装载 /plugins/??<包名>/client.js,装载后会校验 __ModuleLoader__ 里存在该 id,写短名会在挂载阶段直接抛错、设置页永远不出现(test/client-logic.test.js 有断言守着这条)。

  • 改包名时必须同步四处(漏一处就会出问题):package.json 的 name、cordis.patch.yml 的 name(bundle 行)、client.js 里 __ModuleLoader__.load 的 id(必须等于包名)、以及每个已安装 profile 的依赖键与 dsh.profile.bundles 列表(重新 dsh plugin remove/add 即可)。本分支用 @zfdx123/ scope 是因为无 scope 的 dsh-mcp-manager 已被他人占用。

  • 表单/配色分两层:布局与几何走 React 内联样式,颜色走插件自己的 --mcp-* 变量(优先取外壳的 --dsw-alias-*,令牌缺失时回退到内置亮/暗色)。插件会注入一小段自有样式(<style id="dsh-mcp-manager-style">,随 fiber 卸载移除),只做内联样式表达不了的事:按 body[data-ds-dark-theme] 绑定 color-scheme、给原生控件与每个 <option> 显式写死背景/文字色、占位符与禁用态定色。外壳自己不声明 color-scheme,暗色主题下 <select> 下拉弹层 / 数字微调按钮 / 滚动条会退回亮色渲染,浅色文字落在白底弹层上就是「文字与背景同色看不见」。这段样式的选择器只命中本插件自己的 .dsh-mcp-manager 容器,不依赖任何外壳类名。

  • 交互件优先用外壳原生组件(@deepseek-ai/dsh-client-ui-primitives 挂在基线模块表里,直接 require,不需要在 dsh.client 声明 external/inject):Button / Input / Switch / Tag / StateDot、删除与关闭的确认框 RiskConfirmation,以及图标。取用一律守卫式(loadPrimitives() 形状校验;单个组件/图标缺失只降级那一处,不把整套原语扔掉),拿不到就退回自带元素与文字字形,绝不白屏。所以「添加服务器」的加号有两条路:有 IconPlusOutline16 时走 Button 的 icon 槽(图标不进 children,无障碍名与文案不被图标污染),文案用字典里不带字形的 addServerNoIcon;没有图标时用 addServer(值自带全角 +),一个字都不改。确认框的勾选项与关闭按钮文案同样归 zh/en 字典(confirmDeleteAck / confirmDisableAck / close)。

  • link: 安装下必做的一件事:用宿主真实加载的副本再跑一遍日志文案契约(因为本地 node_modules 的那份可能不是宿主用的那份):

    # 用宿主真实加载的副本再跑一遍日志文案契约(link 安装下必做)
    DSH_MCP_CLIENT_SRC="C:/Users/<you>/.dsh/profiles/node_modules/@deepseek-ai/dsh-mcp-client/lib/index.js" node --test test/log-contract.test.js
    # 逐条打印文案 → 状态翻译结果
    MCP_LOG_CONTRACT_PRINT=1 node test/log-contract.test.js

    同时把 devDependencies 的 @deepseek-ai/dsh-mcp-client 钉在我们声明的宿主版本(^0.2.1-alpha.2),让本地测试与线上同源——测试跑的就是受支持的版本。

兼容性

| 组件 | 版本要求 | |---|---| | Node | ^22.19.0 \|\| >=24.0.0 | | DSH | 0.2.1-alpha.2(1.0.13 起只声明 ^0.2.1-alpha.2 这一条,已实测) | | @deepseek-ai/dsh-mcp-client | ^0.2.1-alpha.2(peerDependency;与 0.1.7-rc.2 起的各版源码逐字节相同,见下) | | @deepseek-ai/dsh-settings | ^0.2.1-alpha.2(peerDependency;ctx.settings 由宿主提供) | | @deepseek-ai/cordis | ~4.0.5-alpha.1 |

为什么只声明 0.2.1-alpha.2

本包声明只支持一个运行时,理由是这个运行时是唯一被实测过的版本,而 semver 的 prerelease 规则让「同时声明几个元组」必须写成 a || b 而不是一条 caret——多声明的每一版都得自己跑一遍核对。

0.2.1-alpha.2 这一轮,本包用到的一方包源码逐字节相同:dsh-mcp-client、dsh-settings (lib/index.js 22630B,rc.2 与 alpha.1 完全一致,test/log-contract.test.js 扫描的就是它)。 真正变了的是外围:cordis 4.0.4 → 4.0.5-alpha.1、schemastery 3.18.4 → 3.18.5-alpha.1 (后者逐字节相同,只差版本号),以及 dsh-invariants 整个包被上游删除(我们从不引用它)。 cordis 这条尤其要紧:0.2.1-alpha.2 的每个 @deepseek-ai/dsh-* 都 peer 了 ~4.0.5-alpha.1,所以我们的 peer 范围再写 ^4.0.4 就不是「宽松」而是装不上—— 严格 semver 下 ^4.0.4 不接纳 4.0.5-alpha.1,npm install 直接 ERESOLVE(实测踩过, 现在由 release-check 用 SUPPORTED_CORDIS 拦下)。

  • 安装期 peer 兼容性闸门在 dsh-app-boot:semver.satisfies(runtimeVersion, peerRange, { includePrerelease: true }) 逐个检查 @deepseek-ai/dsh* 的 peer 范围,不满足就直接拒绝安装 (Plugin … is incompatible with dsh …,并给出 allow-version 逃生口)。注意这个判定比严格 semver 宽:satisfies('0.2.1-alpha.2', '^0.2.0-rc.1', { includePrerelease: true }) === true, 而 npm 解析用的严格 semver 是 false。所以「只支持一版」是我们把范围收窄成恰好一条的结果, 不是闸门替我们挡住的;反过来,范围写错了(如把 cordis 留在 ^4.0.4)装都装不上。
  • 宿主的根 Include 会把裸包名(非 ./cordis: 开头)用宿主自己的安装位置解析 (dsh-app-boot 的 HostResolvedRootInclude),所以插件运行时拿到的始终是宿主的 dsh-* 副本。peer 范围因此纯粹是兼容性契约,写宽写窄直接决定能不能装、以及在哪个 版本上被验证过。

核对方式(可复现):用隔离的 DSH_HOME 把本包(file: 规格,即工作区源码)装进目标版本的 web profile,7 个包全部安装成功、--dump-config 里 7 行齐备、真启动到 dsh web: http://127.0.0.1:<port>/ 且无插件装载报错;本包的 devDependencies 钉在最新受支持版 (0.2.1-alpha.2),所以 npm test 跑的就是它(含全部单测与 E2E)。 这套流程在 0.2.0-rc.1 / 0.2.0-rc.2 / 0.2.1-alpha.1 / 0.2.1-alpha.2 上各跑过一次,并配了闸门阳性对照 (故意把一条 peer 打回旧版,确认安装确实被拒——否则无法区分「闸门放行」与「闸门没跑」)。

历史修复记录(当前版本 1.0.13)

本包版本已到 1.0.13;下面两张表是历史版本的修复记录,保留下来是因为它们记录了「代码为什么长这样」(表内的版本与 peer 范围是当时的,不是当前声明)。表里引用的 0.1.5 / 0.1.6-alpha.x 都是 DSH 侧(或 mcp-client 侧)的版本号,不是本包的版本。

0.3.4(历史):按 DSH 0.1.6-alpha.1 的核对结果做的修复

| 问题 | 影响 | 修法 | |---|---|---| | 日志文案漂移:0.1.6-alpha.1 把 failed generation did not close within … 改写成 failed generation could not confirm transport closure …(拆卸期文案同样被改写) | 规则以整句英文文案为键 → 匹配落空 → settleFailedGeneration 的终态(永久停止重连)在设置页不呈现,仍显示「已挂载」 | 规则表改为「前缀 + 关键词」,覆盖新旧两种措辞;未识别的 error/warn 一律透传原文(未知 ≠ 看起来正常);新增 test/log-contract.test.js 扫描上游源码文案,改词即 CI 变红 | | 1 MiB 请求体上限只对一条载体生效:connection.fetch 路径直接 request.text(),实际上限是宿主的 300MB | 体积契约形同虚设 | 上限判定下沉到与载体无关的 lib/api.js:两条路径统一返回 413(BODY_TOO_LARGE),边界值不误杀 | | HEAD 未注册:处理器实现了 HEAD,路由只注册 GET/POST | 已鉴权的 HEAD 落到 404 | methods: ['GET','HEAD','POST'](与宿主允许的枚举一致) | | apply 阶段同步取 connection:服务尚未激活时静默降级到裸 webServer 路由且不留痕迹 | 装配顺序一变就换载体,行为差异不可观测 | 仍在 apply 时优先用 Fetch 路由;取不到时先挂裸路由,再 ctx.inject(['connection'], …) 等它出现后升级并撤掉裸路由(不把 connection 放进顶层 inject,否则 TUI/headless 组装会加载失败) | | 修订号依赖描述符字段名:describe() 的 ns 字段一旦被上游改名(dsh-settings README 已把 ns→namespace 列为 TODO),currentRev() 退回 0 | 设置页每次保存都 409「已被其他窗口/页面修改」,保存链路整体不可用 | settings/document-updated (ns, revision) 的推送值兜底(事件参数是位置参数,不受字段改名影响) |

0.3.0(历史):针对 DSH 0.1.5 对齐的几处

都是按运行时安装的实际包逐条核对,不是猜的:

  1. 客户端 bundle 注册 id:必须是包名(@zfdx123/dsh-mcp-manager)。宿主装完 /plugins/??<包名>/client.js 后会校验该 id 的 factory 是否注册过,写短名会抛 bundle ... loaded without registering "<id>",设置页永远不出现。
  2. HTTP 接口载体:0.1.5 的 /api 由 connection 插件以 prefix 路由独占,并施加 Host/Origin 信任栅栏 + 浏览器鉴权。插件改用 connection.fetch.register 注册精确 Fetch 路由(宿主负责鉴权),不再裸挂 /api/...。
  3. reconnect 配置:0.1.5 的 mcp-client Config 新增可选项 reconnect: { enabled, initialDelayMs, maxDelayMs, maxAttempts };本插件按需输出该键(只写改过的子键,其余交回 mcp-client 默认值)。
  4. 导航图标覆盖移除:外壳不再输出 VOzbGW_navList/navCell/navIcon,CSS 覆盖在任何版本上都不会命中,已删除(连带删除注入 <style> 与自检代码)。
  5. 同源实例隔离:mcp-client 用 activeServerNames 按 scope 保留 serverName,卸载/重挂同名实例必须等旧实例 dispose() 完成,否则报 already in use——本插件的挂载路径保持串行等待。

测试(兼容性)

npm test(node:test,无需额外框架):

  • test/host-logic.test.js:settings 命名空间 schema、HTTP 校验、mcp-client 配置构造(含 reconnect)、配置指纹、mcp-client 日志 → 状态翻译(用真实日志文案做夹具)、密钥引用解析
  • test/api.test.js:/api/mcp/servers 的纯逻辑(GET/POST、rev 409、400 校验、405、回环判定、请求体上限 413 与边界值)
  • test/log-contract.test.js:日志文案契约快照——从实际安装的 @deepseek-ai/dsh-mcp-client 源码抽出全部 ctx.logger.* 文案,逐条过一遍状态翻译,要求每条 error/warn 都被规则表翻译(而不是落到"未识别透传"兜底);上游改词 → 这条先红。可用 DSH_MCP_CLIENT_SRC 指向宿主真实加载的副本
  • test/tls.test.js:TLS 策略提取、定向 fetch 分流(只有登记过的 origin 被分流)、错误链展开与提示、引用计数与可逆性、失败路径不静默
  • test/stdio.test.js:命令行分词(引号规则、反斜杠不转义)、stdio 配置归一化(粘进来的命令行 / npx 风格 / 带空格路径不被误拆 / 引号保护)、真实文件系统探针(含 Windows 补 .exe)、启动前置检查
  • test/client-logic.test.js:把 client.js 加载进 vm 沙箱,测注册契约(id = 包名)、表单渲染契约(stdio 与 streamable-http 的连接参数互斥、通用项都在)、主题样式契约(color-scheme 亮/暗两套、option 显式配色、--mcp-* 定义与引用一致、样式表注入/卸载)、外壳原生组件契约(kit 路径用原生 Button/Input/Switch/Tag/StateDot/RiskConfirmation/图标,且「没有这个包」「同名模块形状不对」「有原语但缺单个组件」三种降级都退回自带元素与文字字形),以及「客户端表单产出 → 宿主校验」的往返契约
  • test/contract.test.js:用真实的 @deepseek-ai/dsh-mcp-client Config schema 校验 buildClientConfig 的产出——它两边的配置契约一旦偏离,测试先红;同时断言 TLS 字段绝不进入实例配置
  • test/e2e.test.js:端到端冒烟——内存 Cordis 上下文 + 假 settings/tools/webServer/connection 服务 + 极简 stdio MCP 服务器(fixtures/fake-mcp-server.mjs),走真实 HTTP 验证「保存即生效」全链路(挂载、工具注册、真实失败状态、rev 409、开关、引用),并断言接口注册在 connection.fetch 上、未鉴权请求 401
  • test/tls-e2e.test.js:自签名 HTTPS MCP 服务器(fixtures/tls-mcp-server.mjs + fixtures/tls/*.pem,一次性生成的测试证书)端到端——对照组(不开 TLS 选项)必然失败并记录证书原因;开 tlsInsecure 后工具注册;改用 tlsCaFile 也能连通;移除后 globalThis.fetch 还原

scripts/check-pack.mjs 校验打包内容白名单,可单独 npm run check:pack 执行。

许可

MIT