Bundle
@fishlikewater/dsh-mcp-manager
MCP 服务器管理器:在 DSH 设置页配置 MCP 服务器,宿主侧按配置热挂载/卸载 mcp-client 实例
- Source
- fishlikewater
- stars
- 2 stars
- License
- MIT
- Updated
- Updated 10 days ago
Readme
# dsh-mcp-manager
MCP 服务器管理器:给 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(DSH)加一个「设置 → MCP 服务器」页面,在界面上配置 MCP(Model Context Protocol)服务器,**保存即生效**——宿主侧自动挂载/卸载 `@deepseek-ai/dsh-mcp-client` 实例,我(模型)随即拿到或失去 `mcp__<名称>__<工具名>` 形式的工具。
无需再手写 `cordis.patch.yml` 里的 MCP 行,也无需重启进程。
## 功能
- 设置页管理:列出 / 新增 / 编辑 / 删除 MCP 服务器,带挂载状态
- 每台服务器可单独开启/关闭(`enabled` 开关):关闭立即停止实例并移除工具,开启重新挂载并注册工具,状态持久化
- 两种传输:`stdio`(本地命令)与 `streamable-http`(远程 URL)
- **真实挂载状态**:连接失败、重连中、重连成功、放弃重连等异步事件实时反映到设置页(截获 mcp-client 日志驱动),不再只显示乐观的「已挂载」
- **并发保存保护**:GET/POST 携带 `rev` 修订号(乐观锁),配置被其他窗口改过时返回 409 并提示刷新,避免静默覆盖
- 每台服务器可配置 `toolCallTimeoutMs`(工具调用超时)与 `failOnStartupError`(启动失败即报错)
- **密钥不入盘**:env / headers 的值支持 `env:NAME`(进程环境变量)与 `cred:NAME`(DSH 凭据)引用,挂载时解析,settings.yaml 只存引用
- 配置持久化在 `settings.yaml` 的 `mcp` 命名空间(`mcp.servers`),改动热生效
- 宿主侧按配置动态挂载 `@deepseek-ai/dsh-mcp-client` 实例(断线自动重连、工具自动注册/注销由它负责)
- headless profile 也能用:没有 `webServer` 服务时跳过 HTTP 接口,只做实例管理
## 安装
> 前置:已安装 `dsh` CLI(以及 pnpm;`dsh plugin` 命令转发给 pnpm)。
```sh
# 从 npm 安装(发布后)
dsh plugin --profile web add @fishlikewater/dsh-mcp-manager
# 或从本地 checkout 安装
dsh plugin --profile web add ./path/to/dsh-mcp-manager
```
`dsh plugin add` 会把包安装进 profile 的依赖,并因为本包声明了 `dsh.bundle`,自动把 `@fishlikewater/dsh-mcp-manager` 追加到该 profile 的 `dsh.profile.bundles`(即应用本包的 `cordis.patch.yml` 层)。验证组合树:
```sh
dsh --profile web --dump-config # 应能看到 mcp-manager 行
```
**重启 dsh 进程后生效**(bundle 层在启动时组合;插件代码改动也需要重启,见下文「开发」)。重启后刷新浏览器页面,打开 设置 → MCP 服务器 即可。
卸载:`dsh plugin --profile web remove @fishlikewater/dsh-mcp-manager`(同时移除依赖与 bundle 层)。
## 使用
设置页各字段:
| 字段 | 传输 | 说明 |
|---|---|---|
| 名称(serverName) | 两者 | `[A-Za-z0-9_-]{1,32}`,全局唯一,决定工具名前缀 `mcp__<名称>__…` |
| 传输方式 | 两者 | `stdio` / `streamable-http` |
| 命令(command) | stdio | 可执行文件(支持绝对路径)或 `npx` 之类命令 |
| 参数(args) | stdio | 每行一个 |
| 环境变量(env) | stdio | 每行 `KEY=VALUE`;值支持 `env:NAME` / `cred:NAME` 引用 |
| 工作目录(cwd) | stdio | 可选 |
| URL | http | 如 `http://localhost:3000/mcp` |
| 请求头(headers) | http | 每行 `KEY: VALUE`;值支持 `env:NAME` / `cred:NAME` 引用 |
| 工具调用超时(toolCallTimeoutMs) | 两者 | 毫秒,默认 60000 |
| 启动失败即报错(failOnStartupError) | 两者 | 是否在首次连接/工具同步失败时报错(默认否,自动重连) |
保存后立即生效;删除后对应工具立即移除。
每张服务器卡片上的「关闭 / 开启」按钮会立即停止 / 启动对应实例:关闭后工具马上移除、不再重连;开启后重新挂载并注册工具。开关状态(`enabled`,默认 `true`)随配置一起持久化在 settings.yaml,重启 dsh 后保持;只影响被切换的那一台,其余服务器不受影响。
### 示例
`examples/mcp-servers.example.yaml` 给出了 settings.yaml 中 `mcp` 命名空间的示例(stdio 与 streamable-http 各一)。可以直接把它合并进 `$DSH_HOME/settings.yaml` 作为初始配置,再从设置页调整。
## HTTP 接口(供设置页使用)
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | `/api/mcp/servers` | 返回 `{ rev, servers, status }`;`rev` 是命名空间修订号(乐观锁),`status` 是 `serverName -> { state: 'ok'\|'error'\|'disabled', message }` |
| POST | `/api/mcp/servers` | 请求体 `{ rev?, servers: [...] }`,整体替换并触发重挂载;`rev` 与当前修订不符返回 409 `{ code: 'conflict', rev }`;校验失败返回 400 `{ error }` |
密钥引用(`env:NAME` / `cred:NAME`)在挂载时解析:`env:` 读进程环境变量;`cred:` 经 DSH 的 `credentials` 服务解析(`resolve`),服务不可用或未配置时保留字面值并打日志。settings.yaml 里始终只存引用文本。
## 开发
结构:
```
├── package.json # dsh.bundle.patch + dsh.client 双 manifest
├── cordis.patch.yml # bundle 层:插入 mcp-manager 行
├── index.js # 宿主侧:settings 注册 + 实例挂载 + HTTP 接口
├── lib/logic.js # 纯逻辑:settings schema、校验、配置构造、指纹、日志翻译、密钥引用
├── client.js # 客户端 bundle(手写 CJS factory,经 ./client 导出)
├── test/ # node:test 套件(见下)
├── fixtures/ # E2E 用的极简 stdio MCP 服务器(不打包、不被当作测试执行)
├── scripts/ # 发布前 pack 校验等
├── examples/
└── .github/workflows/ # CI:Node × dsh-mcp-client 版本矩阵 + pack 校验
```
注意:
- **改代码后必须重启 dsh 进程**:Node 的 ESM 模块缓存不会随文件变化失效。配置数据不需要重启(设置页保存即热生效)。
- `client.js` 是给浏览器模块加载器的构建产物(`window.__ModuleLoader__.load` 格式),直接手写维护,不需要打包器。
- 设置页导航图标:外壳按 section id 硬编码图标且没有注册点,本插件用一小段 CSS 覆盖把「MCP 服务器」行的图标换成链接图标;选择器依赖外壳的类名哈希(`VOzbGW_*`),DSH 升级后若失效只会安静地退回默认齿轮图标,不影响功能。
- `@deepseek-ai/dsh-mcp-client` 以 peerDependency 声明,由运行时提供;`@deepseek-ai/schemastery` 为普通依赖。
### 测试(兼容性)
`npm test`(node:test,无需额外框架):
- `test/host-logic.test.js`:settings 命名空间 schema、HTTP 校验、mcp-client 配置构造、配置指纹、mcp-client 日志 → 状态翻译(用真实日志文案做夹具)、密钥引用解析
- `test/client-logic.test.js`:把 `client.js` 加载进 vm 沙箱,测解析/组装助手,以及「客户端表单产出 → 宿主校验」的往返契约
- `test/contract.test.js`:用真实的 `@deepseek-ai/dsh-mcp-client` `Config` schema 校验 `buildClientConfig` 的产出——它两边的配置契约一旦偏离,测试先红
- `test/e2e.test.js`:端到端冒烟——内存 Cordis 上下文 + 假 settings/tools/webServer 服务 + 极简 stdio MCP 服务器(`fixtures/fake-mcp-server.mjs`),走真实 HTTP 验证「保存即生效」全链路(挂载、工具注册、真实失败状态、rev 409、开关、引用)
CI(`.github/workflows/ci.yml`)跑 3 个 Node(18/20/22)× 3 个已发布的 `dsh-mcp-client`(`0.1.0-rc.6` / `0.1.0-rc.8` / `0.1.1-rc.2`)矩阵,另有 pack job 校验发布内容白名单(`node scripts/check-pack.mjs`,npm publish 前由 `prepublishOnly` 自动执行)。
## 发布
```sh
npm publish
```
> 包名使用 scope 版 `@fishlikewater/dsh-mcp-manager`:无 scope 的 `dsh-mcp-manager` 已被他人占用(github.com/Nichts0v0/dsh-mcp-manager)。发布者的 npm 账号必须是 `fishlikewater`(scope 归属),`publishConfig.access: public` 已配置,无需 `--access public`。改包名时需同步修改 `cordis.patch.yml` 里的 `name` 行。
## License
MIT
Install
dsh plugin --profile web add github:fishlikewater/dsh-mcp-manager
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install fishlikewater-dsh-mcp-manager from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.