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 +21 -0
- package/README.md +149 -0
- package/cordis.patch.yml +6 -0
- package/lib/client.iife.js +497 -0
- package/lib/index.mjs +328 -0
- package/package.json +62 -0
- package/scripts/install.mjs +105 -0
- package/src/client.ts +482 -0
- package/src/config.ts +108 -0
- package/src/dsh-mcp-plus.ts +197 -0
- package/src/http.ts +116 -0
- package/src/servers-store.ts +44 -0
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)
|
|
4
|
+
|
|
5
|
+
**DSH(DeepSeek Harness)增强型 MCP 客户端插件** —— 在 DSH 的设置界面里管理多台 MCP 服务器:增删改配置、实时查看连接状态,服务器的工具自动发现并注册给 Agent 使用。
|
|
6
|
+
|
|
7
|
+

|
|
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
|
+

|
|
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
|
package/cordis.patch.yml
ADDED
|
@@ -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
|
+
})();
|