dsh-mcp-plus 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 sojo-negai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,149 @@
1
+ # dsh-mcp-plus
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
4
+
5
+ **DSH(DeepSeek Harness)增强型 MCP 客户端插件** —— 在 DSH 的设置界面里管理多台 MCP 服务器:增删改配置、实时查看连接状态,服务器的工具自动发现并注册给 Agent 使用。
6
+
7
+ ![dsh-mcp-plus 设置页](docs/screenshots/overview.png)
8
+
9
+ ## 特性
10
+
11
+ - 🖥️ **图形化管理**:在 DSH 设置 → 「MCP 服务器」页直接编辑 JSON 配置,无需手改 yml
12
+ - 🟢 **实时状态**:每台服务器一个状态圆点 —— 🟢 已连接 / 🟡 连接中(闪烁)/ ⚪ 未连接或已禁用
13
+ - 🎛️ **独立启停**:每台服务器支持 `enabled` 开关,禁用不删除,配置保留
14
+ - 🔁 **自动重连**:保存配置后自动重连并重新注册工具,无需重启 DSH
15
+ - 📋 **兼容主流格式**:直接粘贴 Claude Desktop 风格的 `mcpServers` JSON 即可导入
16
+
17
+ ## 前置条件
18
+
19
+ | 依赖 | 版本要求 |
20
+ |---|---|
21
+ | DSH(DeepSeek Harness) | ≥ 0.1.2-alpha.1(host 半与 client 半均在该版本实测加载) |
22
+ | Node.js | ≥ 18 |
23
+ | pnpm | ≥ 9(构建时使用) |
24
+
25
+ ## 安装
26
+
27
+ ### 方式一:脚本安装(推荐)
28
+
29
+ 克隆本仓库后,在项目根目录执行:
30
+
31
+ ```bash
32
+ pnpm install && pnpm build
33
+ node scripts/install.mjs --profile web # 装到 web profile
34
+ # node scripts/install.mjs --all # 或装到所有 profile
35
+ ```
36
+
37
+ 然后在 profile 目录里刷新依赖并重启 DSH:
38
+
39
+ ```bash
40
+ cd ~/.dsh/profiles/web
41
+ pnpm install
42
+ pnpm dsh web
43
+ ```
44
+
45
+ ### 方式二:npm 安装
46
+
47
+ ```bash
48
+ cd ~/.dsh/profiles/web
49
+ pnpm add dsh-mcp-plus
50
+ ```
51
+
52
+ 包内自带 `dsh.bundle.patch` 声明,安装后把 `dsh-mcp-plus` 加进 profile 的 `dsh.profile.bundles` 数组即可激活。
53
+
54
+ ## 使用
55
+
56
+ 1. 启动 DSH,打开 Web 界面
57
+ 2. 侧边栏点「设置」→ 顶部 tabs 选「MCP 服务器」
58
+ 3. 点「修改配置(JSON)」,粘贴/编辑服务器配置,点「保存」
59
+
60
+ ### 配置格式
61
+
62
+ 支持以下三种形态(推荐第一种,与 Claude Desktop 配置兼容):
63
+
64
+ ```jsonc
65
+ // ① 对象映射:键即服务器名(推荐,可直接粘贴 Claude Desktop 的 mcpServers 内容)
66
+ {
67
+ "chrome-devtools": {
68
+ "command": "cmd.exe",
69
+ "args": ["/c", "npx", "-y", "chrome-devtools-mcp@latest"],
70
+ "env": {},
71
+ "cwd": "",
72
+ "enabled": true
73
+ },
74
+ "filesystem": {
75
+ "command": "node",
76
+ "args": ["path/to/mcp-server-filesystem/dist/index.js", "C:/允许访问的目录"]
77
+ }
78
+ }
79
+
80
+ // ② 带包裹层的完整 Claude Desktop 格式(直接整段粘贴也行)
81
+ { "mcpServers": { "服务器名": { "command": "...", "args": ["..."] } } }
82
+
83
+ // ③ 原生数组格式
84
+ [ { "transport": "stdio", "serverName": "名字", "command": "...", "args": ["..."] } ]
85
+ ```
86
+
87
+ **字段说明**:
88
+
89
+ <!-- 截图位 3:JSON 编辑器(展示编辑态 + 底部保存栏)
90
+ ![JSON 编辑](docs/screenshots/json-editor.png)
91
+ -->
92
+
93
+ | 字段 | 必填 | 说明 |
94
+ |---|---|---|
95
+ | `command` | ✅ | 启动 MCP 服务器的可执行文件 |
96
+ | `args` | — | 命令行参数数组 |
97
+ | `env` | — | 额外环境变量(`{ "KEY": "value" }`) |
98
+ | `cwd` | — | 子进程工作目录 |
99
+ | `enabled` | — | `false` = 保留配置但不连接(默认 `true`) |
100
+ | `serverName` | 数组形态必填 | 服务器名(对象形态用键名),`[A-Za-z0-9_-]{1,32}`,用作工具名前缀 |
101
+
102
+ 保存后插件自动连接所有启用中的服务器,发现的工具以 `mcp__<服务器名>__<工具名>` 注册给 Agent。
103
+
104
+ ## 状态说明
105
+
106
+ 列表中每台服务器名字前的圆点表示实时连接状态:
107
+
108
+ - 🟢 **已连接** —— 工具已注册,Agent 可调用
109
+ - 🟡 **连接中**(闪烁)—— 正在启动/握手(`npx` 类命令首次冷启动可能需要较长时间)
110
+ - ⚪ **未连接** —— 连接失败(检查 command/args 是否正确,详见终端日志)
111
+ - ⚪ **已禁用** —— `enabled: false`,配置保留但不连接
112
+
113
+ ## 开发
114
+
115
+ ```bash
116
+ pnpm install
117
+ pnpm build # 构建宿主半 + 浏览器半到 lib/
118
+ ```
119
+
120
+ 源码模式(`pnpm dsh`,带 tsx)启动还需要一个 patch 覆盖层,把 TS 入口挂进 profile。该文件写的是本机绝对路径,不入库,请自行在仓库根创建 `cordis.yml`:
121
+
122
+ ```yaml
123
+ - insert:
124
+ - id: dsh-mcp-plus
125
+ name: 'file://<本仓库绝对路径>/src/dsh-mcp-plus.ts'
126
+ ```
127
+
128
+ 把 `<本仓库绝对路径>` 换成实际路径(正斜杠)后启动:
129
+
130
+ ```bash
131
+ pnpm dsh web --patch ./cordis.yml
132
+ ```
133
+
134
+ 插件装进 profile 后走的是编译产物,不需要这个文件——`cordis.patch.yml` 里的 `name` 用的是包名。
135
+
136
+ 项目结构:
137
+
138
+ ```
139
+ src/
140
+ ├── dsh-mcp-plus.ts # 宿主半入口:连接管理、工具注册、生命周期
141
+ ├── client.ts # 浏览器半入口:设置页 UI(React)
142
+ ├── config.ts # 配置类型与校验(四种 JSON 形态规范化)
143
+ ├── servers-store.ts # servers.json 读写(~/.dsh/dsh-mcp-plus/servers.json)
144
+ └── http.ts # HTTP 端点(UI 与宿主半的读写通道)
145
+ ```
146
+
147
+ ## License
148
+
149
+ MIT
@@ -0,0 +1,6 @@
1
+ # dsh-mcp-plus 的 bundle patch —— 安装到 profile 后自动进配置层
2
+ # id: dsh-mcp-plus 是唯一标识(profile 加载时由 loader 解析 name 找到入口)
3
+ # 这里不放 config——配置由用户在 profile 的 cordis.yml 或 settings UI 中提供
4
+ - insert:
5
+ - id: dsh-mcp-plus
6
+ name: 'dsh-mcp-plus'
@@ -0,0 +1,497 @@
1
+ (function() {
2
+ //#region src/client.ts
3
+ function makeFactory() {
4
+ return (require) => {
5
+ const React = require("react");
6
+ const { jsx: h } = require("react/jsx-runtime");
7
+ const name = "dsh-mcp-plus";
8
+ const inject = ["slots"];
9
+ /** 共享样式 —— 用 DSH 语义 token,暗色主题自动适配 */
10
+ const styles = {
11
+ section: {
12
+ padding: "16px 24px 80px",
13
+ maxWidth: "720px",
14
+ margin: "0 auto",
15
+ position: "relative"
16
+ },
17
+ desc: {
18
+ fontSize: "13px",
19
+ color: "var(--dsw-alias-label-tertiary)",
20
+ margin: "0 0 16px"
21
+ },
22
+ btn: {
23
+ padding: "8px 16px",
24
+ cursor: "pointer",
25
+ borderRadius: "6px",
26
+ border: "1px solid var(--dsw-alias-border-l2)",
27
+ background: "transparent",
28
+ color: "var(--dsw-alias-label-primary)"
29
+ },
30
+ btnPrimary: {
31
+ padding: "8px 20px",
32
+ cursor: "pointer",
33
+ borderRadius: "6px",
34
+ border: "1px solid var(--dsw-alias-state-business-primary)",
35
+ background: "var(--dsw-alias-state-business-primary)",
36
+ color: "var(--dsw-alias-label-primary)",
37
+ fontWeight: 600
38
+ },
39
+ row: {
40
+ display: "flex",
41
+ alignItems: "center",
42
+ gap: "12px",
43
+ padding: "10px 12px",
44
+ marginBottom: "6px",
45
+ borderRadius: "8px",
46
+ border: "1px solid var(--dsw-alias-border-l2)",
47
+ background: "transparent"
48
+ },
49
+ rowCellName: {
50
+ flex: "0 0 180px",
51
+ fontWeight: 600,
52
+ fontSize: "13px",
53
+ color: "var(--dsw-alias-label-primary)",
54
+ overflow: "hidden",
55
+ textOverflow: "ellipsis",
56
+ whiteSpace: "nowrap"
57
+ },
58
+ rowCellCmd: {
59
+ flex: 1,
60
+ fontSize: "12px",
61
+ color: "var(--dsw-alias-label-tertiary)",
62
+ fontFamily: "monospace",
63
+ overflow: "hidden",
64
+ textOverflow: "ellipsis",
65
+ whiteSpace: "nowrap"
66
+ },
67
+ rowActions: {
68
+ display: "flex",
69
+ gap: "4px"
70
+ },
71
+ iconBtn: {
72
+ padding: "4px 8px",
73
+ cursor: "pointer",
74
+ fontSize: "12px",
75
+ borderRadius: "4px",
76
+ border: "none",
77
+ background: "transparent",
78
+ color: "var(--dsw-alias-label-tertiary)"
79
+ },
80
+ stickyBar: {
81
+ position: "sticky",
82
+ bottom: 0,
83
+ padding: "12px 20px",
84
+ background: "transparent",
85
+ backdropFilter: "blur(8px)",
86
+ WebkitBackdropFilter: "blur(8px)",
87
+ borderTop: "1px solid var(--dsw-alias-border-l2)",
88
+ display: "flex",
89
+ alignItems: "center",
90
+ justifyContent: "space-between",
91
+ gap: "12px",
92
+ width: "calc(100% - 48px)",
93
+ maxWidth: "672px",
94
+ margin: "24px auto 0",
95
+ borderRadius: "10px",
96
+ border: "1px solid var(--dsw-alias-border-l2)",
97
+ boxShadow: "0 2px 12px var(--dsw-alias-bg-mask-1, rgba(0,0,0,0.12))"
98
+ },
99
+ stickySummary: {
100
+ display: "flex",
101
+ flexDirection: "column",
102
+ gap: "2px",
103
+ minWidth: 0
104
+ },
105
+ stickyCount: {
106
+ fontSize: "13px",
107
+ color: "var(--dsw-alias-label-primary)",
108
+ fontWeight: 600
109
+ },
110
+ stickyHint: {
111
+ fontSize: "11px",
112
+ color: "var(--dsw-alias-label-tertiary)"
113
+ },
114
+ msgErr: {
115
+ fontSize: "13px",
116
+ color: "var(--dsw-alias-state-error-primary)"
117
+ },
118
+ textarea: {
119
+ width: "100%",
120
+ boxSizing: "border-box",
121
+ padding: "10px 12px",
122
+ borderRadius: "8px",
123
+ border: "1px solid var(--dsw-alias-border-l2)",
124
+ background: "transparent",
125
+ color: "var(--dsw-alias-label-primary)",
126
+ fontFamily: "monospace",
127
+ fontSize: "13px",
128
+ lineHeight: "1.5",
129
+ minHeight: "320px",
130
+ maxHeight: "60vh",
131
+ resize: "vertical"
132
+ },
133
+ modalBackdrop: {
134
+ position: "fixed",
135
+ top: 0,
136
+ left: 0,
137
+ right: 0,
138
+ bottom: 0,
139
+ background: "rgba(0,0,0,0.5)",
140
+ display: "flex",
141
+ alignItems: "center",
142
+ justifyContent: "center",
143
+ zIndex: 1e3
144
+ },
145
+ modalCard: {
146
+ background: "var(--dsw-alias-bg-layer-1)",
147
+ borderRadius: "12px",
148
+ padding: "24px",
149
+ maxWidth: "480px",
150
+ width: "90%",
151
+ maxHeight: "80vh",
152
+ overflowY: "auto",
153
+ border: "1px solid var(--dsw-alias-border-l2)"
154
+ }
155
+ };
156
+ /** 复选框风格 */
157
+ function Checkbox(props) {
158
+ return h("input", {
159
+ type: "checkbox",
160
+ checked: props.checked,
161
+ onChange: (e) => props.onChange(e.target.checked),
162
+ style: {
163
+ width: "16px",
164
+ height: "16px",
165
+ cursor: "pointer"
166
+ }
167
+ });
168
+ }
169
+ /** 把 ServerRow 数组转为 JSON 字符串(给文本域预填) */
170
+ function rowsToJsonText(rows) {
171
+ const map = {};
172
+ for (const r of rows) {
173
+ const { serverName, ...cfg } = r;
174
+ map[serverName] = cfg;
175
+ }
176
+ return JSON.stringify(map, null, 2);
177
+ }
178
+ /** 把任意 JSON 文本解析为 ServerRow 数组(宽容:支持四种形态) */
179
+ function parseJsonText(text) {
180
+ const parsed = JSON.parse(text);
181
+ const root = parsed && typeof parsed === "object" && "mcpServers" in parsed ? parsed.mcpServers : parsed;
182
+ if (root && typeof root === "object" && !Array.isArray(root) && !("servers" in root)) return Object.entries(root).map(([serverName, cfg]) => ({
183
+ serverName,
184
+ command: typeof cfg.command === "string" ? cfg.command : "",
185
+ args: Array.isArray(cfg.args) ? cfg.args : [],
186
+ env: cfg.env && typeof cfg.env === "object" ? cfg.env : {},
187
+ cwd: typeof cfg.cwd === "string" ? cfg.cwd : "",
188
+ enabled: cfg.enabled !== false
189
+ }));
190
+ if (Array.isArray(parsed)) return parsed.map((raw) => {
191
+ const cfg = raw && typeof raw === "object" ? raw : {};
192
+ return {
193
+ serverName: typeof cfg.serverName === "string" ? cfg.serverName : "",
194
+ command: typeof cfg.command === "string" ? cfg.command : "",
195
+ args: Array.isArray(cfg.args) ? cfg.args : [],
196
+ env: cfg.env && typeof cfg.env === "object" ? cfg.env : {},
197
+ cwd: typeof cfg.cwd === "string" ? cfg.cwd : "",
198
+ enabled: cfg.enabled !== false
199
+ };
200
+ });
201
+ if (root && typeof root === "object" && "servers" in root) return parseJsonText(JSON.stringify(root.servers));
202
+ throw new Error("JSON 顶层必须是对象映射或数组");
203
+ }
204
+ /** 主组件 */
205
+ function McpServersSection() {
206
+ const [rows, setRows] = React.useState(null);
207
+ const [mode, setMode] = React.useState("list");
208
+ const [jsonText, setJsonText] = React.useState("");
209
+ const [adding, setAdding] = React.useState(false);
210
+ const [busy, setBusy] = React.useState(false);
211
+ const [dirty, setDirty] = React.useState(false);
212
+ React.useEffect(() => {
213
+ refresh().catch(() => {
214
+ setRows([]);
215
+ setJsonText("{}");
216
+ alert("加载失败:无法连接 host 端点");
217
+ });
218
+ }, []);
219
+ /** 从 host 拉取服务器列表(含状态)并更新 UI */
220
+ const refresh = () => fetch("/dsh-mcp-plus/servers").then((r) => r.json()).then((d) => {
221
+ const reloaded = (d.servers ?? []).map((raw) => {
222
+ const s = raw;
223
+ const status = s.status === "connecting" || s.status === "connected" || s.status === "disconnected" || s.status === "disabled" ? s.status : "connecting";
224
+ return {
225
+ serverName: typeof s.serverName === "string" ? s.serverName : "",
226
+ command: typeof s.command === "string" ? s.command : "",
227
+ args: Array.isArray(s.args) ? s.args : [],
228
+ env: s.env && typeof s.env === "object" ? s.env : {},
229
+ cwd: typeof s.cwd === "string" ? s.cwd : "",
230
+ enabled: s.enabled !== false,
231
+ status
232
+ };
233
+ });
234
+ setRows(reloaded);
235
+ setJsonText(rowsToJsonText(reloaded));
236
+ return reloaded;
237
+ });
238
+ /** 保存:只写盘,立即完成;重连由 host 异步做,状态经轮询圆点展示 */
239
+ const save = (payload) => {
240
+ setBusy(true);
241
+ const clean = payload.map(({ serverName, command, args, env, cwd, enabled }) => ({
242
+ transport: "stdio",
243
+ serverName,
244
+ command,
245
+ args,
246
+ env,
247
+ cwd,
248
+ ...enabled !== void 0 ? { enabled } : {}
249
+ }));
250
+ fetch("/dsh-mcp-plus/servers", {
251
+ method: "PUT",
252
+ headers: { "Content-Type": "application/json" },
253
+ body: JSON.stringify({ servers: clean })
254
+ }).then((r) => r.json()).then((data) => {
255
+ if (data.ok) {
256
+ refresh();
257
+ let polls = 0;
258
+ const timer = setInterval(() => {
259
+ polls += 1;
260
+ refresh().then((list) => {
261
+ if (!list.some((r) => r.status === "connecting") || polls >= 90) clearInterval(timer);
262
+ }).catch(() => clearInterval(timer));
263
+ }, 1e3);
264
+ } else alert("保存失败:" + (data.error ?? "未知错误"));
265
+ }).catch((err) => {
266
+ alert("保存失败:" + String(err));
267
+ }).finally(() => setBusy(false));
268
+ };
269
+ if (rows === null) return h("div", {
270
+ style: {
271
+ ...styles.section,
272
+ color: "var(--muted-foreground, #888)"
273
+ },
274
+ children: "加载中…"
275
+ });
276
+ const toggleButton = mode === "list" ? h("button", {
277
+ style: styles.btn,
278
+ onClick: () => {
279
+ setMode("json");
280
+ setJsonText(rowsToJsonText(rows));
281
+ },
282
+ children: "修改配置(JSON)"
283
+ }) : h("button", {
284
+ style: styles.btn,
285
+ onClick: () => {
286
+ setMode("list");
287
+ },
288
+ children: "关闭编辑"
289
+ });
290
+ const topbar = h("div", {
291
+ style: {
292
+ display: "flex",
293
+ justifyContent: "space-between",
294
+ alignItems: "center",
295
+ marginBottom: "16px"
296
+ },
297
+ children: [h("div", { children: [h("h2", {
298
+ style: { margin: "0 0 4px" },
299
+ children: "MCP 服务器"
300
+ }), h("p", {
301
+ style: styles.desc,
302
+ children: `共 ${rows.length} 台服务器,${rows.filter((r) => r.enabled).length} 台启用中。`
303
+ })] }), toggleButton]
304
+ });
305
+ const listView = h("div", { children: [rows.length === 0 ? h("p", {
306
+ style: styles.desc,
307
+ children: "暂无服务器。点击右上角「修改配置(JSON)」添加。"
308
+ }) : h("div", {
309
+ style: { marginBottom: "16px" },
310
+ children: rows.map((row, i) => {
311
+ const cmd = [row.command, ...row.args].filter(Boolean).join(" ");
312
+ const dimName = row.enabled ? styles.rowCellName : {
313
+ ...styles.rowCellName,
314
+ color: "var(--dsw-alias-label-tertiary)",
315
+ fontWeight: 400
316
+ };
317
+ const dimCmd = row.enabled ? styles.rowCellCmd : {
318
+ ...styles.rowCellCmd,
319
+ color: "var(--dsw-alias-label-tertiary)"
320
+ };
321
+ const dotColor = row.status === "connected" ? "var(--dsw-alias-state-success-primary)" : row.status === "connecting" ? "var(--dsw-alias-state-warn-primary)" : "var(--dsw-alias-label-tertiary)";
322
+ const statusLabel = row.status === "connected" ? "已连接" : row.status === "connecting" ? "连接中…" : row.status === "disabled" ? "已禁用" : "未连接";
323
+ return h("div", {
324
+ key: row.serverName || String(i),
325
+ style: styles.row,
326
+ children: [
327
+ h(Checkbox, {
328
+ checked: row.enabled,
329
+ onChange: (v) => {
330
+ setRows(rows.map((r, j) => j === i ? {
331
+ ...r,
332
+ enabled: v
333
+ } : r));
334
+ setDirty(true);
335
+ }
336
+ }),
337
+ h("div", {
338
+ style: {
339
+ ...dimName,
340
+ display: "flex",
341
+ alignItems: "center",
342
+ gap: "8px"
343
+ },
344
+ children: [h("span", {
345
+ title: statusLabel,
346
+ "data-status": row.status,
347
+ style: {
348
+ width: "10px",
349
+ height: "10px",
350
+ borderRadius: "50%",
351
+ background: dotColor,
352
+ display: "inline-block",
353
+ flex: "0 0 auto",
354
+ boxShadow: row.status === "connected" ? `0 0 4px ${dotColor}` : "none",
355
+ animation: row.status === "connecting" ? "dshMcpPlusPulse 1s ease-in-out infinite" : void 0
356
+ }
357
+ }), row.serverName || "(未命名)"]
358
+ }),
359
+ h("div", {
360
+ style: dimCmd,
361
+ title: cmd,
362
+ children: cmd
363
+ })
364
+ ]
365
+ });
366
+ })
367
+ }), h("p", {
368
+ style: styles.desc,
369
+ children: "通过「修改配置(JSON)」增删服务器。"
370
+ })] });
371
+ const jsonView = h("div", { children: [h("p", {
372
+ style: styles.desc,
373
+ children: "直接编辑 JSON(支持对象映射 { 名字: { command, args, env, cwd, enabled } } 或数组)。"
374
+ }), h("textarea", {
375
+ style: styles.textarea,
376
+ value: jsonText,
377
+ spellCheck: false,
378
+ onKeyDown: (e) => {
379
+ if (e.ctrlKey && e.key === "Enter") {
380
+ e.preventDefault();
381
+ saveDirtyJson();
382
+ }
383
+ },
384
+ onChange: (e) => {
385
+ setJsonText(e.target.value);
386
+ setDirty(true);
387
+ }
388
+ })] });
389
+ const enabledCount = rows.filter((r) => r.enabled).length;
390
+ const disabledCount = rows.length - enabledCount;
391
+ /** JSON 模式保存:先解析预检,成功才发请求(Ctrl+Enter 和按钮共用) */
392
+ const saveDirtyJson = () => {
393
+ try {
394
+ save(parseJsonText(jsonText));
395
+ } catch (err) {
396
+ alert("JSON 解析失败:" + (err instanceof Error ? err.message : String(err)));
397
+ }
398
+ };
399
+ const sticky = h("div", {
400
+ style: styles.stickyBar,
401
+ children: [h("div", {
402
+ style: styles.stickySummary,
403
+ children: [h("div", {
404
+ style: styles.stickyCount,
405
+ children: `${enabledCount} 台启用中${disabledCount > 0 ? `,${disabledCount} 台已禁用` : ""}`
406
+ }), h("div", {
407
+ style: styles.stickyHint,
408
+ children: dirty ? "有未保存的修改" : "保存后将自动重连"
409
+ })]
410
+ }), h("div", {
411
+ style: {
412
+ display: "flex",
413
+ gap: "8px"
414
+ },
415
+ children: [h("button", {
416
+ style: styles.btn,
417
+ onClick: () => {
418
+ fetch("/dsh-mcp-plus/servers").then((r) => r.json()).then((d) => {
419
+ const reloaded = (d.servers ?? []).map((raw) => {
420
+ const s = raw;
421
+ const status = s.status === "connected" || s.status === "disconnected" || s.status === "disabled" ? s.status : "disconnected";
422
+ return {
423
+ serverName: typeof s.serverName === "string" ? s.serverName : "",
424
+ command: typeof s.command === "string" ? s.command : "",
425
+ args: Array.isArray(s.args) ? s.args : [],
426
+ env: s.env && typeof s.env === "object" ? s.env : {},
427
+ cwd: typeof s.cwd === "string" ? s.cwd : "",
428
+ enabled: s.enabled !== false,
429
+ status
430
+ };
431
+ });
432
+ setRows(reloaded);
433
+ setJsonText(rowsToJsonText(reloaded));
434
+ setMode("list");
435
+ setDirty(false);
436
+ }).catch(() => alert("取消失败:无法从 host 重新加载"));
437
+ },
438
+ children: "取消"
439
+ }), h("button", {
440
+ style: {
441
+ ...styles.btnPrimary,
442
+ opacity: busy ? .5 : 1,
443
+ outline: dirty ? "2px solid var(--dsw-alias-state-business-primary)" : "none",
444
+ outlineOffset: "2px"
445
+ },
446
+ disabled: busy,
447
+ onClick: () => {
448
+ if (mode === "list") {
449
+ save(rows);
450
+ return;
451
+ }
452
+ saveDirtyJson();
453
+ },
454
+ children: busy ? "保存中…" : "保存"
455
+ })]
456
+ })]
457
+ });
458
+ return h("div", {
459
+ style: styles.section,
460
+ children: [
461
+ topbar,
462
+ mode === "list" ? listView : jsonView,
463
+ sticky
464
+ ]
465
+ });
466
+ }
467
+ function apply(ctx) {
468
+ const c = ctx;
469
+ if (typeof document !== "undefined" && !document.getElementById("dsh-mcp-plus-styles")) {
470
+ const style = document.createElement("style");
471
+ style.id = "dsh-mcp-plus-styles";
472
+ style.textContent = "@keyframes dshMcpPlusPulse { 0%,100% { opacity: 1 } 50% { opacity: 0.35 } }";
473
+ document.head.appendChild(style);
474
+ }
475
+ c.slots.inject("settings.section", function* () {
476
+ yield c.slots.register({
477
+ name: "settings.section",
478
+ id: "mcp-servers",
479
+ order: 40,
480
+ label: () => "MCP 服务器",
481
+ inject: () => ({})
482
+ }, McpServersSection);
483
+ });
484
+ }
485
+ return {
486
+ name,
487
+ inject,
488
+ apply
489
+ };
490
+ };
491
+ }
492
+ window.__ModuleLoader__.load({
493
+ id: "dsh-mcp-plus",
494
+ factory: makeFactory()
495
+ });
496
+ //#endregion
497
+ })();