@oh-agent/dsh-mcp-manager 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +351 -2
- package/README.zh.md +213 -0
- package/cordis.patch.yml +7 -0
- package/icon.svg +7 -0
- package/lib/adapters.js +346 -0
- package/lib/canonical.js +488 -0
- package/lib/client.js +1151 -0
- package/lib/dsh-patch.js +329 -0
- package/lib/dsh.js +259 -0
- package/lib/index.js +495 -0
- package/lib/ops.js +325 -0
- package/lib/probe.js +252 -0
- package/lib/scan.js +353 -0
- package/lib/store.js +257 -0
- package/lib/surgical.js +581 -0
- package/lib/toggle.js +194 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +66 -4
package/lib/adapters.js
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client adapters: where each tool stores MCP servers, in what format, and how
|
|
3
|
+
* its dialect differs from the canonical model.
|
|
4
|
+
*
|
|
5
|
+
* A row here is a declaration, not code. The scanner and the writer are generic
|
|
6
|
+
* and driven entirely by these declarations, so adding a client is a data edit.
|
|
7
|
+
* Every path is resolved per (os, client, scope); nothing is built by string
|
|
8
|
+
* concatenating `~`, because the real paths disagree more than they match
|
|
9
|
+
* (`%APPDATA%\Trae\User\mcp.json`, Roo's `rooveterinaryinc.roo-cline` directory
|
|
10
|
+
* holding `cline_mcp_settings.json`, Qoder's IDE cache vs its CLI settings).
|
|
11
|
+
*
|
|
12
|
+
* `caps` states honestly what a client can express. The UI greys out what a
|
|
13
|
+
* target cannot represent instead of silently dropping the field.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { homedir, platform as osPlatform } from 'node:os';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
|
|
19
|
+
const HOME = homedir();
|
|
20
|
+
const IS_WIN = osPlatform() === 'win32';
|
|
21
|
+
const IS_MAC = osPlatform() === 'darwin';
|
|
22
|
+
|
|
23
|
+
function appData(...parts) {
|
|
24
|
+
if (IS_WIN) return join(process.env.APPDATA ?? join(HOME, 'AppData', 'Roaming'), ...parts);
|
|
25
|
+
if (IS_MAC) return join(HOME, 'Library', 'Application Support', ...parts);
|
|
26
|
+
return join(process.env.XDG_CONFIG_HOME ?? join(HOME, '.config'), ...parts);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function xdgConfig(...parts) {
|
|
30
|
+
if (IS_WIN) return join(HOME, '.config', ...parts);
|
|
31
|
+
if (IS_MAC) return join(HOME, '.config', ...parts);
|
|
32
|
+
return join(process.env.XDG_CONFIG_HOME ?? join(HOME, '.config'), ...parts);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function vscodeUserDir() {
|
|
36
|
+
if (IS_WIN) return appData('Code', 'User');
|
|
37
|
+
if (IS_MAC) return join(HOME, 'Library', 'Application Support', 'Code', 'User');
|
|
38
|
+
return join(process.env.XDG_CONFIG_HOME ?? join(HOME, '.config'), 'Code', 'User');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Zed is the one client whose config does NOT live in the macOS Application
|
|
43
|
+
* Support directory: it uses `~/.config/zed` on macOS *and* Linux (honouring
|
|
44
|
+
* XDG_CONFIG_HOME), and `%APPDATA%\Zed` on Windows. Sending it through
|
|
45
|
+
* `appData()` would point macOS users at a directory Zed never reads.
|
|
46
|
+
*/
|
|
47
|
+
function zedConfig(...parts) {
|
|
48
|
+
if (IS_WIN) return appData('Zed', ...parts);
|
|
49
|
+
return join(process.env.XDG_CONFIG_HOME ?? join(HOME, '.config'), 'zed', ...parts);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const STDIO_CAPS = { enabled: true, autoApprove: false, timeoutMs: false, envFile: false, oauth: false, projectScope: false };
|
|
53
|
+
|
|
54
|
+
/** Transport→type spellings per family, both directions. */
|
|
55
|
+
const TYPE = {
|
|
56
|
+
claudeCode: {
|
|
57
|
+
typeMap: { stdio: 'stdio', http: 'http-stream', 'streamable-http': 'http-stream', 'streamablehttp': 'http-stream', sse: 'http-sse', ws: 'http-stream', sdk: 'http-stream' },
|
|
58
|
+
reverseType: { stdio: 'stdio', 'http-stream': 'http', 'http-sse': 'sse' },
|
|
59
|
+
typeRequired: true,
|
|
60
|
+
stdioType: 'stdio',
|
|
61
|
+
},
|
|
62
|
+
vscode: {
|
|
63
|
+
typeMap: { stdio: 'stdio', http: 'http-stream', 'streamable-http': 'http-stream', sse: 'http-sse' },
|
|
64
|
+
reverseType: { stdio: 'stdio', 'http-stream': 'http', 'http-sse': 'sse' },
|
|
65
|
+
},
|
|
66
|
+
cursor: { typeMap: { stdio: 'stdio', sse: 'http-sse' }, reverseType: { sse: 'sse' } },
|
|
67
|
+
roo: {
|
|
68
|
+
typeMap: { stdio: 'stdio', 'streamable-http': 'http-stream', sse: 'http-sse' },
|
|
69
|
+
reverseType: { 'http-stream': 'streamable-http', 'http-sse': 'sse' },
|
|
70
|
+
},
|
|
71
|
+
gemini: {
|
|
72
|
+
typeMap: {},
|
|
73
|
+
urlKey: 'httpUrl',
|
|
74
|
+
// `httpUrl` means streamable HTTP; plain `url` means SSE.
|
|
75
|
+
sseUrlKey: 'url',
|
|
76
|
+
reverseType: undefined,
|
|
77
|
+
},
|
|
78
|
+
opencode: {
|
|
79
|
+
typeMap: { local: 'stdio', remote: 'http-stream' },
|
|
80
|
+
reverseType: { stdio: 'local', 'http-stream': 'remote', 'http-sse': 'remote' },
|
|
81
|
+
envKey: 'environment',
|
|
82
|
+
commandArray: true,
|
|
83
|
+
enabledField: 'enabled',
|
|
84
|
+
},
|
|
85
|
+
zed: { typeMap: {} },
|
|
86
|
+
continueLike: { typeMap: {} },
|
|
87
|
+
plain: { typeMap: {} },
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The catalog. `order` decides UI grouping (installed/used clients first).
|
|
92
|
+
* `caps.projectScope` marks clients whose project-level file exists.
|
|
93
|
+
*/
|
|
94
|
+
export const CLIENTS = [
|
|
95
|
+
{
|
|
96
|
+
id: 'qoder-ide',
|
|
97
|
+
label: { en: 'Qoder (IDE)', zh: 'Qoder(IDE)' },
|
|
98
|
+
order: 20,
|
|
99
|
+
format: 'json',
|
|
100
|
+
containerKey: 'mcpServers',
|
|
101
|
+
dialect: TYPE.claudeCode,
|
|
102
|
+
caps: { ...STDIO_CAPS, timeoutMs: true },
|
|
103
|
+
note: { en: 'IDE cache file; six-value type enum including sdk/ws.', zh: 'IDE 缓存文件;type 为六值枚举(含 sdk/ws)。' },
|
|
104
|
+
paths: () => [{ scope: 'global', path: appData('Qoder', 'SharedClientCache', 'mcp.json') }],
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: 'opencode',
|
|
108
|
+
label: { en: 'OpenCode', zh: 'OpenCode' },
|
|
109
|
+
order: 30,
|
|
110
|
+
format: 'json',
|
|
111
|
+
containerKey: 'mcp',
|
|
112
|
+
dialect: TYPE.opencode,
|
|
113
|
+
caps: { ...STDIO_CAPS, enabled: true, projectScope: true },
|
|
114
|
+
note: { en: '`command` is an array, env is `environment`, type is local/remote, variables use {env:VAR}.', zh: '`command` 为数组、环境变量键为 `environment`、type 为 local/remote、变量语法 {env:VAR}。' },
|
|
115
|
+
paths: () => [
|
|
116
|
+
{ scope: 'global', path: xdgConfig('opencode', 'opencode.json') },
|
|
117
|
+
{ scope: 'project', path: join('opencode.json'), relative: true },
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: 'trae',
|
|
122
|
+
label: { en: 'Trae', zh: 'Trae' },
|
|
123
|
+
order: 40,
|
|
124
|
+
format: 'json',
|
|
125
|
+
containerKey: 'mcpServers',
|
|
126
|
+
dialect: TYPE.plain,
|
|
127
|
+
caps: { ...STDIO_CAPS },
|
|
128
|
+
note: { en: 'Global path carries an extra `User` level.', zh: '全局路径多一层 `User`。' },
|
|
129
|
+
paths: () => [{ scope: 'global', path: appData('Trae', 'User', 'mcp.json') }],
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
/*
|
|
133
|
+
* The mainland build of Trae is a separate installation with its own config
|
|
134
|
+
* directory, and it can be installed alongside the international build on the
|
|
135
|
+
* same machine — so it is its own row, not a second path on `trae`. Its file
|
|
136
|
+
* is the same shape (`mcpServers`, plain stdio entries) but carries extra
|
|
137
|
+
* `fromGalleryId` / `disabled` keys from the built-in gallery; `disabled` is
|
|
138
|
+
* the one modelled field and the rest round-trips untouched through `extra`.
|
|
139
|
+
*/
|
|
140
|
+
id: 'trae-cn',
|
|
141
|
+
label: { en: 'Trae CN', zh: 'Trae 国内版' },
|
|
142
|
+
order: 41,
|
|
143
|
+
format: 'json',
|
|
144
|
+
containerKey: 'mcpServers',
|
|
145
|
+
dialect: TYPE.plain,
|
|
146
|
+
caps: { ...STDIO_CAPS },
|
|
147
|
+
note: { en: 'Mainland build, separate install; entries carry gallery-only keys that are preserved as-is.', zh: '国内版为独立安装;条目带画廊专用键,会原样保留。' },
|
|
148
|
+
paths: () => [{ scope: 'global', path: appData('Trae CN', 'User', 'mcp.json') }],
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: 'codebuddy',
|
|
152
|
+
label: { en: 'CodeBuddy', zh: 'CodeBuddy' },
|
|
153
|
+
order: 50,
|
|
154
|
+
format: 'json',
|
|
155
|
+
containerKey: 'mcpServers',
|
|
156
|
+
dialect: TYPE.plain,
|
|
157
|
+
caps: { ...STDIO_CAPS },
|
|
158
|
+
paths: () => [{ scope: 'global', path: join(HOME, '.codebuddy', 'mcp.json') }],
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: 'roo',
|
|
162
|
+
label: { en: 'Roo Code', zh: 'Roo Code' },
|
|
163
|
+
order: 61,
|
|
164
|
+
format: 'json',
|
|
165
|
+
containerKey: 'mcpServers',
|
|
166
|
+
dialect: TYPE.roo,
|
|
167
|
+
caps: { ...STDIO_CAPS, enabled: true, autoApprove: true, timeoutMs: true, projectScope: true },
|
|
168
|
+
note: { en: 'Auto-approve is `alwaysAllow`; timeout is in seconds.', zh: '自动批准字段为 `alwaysAllow`;超时为秒。' },
|
|
169
|
+
paths: () => [
|
|
170
|
+
{ scope: 'global', path: join(vscodeUserDir(), 'globalStorage', 'rooveterinaryinc.roo-cline', 'settings', 'mcp_settings.json') },
|
|
171
|
+
{ scope: 'project', path: join('.roo', 'mcp.json'), relative: true },
|
|
172
|
+
],
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: 'claude-desktop',
|
|
176
|
+
label: { en: 'Claude Desktop', zh: 'Claude Desktop' },
|
|
177
|
+
order: 70,
|
|
178
|
+
format: 'json',
|
|
179
|
+
containerKey: 'mcpServers',
|
|
180
|
+
dialect: TYPE.plain,
|
|
181
|
+
caps: { ...STDIO_CAPS },
|
|
182
|
+
note: { en: 'Official build is stdio only; remote needs an mcp-remote bridge.', zh: '官方仅支持 stdio;远程需 mcp-remote 桥接。' },
|
|
183
|
+
paths: () => [{ scope: 'global', path: appData('Claude', 'claude_desktop_config.json') }],
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
id: 'claude-code',
|
|
187
|
+
label: { en: 'Claude Code', zh: 'Claude Code' },
|
|
188
|
+
order: 71,
|
|
189
|
+
format: 'json',
|
|
190
|
+
containerKey: 'mcpServers',
|
|
191
|
+
dialect: TYPE.claudeCode,
|
|
192
|
+
caps: { ...STDIO_CAPS, timeoutMs: true, projectScope: true },
|
|
193
|
+
note: { en: 'Remote entries must declare `type`; project scope also needs approval keys in ~/.claude/settings.json.', zh: '远程条目必须显式声明 `type`;项目级还需在 ~/.claude/settings.json 写入批准字段。' },
|
|
194
|
+
paths: () => [
|
|
195
|
+
{ scope: 'global', path: join(HOME, '.claude.json') },
|
|
196
|
+
{ scope: 'project', path: join('.mcp.json'), relative: true },
|
|
197
|
+
],
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
id: 'cursor',
|
|
201
|
+
label: { en: 'Cursor', zh: 'Cursor' },
|
|
202
|
+
order: 72,
|
|
203
|
+
format: 'json',
|
|
204
|
+
containerKey: 'mcpServers',
|
|
205
|
+
dialect: TYPE.cursor,
|
|
206
|
+
caps: { ...STDIO_CAPS, envFile: true, projectScope: true },
|
|
207
|
+
note: { en: 'Remote entries are recognised by `url` alone; no type tag required.', zh: '远程条目仅靠 `url` 判别,无需 type。' },
|
|
208
|
+
paths: () => [
|
|
209
|
+
{ scope: 'global', path: join(HOME, '.cursor', 'mcp.json') },
|
|
210
|
+
{ scope: 'project', path: join('.cursor', 'mcp.json'), relative: true },
|
|
211
|
+
],
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
id: 'windsurf',
|
|
215
|
+
label: { en: 'Windsurf', zh: 'Windsurf' },
|
|
216
|
+
order: 73,
|
|
217
|
+
format: 'json',
|
|
218
|
+
containerKey: 'mcpServers',
|
|
219
|
+
dialect: { ...TYPE.plain, urlKey: 'serverUrl' },
|
|
220
|
+
caps: { ...STDIO_CAPS },
|
|
221
|
+
note: { en: 'Remote URL key is `serverUrl`; writing `url` silently does nothing.', zh: '远程 URL 键为 `serverUrl`;写成 `url` 会静默失效。' },
|
|
222
|
+
paths: () => [{ scope: 'global', path: join(HOME, '.codeium', 'windsurf', 'mcp_config.json') }],
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
id: 'vscode',
|
|
226
|
+
label: { en: 'VS Code', zh: 'VS Code' },
|
|
227
|
+
order: 74,
|
|
228
|
+
format: 'json',
|
|
229
|
+
containerKey: 'servers',
|
|
230
|
+
dialect: TYPE.vscode,
|
|
231
|
+
caps: { ...STDIO_CAPS, envFile: true, projectScope: true },
|
|
232
|
+
note: { en: 'Container key is `servers`; top-level `inputs`/`sandbox` must survive rewrites.', zh: '容器键为 `servers`;顶层 `inputs`/`sandbox` 必须保留。' },
|
|
233
|
+
paths: () => [
|
|
234
|
+
{ scope: 'global', path: join(vscodeUserDir(), 'mcp.json') },
|
|
235
|
+
{ scope: 'project', path: join('.vscode', 'mcp.json'), relative: true },
|
|
236
|
+
],
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
id: 'gemini',
|
|
240
|
+
label: { en: 'Gemini CLI', zh: 'Gemini CLI' },
|
|
241
|
+
order: 75,
|
|
242
|
+
format: 'json',
|
|
243
|
+
containerKey: 'mcpServers',
|
|
244
|
+
dialect: TYPE.gemini,
|
|
245
|
+
caps: { ...STDIO_CAPS, timeoutMs: true },
|
|
246
|
+
note: { en: '`httpUrl` is streamable HTTP, `url` is SSE; timeout is milliseconds.', zh: '`httpUrl` 表示 streamable HTTP,`url` 表示 SSE;超时单位为毫秒。' },
|
|
247
|
+
paths: () => [{ scope: 'global', path: join(HOME, '.gemini', 'settings.json') }],
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
id: 'qwen',
|
|
251
|
+
label: { en: 'Qwen Code', zh: 'Qwen Code' },
|
|
252
|
+
order: 76,
|
|
253
|
+
format: 'json',
|
|
254
|
+
containerKey: 'mcpServers',
|
|
255
|
+
dialect: TYPE.gemini,
|
|
256
|
+
caps: { ...STDIO_CAPS, timeoutMs: true },
|
|
257
|
+
paths: () => [{ scope: 'global', path: join(HOME, '.qwen', 'settings.json') }],
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
id: 'copilot-cli',
|
|
261
|
+
label: { en: 'GitHub Copilot CLI', zh: 'GitHub Copilot CLI' },
|
|
262
|
+
order: 77,
|
|
263
|
+
format: 'json',
|
|
264
|
+
containerKey: 'mcpServers',
|
|
265
|
+
dialect: TYPE.plain,
|
|
266
|
+
caps: { ...STDIO_CAPS },
|
|
267
|
+
paths: () => [{ scope: 'global', path: join(process.env.COPILOT_HOME ?? join(HOME, '.copilot'), 'mcp-config.json') }],
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
id: 'kiro',
|
|
271
|
+
label: { en: 'Kiro', zh: 'Kiro' },
|
|
272
|
+
order: 78,
|
|
273
|
+
format: 'json',
|
|
274
|
+
containerKey: 'mcpServers',
|
|
275
|
+
dialect: TYPE.plain,
|
|
276
|
+
caps: { ...STDIO_CAPS, enabled: true, autoApprove: true, projectScope: true },
|
|
277
|
+
note: { en: 'Saving reconnects immediately; no restart needed.', zh: '保存即自动重连,无需重启。' },
|
|
278
|
+
paths: () => [
|
|
279
|
+
{ scope: 'global', path: join(HOME, '.kiro', 'settings', 'mcp.json') },
|
|
280
|
+
{ scope: 'project', path: join('.kiro', 'settings', 'mcp.json'), relative: true },
|
|
281
|
+
],
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
id: 'zed',
|
|
285
|
+
label: { en: 'Zed', zh: 'Zed' },
|
|
286
|
+
order: 79,
|
|
287
|
+
format: 'json',
|
|
288
|
+
containerKey: 'context_servers',
|
|
289
|
+
dialect: TYPE.zed,
|
|
290
|
+
caps: { ...STDIO_CAPS },
|
|
291
|
+
note: { en: 'Container key is `context_servers`; local vs remote is inferred from command/url.', zh: '容器键为 `context_servers`;本地/远程靠 command/url 判别。' },
|
|
292
|
+
paths: () => [{ scope: 'global', path: zedConfig('settings.json') }],
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
id: 'continue',
|
|
296
|
+
label: { en: 'Continue', zh: 'Continue' },
|
|
297
|
+
order: 80,
|
|
298
|
+
format: 'yaml',
|
|
299
|
+
containerKey: 'mcpServers',
|
|
300
|
+
dialect: TYPE.continueLike,
|
|
301
|
+
caps: { ...STDIO_CAPS },
|
|
302
|
+
readOnly: true,
|
|
303
|
+
note: { en: 'YAML with an array of named servers; read-only here to protect hand-written structure.', zh: 'YAML 且为带 name 的数组形式;此处只读以保护手写结构。' },
|
|
304
|
+
paths: () => [{ scope: 'global', path: join(HOME, '.continue', 'config.yaml') }],
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
id: 'dsh',
|
|
308
|
+
label: { en: 'DeepSeek Harness', zh: 'DeepSeek Harness' },
|
|
309
|
+
order: 5,
|
|
310
|
+
format: 'entry',
|
|
311
|
+
containerKey: 'config',
|
|
312
|
+
dialect: TYPE.plain,
|
|
313
|
+
caps: { ...STDIO_CAPS, enabled: true, oauth: false },
|
|
314
|
+
managed: true,
|
|
315
|
+
note: { en: 'Written through configEditor against @deepseek-ai/dsh-mcp-client; applies hot, no restart.', zh: '通过 configEditor 写入 @deepseek-ai/dsh-mcp-client;热替换即时生效,无需重启。' },
|
|
316
|
+
paths: () => [],
|
|
317
|
+
},
|
|
318
|
+
];
|
|
319
|
+
|
|
320
|
+
export const CLIENT_BY_ID = new Map(CLIENTS.map((client) => [client.id, client]));
|
|
321
|
+
|
|
322
|
+
/** Resolve `relative` paths against a workspace root. */
|
|
323
|
+
export function resolveClientPaths(client, workspaceRoot) {
|
|
324
|
+
const out = [];
|
|
325
|
+
for (const location of client.paths()) {
|
|
326
|
+
if (location.relative) {
|
|
327
|
+
if (!workspaceRoot) continue;
|
|
328
|
+
out.push({ scope: location.scope, path: join(workspaceRoot, location.path), relative: location.path });
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
out.push({ scope: location.scope, path: location.path });
|
|
332
|
+
}
|
|
333
|
+
return out;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** All distinct global paths, used by the scanner. */
|
|
337
|
+
export function globalPaths() {
|
|
338
|
+
const out = [];
|
|
339
|
+
for (const client of CLIENTS) {
|
|
340
|
+
if (client.managed) continue;
|
|
341
|
+
for (const location of resolveClientPaths(client, undefined)) {
|
|
342
|
+
if (location.scope === 'global') out.push({ clientId: client.id, path: location.path });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return out;
|
|
346
|
+
}
|