dsh-mcp-manager
Verified@zfdx123/dsh-mcp-manager · v1.0.9 · MIT · Web UI
MCP 服务器管理器:在 DSH 设置页配置 MCP 服务器,宿主侧按配置热挂载/卸载 mcp-client 实例(支持内网自签名证书)
Install
dsh plugin add @zfdx123/dsh-mcp-manager 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.
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。两种解法:
- 推荐:把服务器证书(或签发它的 CA)导出成 PEM,填进
tlsCaFile。证书仍然校验,只是多信任这一个 CA。 - 临时/应急:把
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,完全看不出原因。插件对此做了两件事:
自动归一化:
command里混进来的参数、args每行里的多个 token 都会按引号规则拆开(不解释反斜杠转义,Windows 路径安全;C:\Program Files\...\node.exe这种真实存在的带空格路径不会被误拆,也可用引号显式声明为一个整体)。每次改写都会写一条宿主告警。启动前置检查:可执行文件找不到、脚本文件(绝对路径)不存在、工作目录不存在、
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.1.7-rc.2(engines.dsh 与 peer 范围;本包在 1.0.5 那一版按 0.1.7-rc.2 的设置 API 重写过,此前按 0.1.6-alpha.2 逐条核对过。0.1.5 / 0.1.6-alpha.* 系列也已核对,但已不在声明范围内) |
@deepseek-ai/dsh-mcp-client |
^0.1.7-rc.2(peerDependency,由运行时提供;reconnect 配置从 0.1.5 起可选,maxInstructionBytes 从 0.1.6 起可选、默认 32768) |
@deepseek-ai/dsh-settings |
^0.1.7-rc.2(peerDependency,可选:ctx.settings 由宿主提供) |
@deepseek-ai/cordis |
^4.0.4(peerDependency) |
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扫描上游源码文案(改词即变红)、确认窗口——挂载后没有正向证据就不亮绿,所以文案漂移最多让状态停在「连接中 / 未确认」,不会退回「乐观的已挂载」。 - 确认窗口是启发式,不是握手:它靠「工具注册」这一个正向信号 + 超时兜底。一台连得上但确实没有工具的 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.1.7-rc.2),让本地测试与线上尽量同源。
兼容性
| 组件 | 版本要求 |
|---|---|
| Node | ^22.19.0 || >=24.0.0 |
| DSH | 0.1.7-rc.2(本包在 1.0.5 按该版本的设置 API 逐条重写过;0.1.6-alpha.* 与 0.1.5 系列是更早的校对记录,peer 范围现已收紧到 ^0.1.7-rc.2) |
@deepseek-ai/dsh-mcp-client |
^0.1.7-rc.2(peerDependency;reconnect 配置从 0.1.5 起可选,maxInstructionBytes 从 0.1.6 起可选、默认 32768) |
@deepseek-ai/dsh-settings |
^0.1.7-rc.2(peerDependency;ctx.settings 由宿主提供) |
@deepseek-ai/cordis |
^4.0.4 |
历史修复记录(当前版本 1.0.9)
本包版本已到 1.0.9;下面两张表是历史版本的修复记录,保留下来是因为它们记录了「代码为什么长这样」(表内的版本与 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 对齐的几处
都是按运行时安装的实际包逐条核对,不是猜的:
- 客户端 bundle 注册 id:必须是包名(
@zfdx123/dsh-mcp-manager)。宿主装完/plugins/??<包名>/client.js后会校验该 id 的 factory 是否注册过,写短名会抛bundle ... loaded without registering "<id>",设置页永远不出现。 - HTTP 接口载体:0.1.5 的
/api由connection插件以 prefix 路由独占,并施加 Host/Origin 信任栅栏 + 浏览器鉴权。插件改用connection.fetch.register注册精确 Fetch 路由(宿主负责鉴权),不再裸挂/api/...。 reconnect配置:0.1.5 的 mcp-clientConfig新增可选项reconnect: { enabled, initialDelayMs, maxDelayMs, maxAttempts };本插件按需输出该键(只写改过的子键,其余交回 mcp-client 默认值)。- 导航图标覆盖移除:外壳不再输出
VOzbGW_navList/navCell/navIcon,CSS 覆盖在任何版本上都不会命中,已删除(连带删除注入<style>与自检代码)。 - 同源实例隔离: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-clientConfigschema 校验buildClientConfig的产出——它两边的配置契约一旦偏离,测试先红;同时断言 TLS 字段绝不进入实例配置test/e2e.test.js:端到端冒烟——内存 Cordis 上下文 + 假 settings/tools/webServer/connection 服务 + 极简 stdio MCP 服务器(fixtures/fake-mcp-server.mjs),走真实 HTTP 验证「保存即生效」全链路(挂载、工具注册、真实失败状态、rev 409、开关、引用),并断言接口注册在connection.fetch上、未鉴权请求 401test/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