@oh-agent/dsh-mcp-manager
One settings page for every MCP server on your machine. It reads the real config file of 17 other AI tools (Claude Desktop, Claude Code, Cursor, VS Code, Windsurf, OpenCode, Trae, Trae CN, Qoder, Gemini CLI, Qwen, Zed, Roo Code, Copilot CLI, Kiro, CodeBuddy, Continue) plus DeepSeek Harness itself, g
Install
dsh plugin --profile web add @oh-agent/dsh-mcp-manager
Sandbox test results
| Stage | Result |
|---|---|
| 1 install | ✓ |
| 2 list | ✓ |
| 3 mount | ✓ |
| 4 boot | — |
Four stages run in an isolated sandbox: install (network on) → list → mount → boot (last three with no network).
README
@oh-agent/dsh-mcp-manager
中文 · [English](README.en.md)
一个设置页,管理散落在本机各个 AI 开发工具里的 MCP 服务器 —— 每个配置文件一张卡片,显示它声明了什么;每个服务器一个开关,把它挂载进 DeepSeek Harness;再加一次真连探活,看它到底能不能起来;更重要的是,页面直接告诉你它在 Harness 里到底加载成功没有,失败了可以就地重试。
入口:DeepSeek Harness → 设置 → MCP(位于「技能管理」和「插件市场」之间)。
安装
方式一:插件管理页(推荐)
设置 → 插件管理 → 安装,搜索 @oh-agent/dsh-mcp-manager。
方式二:让 agent 安装
直接说「安装 @oh-agent/dsh-mcp-manager」,等价于:
plugin_manager action: install_bundle target: "@oh-agent/dsh-mcp-manager@0.1.6"
target 也可以填一个绝对目录 —— 本地源码目录,或者
v0.1.6 release 里附的
dsh-mcp-manager.tgz 解出来的目录。
不要手动 pnpm add 进 profile,也不要手改 profile 的 package.json 或
cordis.patch.yml:装了包不等于选中了它的 bundle,手动装的后果是那一行存在但永远不会被挂载。
安装后增加一个设置页和一个 agent 工具(mcp_manage)。它没有任何运行时依赖,不需要构建,也没有 allowBuilds 授权步骤。要求 Node ^22.19.0 || >=24.0.0(DeepSeek Harness 自带 Node 24,无需另装)。
页面会读取其他工具的配置文件,但开关不需要对它们有写权限。所有数据都不离开本机 —— 唯一的对外流量是你主动点的那次探活,且只发往你点的那一个服务器。
它解决什么问题
同一个 MCP 服务器,在每个工具里声明在不同的文件、不同的键、不同的方言下:
工具 · 文件(Windows) · 顶层键
Claude Desktop · %APPDATA%\Claude\claude_desktop_config.json · mcpServers
Claude Code · ~/.claude.json、<proj>/.mcp.json · mcpServers
Cursor · ~/.cursor/mcp.json · mcpServers
Windsurf · ~/.codeium/windsurf/mcp_config.json · mcpServers
VS Code · <ws>/.vscode/mcp.json · servers
Roo Code · …\rooveterinaryinc.roo-cline\settings\cline_mcp_settings.json · mcpServers
Gemini CLI · ~/.gemini/settings.json · mcpServers
Qwen Code · ~/.qwen/settings.json · mcpServers
Qoder IDE · %APPDATA%\Qoder\SharedClientCache\mcp.json · mcpServers
OpenCode · ~/.config/opencode/opencode.json · mcp
Trae · %APPDATA%\Trae\User\mcp.json · mcpServers
Trae CN · %APPDATA%\Trae CN\User\mcp.json · mcpServers
Copilot CLI · ~/.copilot/mcp-config.json · mcpServers
Kiro · ~/.kiro/settings/mcp.json · mcpServers
Zed · settings.json · context_servers
Continue · ~/.continue/config.yaml · mcpServers(YAML 数组)
CodeBuddy · %APPDATA%\CodeBuddy\... · mcpServers
DeepSeek Harness · profile 组合文件 · @deepseek-ai/dsh-mcp-client 行
这些差异不是美观问题,而是手改最容易翻车的地方:
- 远程传输的拼写不同。 Windsurf 只认
serverUrl,写成url会被静默忽略。Gemini 和 Qwen 区分url(SSE)与httpUrl(streamable HTTP),而 Roo 把 streamable HTTP 拼成streamable-http。 - 超时单位不同。 Gemini / Qwen / DSH 是毫秒,Roo 是秒。
- 命令的形态不同。 OpenCode 的
command是数组,env叫environment;其他工具是字符串command加独立的args。 - 有些文件混着无关状态。
~/.claude.json里还有历史记录和 OAuth token;opencode.json里有 provider 和 plugin。整文件重写会把这些一起毁掉。
本插件把这一切读入同一个模型,写回时只改它想改的那一条。
Windows 与 macOS
没有任何路径写死在单一平台上。每个适配器通过读取操作系统的解析器声明自己的文件:
根 · Windows · macOS
appData() · %APPDATA% · ~/Library/Application Support
xdgConfig() · ~/.config · ~/.config
vscodeUserDir() · %APPDATA%\Code\User · ~/Library/Application Support/Code/User
zedConfig() · %APPDATA%\Zed · ~/.config/zed
HOME 相对 · ~/.codex、~/.cursor … · 相同
Zed 是值得点名的例外:macOS 上它读 ~/.config/zed,不是 Application Support,所以它有独立解析器。APPDATA、XDG_CONFIG_HOME、COPILOT_HOME 被设置时会生效。
项目级路径始终拼在工作区根目录上,天然可移植。scripts/test-paths.mjs 在模拟的 win32、darwin、linux 下加载真实的适配器源码,逐个客户端断言解析出的目录。
架构
client.js (设置页) index.js (宿主入口)
│ │
└──────── /api/dsh-mcp-manager ────┘
│ │
scan.js ops.js probe.js
│ │ │
adapters.js store.js surgical.js
│ │ │
canonical.js ledger+ 保字节编辑
(统一模型) 备份 JSON/TOML/YAML
│
dsh.js + dsh-patch.js + toggle.js
canonical.js—— 唯一的语义模型。 每个客户端条目都解码为{name, transport, command, args, env, url, headers, timeoutMs, enabled, autoApprove, extra, secrets}。能力不向最小公约数对齐:某个客户端表达不了的字段,写入时丢弃并作为droppedFields上报,绝不悄悄编造。adapters.js—— 唯一懂格式的层。 十八个客户端即数据:容器键、方言(哪个键表示 url、哪种 type 字符串映射到哪种传输、超时单位)、能力、文件位置。surgical.js—— 保字节编辑器。 携带偏移量的解析树 + 自底向上的编辑,替换一条不会重排整个文件。注释、尾逗号、CRLF、无关的键都活下来;解析不了就拒绝,而不是重写。store.js—— 安全网。 写入前字节备份、比较并交换的哈希校验、归属台账。probe.js—— 它到底能不能用。 手写的 JSON-RPC 客户端(不依赖@modelcontextprotocol/sdk):spawn stdio →initialize→notifications/initialized→tools/list;远程则 POST,带Accept: application/json, text/event-stream并跟随 SSE 响应。只读 —— 从不调用任何工具。dsh.js/dsh-patch.js/toggle.js—— 把 Harness 自己也当成一等目标。 已存在的行通过configEditor.edit()改写(校验 + 热重载)。全新的服务器没有行可寻址,于是写进组合文件的- insert:补丁层,Loader 会自己把它拾起来 —— 操作会等这一行出现并上报live: true。
如果预算内没等到,它上报 activating: true,绝不说「需要重启」:补丁行只会写在 Loader 看得见的地方,所以它迟早会生效。给一个正在生效中的服务器提示重启是实测出来的错误 —— 连续挂载多个服务器会把重排推过任何固定预算,随后它们全部以 live 出现。
发现逻辑读的是 entry.options.name,不是 entry.name:运行中的 Loader 条目自身没有 name 属性(已在运行中的 Host 上验证过),读错字段会静默匹配到零行 —— 结果每个已挂载的服务器都看起来没生效,并索要一次根本不需要的重启。
补丁行写进哪个 profile 只认正在跑的那个 Host:profileContext(boot() 在任何 profile 插件加载之前 provide,含 { name, dir })排第一,DSH_PROFILE_DIR、--profile 参数、DSH_PROFILE 依次兜底,机器上恰好只有一个 profile 时才用「唯一」这个事实,否则报 error.dsh.noProfile 拒绝写。不能只靠 DSH_PROFILE_DIR:dsh-shell-env 只把它注入 shell 工具的执行环境,从不注入 Host 自己的进程 —— 桌面版就是这种情况,加上本机有 5 个 profile,旧逻辑必然解析失败,添加服务器时报 cannot locate the active DSH profile。反向也不行:早先退回 ~/.dsh/profiles/desktop 会写进一个根本没在跑的 profile,行是真的,Loader 永远看不见。
安全模型
写入路径围绕一条规则构建:只重写我们能证明是自己写的东西。
1. 归属台账。 每次写入记录 {path, serverName, fingerprint}。之后同名写入只有在磁盘上的定义仍与该指纹一致(用户没改过)时才生效。
2. 绝不覆盖别人的服务器。 名字已被非我们写入的条目占用时,视为用户的:新来的服务器改用带后缀的名字。
3. 乐观并发。 调用方传入它扫描到的哈希;文件变了就返回 409 和当前哈希,界面提示重新扫描。不存在静默的后写覆盖。
4. 每次修改前备份,按 backupsToKeep 保留,可在页面还原。
5. 原子替换。 写临时文件、rename 覆盖目标、再读回并重新解析,证明结果是合法的。
6. 凭据在离开宿主前就被掩码。 掩码是按键驱动的,不是按值形状 —— 短 token(TOKEN=abc)照样掩码,而间接引用(${FIGMA_API_KEY})原样显示,因为它指向密钥名而不是携带密钥
…(truncated)
Rendered from the README published on npm. Formatted only, not edited.
Versions
Published on npm::8,最后更新 2026-11-04