@sunt2049/dsh-mcp-manager 0.0.0-stage → 0.3.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/client.js ADDED
@@ -0,0 +1,1289 @@
1
+ window.__ModuleLoader__.load({
2
+ id: '@sunt2049/dsh-mcp-manager',
3
+ factory(require) {
4
+ const React = require('react')
5
+ const { useState, useEffect, useMemo, useCallback } = React
6
+ const h = React.createElement
7
+
8
+ const MONO = 'ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace'
9
+
10
+ const C = {
11
+ surface: 'var(--dsw-alias-bg-layer-1)',
12
+ nested: 'var(--dsw-alias-bg-layer-2)',
13
+ border: 'var(--dsw-alias-border-l1)',
14
+ borderStrong: 'var(--dsw-alias-border-l2)',
15
+ text: 'var(--dsw-alias-label-primary)',
16
+ dim: 'var(--dsw-alias-label-secondary)',
17
+ brand: 'var(--dsw-alias-brand-primary)',
18
+ ok: 'var(--dsw-alias-state-success-primary)',
19
+ warn: 'var(--dsw-alias-state-warn-primary)',
20
+ err: 'var(--dsw-alias-state-error-primary)',
21
+ idle: 'var(--dsw-alias-state-idle-primary)',
22
+ }
23
+
24
+ async function getJson(path) {
25
+ try {
26
+ const response = await fetch(path, { headers: { accept: 'application/json' } })
27
+ return await response.json()
28
+ } catch (error) {
29
+ return { ok: false, error: String((error && error.message) || error) }
30
+ }
31
+ }
32
+
33
+ async function postJson(path, body) {
34
+ try {
35
+ const response = await fetch(path, {
36
+ method: 'POST',
37
+ headers: { 'content-type': 'application/json' },
38
+ body: JSON.stringify(body),
39
+ })
40
+ return await response.json()
41
+ } catch (error) {
42
+ return { ok: false, error: String((error && error.message) || error) }
43
+ }
44
+ }
45
+
46
+ function copyText(value) {
47
+ try {
48
+ navigator.clipboard.writeText(String(value))
49
+ return true
50
+ } catch {
51
+ return false
52
+ }
53
+ }
54
+
55
+ function endpointOf(server) {
56
+ if (server.transport === 'streamable-http') return server.url || '(未配置 url)'
57
+ if (server.transport === 'stdio') {
58
+ const args = server.args && server.args.length ? ' ' + server.args.join(' ') : ''
59
+ return (server.command || '(未配置 command)') + args
60
+ }
61
+ return server.transport || '(未知传输)'
62
+ }
63
+
64
+ function statusOf(server) {
65
+ if (server.mounted === false) return { color: C.warn, label: '尚未挂载' }
66
+ if (!server.transport) return { color: C.err, label: '配置不完整' }
67
+ if (!server.tools.length) return { color: C.warn, label: '无工具' }
68
+ return { color: C.ok, label: '已注册' }
69
+ }
70
+
71
+ function parseEnv(text) {
72
+ const env = {}
73
+ for (const raw of String(text || '').split('\n')) {
74
+ const line = raw.trim()
75
+ if (!line || line.startsWith('#')) continue
76
+ const cut = line.indexOf('=')
77
+ if (cut <= 0) continue
78
+ env[line.slice(0, cut).trim()] = line.slice(cut + 1)
79
+ }
80
+ return env
81
+ }
82
+
83
+ /** `Name: Value` per line; the first colon separates, so values may contain colons. */
84
+ function parseHeaders(text) {
85
+ const headers = {}
86
+ for (const raw of String(text || '').split('\n')) {
87
+ const line = raw.trim()
88
+ if (!line || line.startsWith('#')) continue
89
+ const cut = line.indexOf(':')
90
+ if (cut <= 0) continue
91
+ const name = line.slice(0, cut).trim()
92
+ const value = line.slice(cut + 1).trim()
93
+ if (name && value) headers[name] = value
94
+ }
95
+ return headers
96
+ }
97
+
98
+ function verdictLabel(verdict) {
99
+ switch (verdict) {
100
+ case 'port-closed':
101
+ return '端口不通'
102
+ case 'handshake-failed':
103
+ return '端口通,握手失败'
104
+ case 'handshake-ok':
105
+ return '握手成功'
106
+ case 'files-missing':
107
+ return '启动文件缺失'
108
+ case 'files-ok':
109
+ return '启动文件齐全'
110
+ case 'bad-config':
111
+ return '配置不完整'
112
+ default:
113
+ return '无法判断'
114
+ }
115
+ }
116
+
117
+ const S = {
118
+ root: { display: 'flex', flexDirection: 'column', gap: '14px', color: C.text, fontSize: '13px' },
119
+ head: { display: 'flex', alignItems: 'flex-start', gap: '12px', flexWrap: 'wrap' },
120
+ title: { margin: 0, fontSize: '16px', fontWeight: 650, letterSpacing: '.2px' },
121
+ sub: { margin: '3px 0 0', color: C.dim, fontSize: '12px' },
122
+ spacer: { flex: 1 },
123
+ btn: {
124
+ appearance: 'none',
125
+ border: `1px solid ${C.border}`,
126
+ background: C.surface,
127
+ color: C.text,
128
+ borderRadius: '8px',
129
+ padding: '5px 11px',
130
+ fontSize: '12px',
131
+ cursor: 'pointer',
132
+ fontFamily: 'inherit',
133
+ lineHeight: 1.5,
134
+ },
135
+ danger: { borderColor: C.err, color: C.err },
136
+ input: {
137
+ width: '100%',
138
+ boxSizing: 'border-box',
139
+ border: `1px solid ${C.border}`,
140
+ background: C.nested,
141
+ color: C.text,
142
+ borderRadius: '8px',
143
+ padding: '7px 10px',
144
+ fontSize: '12px',
145
+ fontFamily: 'inherit',
146
+ outline: 'none',
147
+ },
148
+ card: { border: `1px solid ${C.border}`, background: C.surface, borderRadius: '12px', overflow: 'hidden' },
149
+ cardHead: {
150
+ display: 'flex',
151
+ alignItems: 'center',
152
+ gap: '10px',
153
+ width: '100%',
154
+ textAlign: 'left',
155
+ padding: '11px 13px',
156
+ background: 'transparent',
157
+ border: 0,
158
+ color: C.text,
159
+ cursor: 'pointer',
160
+ fontFamily: 'inherit',
161
+ fontSize: '13px',
162
+ },
163
+ dot: { width: '8px', height: '8px', borderRadius: '50%', flex: '0 0 auto' },
164
+ name: { fontWeight: 620, flex: '0 0 auto' },
165
+ chip: {
166
+ border: `1px solid ${C.border}`,
167
+ color: C.dim,
168
+ borderRadius: '999px',
169
+ padding: '1px 8px',
170
+ fontSize: '11px',
171
+ flex: '0 0 auto',
172
+ fontFamily: MONO,
173
+ },
174
+ endpoint: {
175
+ fontFamily: MONO,
176
+ fontSize: '11.5px',
177
+ color: C.dim,
178
+ overflow: 'hidden',
179
+ textOverflow: 'ellipsis',
180
+ whiteSpace: 'nowrap',
181
+ flex: '1 1 auto',
182
+ minWidth: 0,
183
+ },
184
+ count: { color: C.dim, fontSize: '11.5px', flex: '0 0 auto' },
185
+ body: { borderTop: `1px solid ${C.border}`, padding: '6px 8px 8px' },
186
+ meta: {
187
+ display: 'flex',
188
+ flexWrap: 'wrap',
189
+ gap: '6px 14px',
190
+ padding: '2px 7px 8px',
191
+ color: C.dim,
192
+ fontSize: '11.5px',
193
+ },
194
+ tool: {
195
+ display: 'flex',
196
+ gap: '10px',
197
+ alignItems: 'baseline',
198
+ width: '100%',
199
+ textAlign: 'left',
200
+ border: 0,
201
+ background: 'transparent',
202
+ color: C.text,
203
+ padding: '7px 8px',
204
+ borderRadius: '7px',
205
+ cursor: 'pointer',
206
+ fontFamily: 'inherit',
207
+ },
208
+ toolName: { fontFamily: MONO, fontSize: '12px', color: C.brand, flex: '0 0 auto' },
209
+ toolDesc: {
210
+ color: C.dim,
211
+ fontSize: '11.5px',
212
+ overflow: 'hidden',
213
+ textOverflow: 'ellipsis',
214
+ whiteSpace: 'nowrap',
215
+ flex: '1 1 auto',
216
+ minWidth: 0,
217
+ },
218
+ pre: {
219
+ margin: '2px 8px 8px',
220
+ padding: '10px 12px',
221
+ borderRadius: '8px',
222
+ border: `1px solid ${C.border}`,
223
+ background: C.nested,
224
+ color: C.text,
225
+ fontFamily: MONO,
226
+ fontSize: '11.5px',
227
+ lineHeight: 1.55,
228
+ whiteSpace: 'pre-wrap',
229
+ wordBreak: 'break-word',
230
+ maxHeight: '340px',
231
+ overflow: 'auto',
232
+ },
233
+ notice: {
234
+ border: `1px solid ${C.border}`,
235
+ borderLeft: `3px solid ${C.warn}`,
236
+ background: C.nested,
237
+ borderRadius: '8px',
238
+ padding: '8px 11px',
239
+ fontSize: '12px',
240
+ color: C.dim,
241
+ },
242
+ error: { borderLeftColor: C.err, color: C.err },
243
+ form: {
244
+ display: 'grid',
245
+ gap: '9px',
246
+ padding: '12px',
247
+ borderTop: `1px solid ${C.border}`,
248
+ background: C.nested,
249
+ },
250
+ field: { display: 'grid', gap: '4px' },
251
+ label: { fontSize: '11.5px', color: C.dim },
252
+ row: { display: 'flex', gap: '8px', alignItems: 'center', flexWrap: 'wrap' },
253
+ empty: { color: C.dim, fontSize: '12px', padding: '10px 12px' },
254
+ }
255
+
256
+ function Chip(props) {
257
+ return h('span', { style: S.chip }, props.children)
258
+ }
259
+
260
+ function Notice(props) {
261
+ return h('div', { style: props.error ? { ...S.notice, ...S.error } : S.notice }, props.children)
262
+ }
263
+
264
+ function SchemaBlock(props) {
265
+ const entry = props.entry
266
+ if (!entry) return null
267
+ if (entry.state === 'loading') return h('div', { style: S.empty }, '正在读取 schema…')
268
+ if (entry.state === 'error') return h(Notice, { error: true }, entry.error)
269
+
270
+ const json = JSON.stringify(entry.parameters, null, 2)
271
+ return h(
272
+ 'div',
273
+ { style: { display: 'grid', gap: '6px' } },
274
+ h(
275
+ 'div',
276
+ { style: { ...S.row, padding: '0 8px' } },
277
+ h('span', { style: { color: C.dim, fontSize: '11.5px' } }, 'JSON Schema · parameters'),
278
+ h('span', { style: S.spacer }),
279
+ h(
280
+ 'button',
281
+ { style: { ...S.btn, padding: '2px 9px', fontSize: '11.5px' }, onClick: () => copyText(json) },
282
+ '复制',
283
+ ),
284
+ ),
285
+ h('pre', { style: S.pre }, json),
286
+ )
287
+ }
288
+
289
+ function field(label, control) {
290
+ return h('div', { style: S.field }, h('span', { style: S.label }, label), control)
291
+ }
292
+
293
+ const HEADER_NAME = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/
294
+
295
+ function EditForm(props) {
296
+ const server = props.server
297
+ const isHttp = server.transport === 'streamable-http'
298
+ const [url, setUrl] = useState(server.url || '')
299
+ const [command, setCommand] = useState(server.command || '')
300
+ const [argsText, setArgsText] = useState((server.args || []).join('\n'))
301
+ const [timeout, setTimeoutMs] = useState(
302
+ server.toolCallTimeoutMs === null ? '' : String(server.toolCallTimeoutMs),
303
+ )
304
+ const [failFast, setFailFast] = useState(server.failOnStartupError === true)
305
+ const [headerRows, setHeaderRows] = useState([{ name: '', value: '' }])
306
+ const [dropped, setDropped] = useState([])
307
+ const [busy, setBusy] = useState(false)
308
+ const [failure, setFailure] = useState(null)
309
+
310
+ const save = useCallback(async () => {
311
+ const patch = { failOnStartupError: failFast }
312
+ if (timeout.trim() !== '') {
313
+ const value = Number(timeout)
314
+ if (!Number.isFinite(value) || value <= 0) {
315
+ setFailure('超时必须是正数毫秒')
316
+ return
317
+ }
318
+ patch.toolCallTimeoutMs = value
319
+ }
320
+ if (isHttp) {
321
+ if (!url.trim()) {
322
+ setFailure('url 不能为空')
323
+ return
324
+ }
325
+ patch.url = url.trim()
326
+
327
+ const headers = {}
328
+ for (const name of dropped) headers[name] = ''
329
+ for (const row of headerRows) {
330
+ const name = row.name.trim()
331
+ if (!name) continue
332
+ if (!HEADER_NAME.test(name)) {
333
+ setFailure(`请求头名不合法:${name}`)
334
+ return
335
+ }
336
+ headers[name] = row.value
337
+ }
338
+ if (Object.keys(headers).length > 0) patch.headers = headers
339
+ } else {
340
+ if (!command.trim()) {
341
+ setFailure('command 不能为空')
342
+ return
343
+ }
344
+ patch.command = command.trim()
345
+ patch.args = argsText
346
+ .split('\n')
347
+ .map((line) => line.trim())
348
+ .filter((line) => line.length > 0)
349
+ }
350
+ setBusy(true)
351
+ setFailure(null)
352
+ const result = await postJson('/mcp-manager/save', { id: server.id, patch })
353
+ setBusy(false)
354
+ if (!result || !result.ok) {
355
+ setFailure((result && result.error) || '保存失败')
356
+ return
357
+ }
358
+ props.onSaved()
359
+ }, [argsText, command, dropped, failFast, headerRows, isHttp, props, timeout, url])
360
+
361
+ return h(
362
+ 'div',
363
+ { style: S.form },
364
+ isHttp
365
+ ? field(
366
+ 'url(streamable-http)',
367
+ h('input', { style: S.input, value: url, onChange: (e) => setUrl(e.target.value), spellCheck: false }),
368
+ )
369
+ : h(
370
+ React.Fragment,
371
+ null,
372
+ field(
373
+ 'command(stdio)',
374
+ h('input', {
375
+ style: S.input,
376
+ value: command,
377
+ onChange: (e) => setCommand(e.target.value),
378
+ spellCheck: false,
379
+ }),
380
+ ),
381
+ field(
382
+ 'args(每行一个)',
383
+ h('textarea', {
384
+ style: { ...S.input, minHeight: '62px', resize: 'vertical' },
385
+ value: argsText,
386
+ onChange: (e) => setArgsText(e.target.value),
387
+ spellCheck: false,
388
+ }),
389
+ ),
390
+ ),
391
+ isHttp
392
+ ? field(
393
+ '请求头(可选,同名覆盖;清空值不影响已有值)',
394
+ h(
395
+ 'div',
396
+ null,
397
+ (server.headerKeys || []).length > 0
398
+ ? h(
399
+ 'div',
400
+ { style: { ...S.row, gap: '5px', flexWrap: 'wrap', marginBottom: '6px' } },
401
+ (server.headerKeys || []).map((name) =>
402
+ dropped.includes(name)
403
+ ? h(
404
+ 'button',
405
+ {
406
+ key: name,
407
+ style: { ...S.btn, padding: '1px 7px', fontSize: '11.5px', textDecoration: 'line-through', opacity: 0.55 },
408
+ title: '撤销删除',
409
+ onClick: () => setDropped((prev) => prev.filter((item) => item !== name)),
410
+ },
411
+ name,
412
+ )
413
+ : h(
414
+ 'button',
415
+ {
416
+ key: name,
417
+ style: { ...S.btn, padding: '1px 7px', fontSize: '11.5px' },
418
+ title: '点击标记删除',
419
+ onClick: () => setDropped((prev) => [...prev, name]),
420
+ },
421
+ name,
422
+ h('span', { style: { color: C.dim, marginLeft: '4px' } }, '×'),
423
+ ),
424
+ ),
425
+ )
426
+ : null,
427
+ headerRows.map((row, index) =>
428
+ h(
429
+ 'div',
430
+ { key: 'h' + index, style: { ...S.row, gap: '5px', marginBottom: '4px' } },
431
+ h('input', {
432
+ style: { ...S.input, flex: '0 0 38%' },
433
+ placeholder: 'Header 名',
434
+ value: row.name,
435
+ spellCheck: false,
436
+ onChange: (event) =>
437
+ setHeaderRows((prev) =>
438
+ prev.map((item, at) => (at === index ? { ...item, name: event.target.value } : item)),
439
+ ),
440
+ }),
441
+ h('input', {
442
+ style: { ...S.input, flex: '1 1 auto' },
443
+ placeholder: '值',
444
+ value: row.value,
445
+ spellCheck: false,
446
+ onChange: (event) =>
447
+ setHeaderRows((prev) =>
448
+ prev.map((item, at) => (at === index ? { ...item, value: event.target.value } : item)),
449
+ ),
450
+ }),
451
+ h(
452
+ 'button',
453
+ {
454
+ style: S.btn,
455
+ onClick: () => setHeaderRows((prev) => prev.filter((_, at) => at !== index)),
456
+ title: '移除此行',
457
+ },
458
+ '×',
459
+ ),
460
+ ),
461
+ ),
462
+ h(
463
+ 'button',
464
+ {
465
+ style: { ...S.btn, marginTop: '2px' },
466
+ onClick: () => setHeaderRows((prev) => [...prev, { name: '', value: '' }]),
467
+ },
468
+ '+ 添加请求头',
469
+ ),
470
+ ),
471
+ )
472
+ : null,
473
+ field(
474
+ 'toolCallTimeoutMs(留空表示用默认 60000)',
475
+ h('input', {
476
+ style: S.input,
477
+ value: timeout,
478
+ onChange: (e) => setTimeoutMs(e.target.value),
479
+ inputMode: 'numeric',
480
+ spellCheck: false,
481
+ }),
482
+ ),
483
+ h(
484
+ 'label',
485
+ { style: { ...S.row, gap: '7px', fontSize: '12px', color: C.dim, cursor: 'pointer' } },
486
+ h('input', { type: 'checkbox', checked: failFast, onChange: (e) => setFailFast(e.target.checked) }),
487
+ '连接失败时拒绝激活(failOnStartupError)',
488
+ ),
489
+ server.envKeys.length
490
+ ? h(Notice, null, 'env 的值不在页面回显,以免泄露密钥;要改 env 请直接改 patch。')
491
+ : null,
492
+ failure ? h(Notice, { error: true }, failure) : null,
493
+ h(
494
+ 'div',
495
+ { style: S.row },
496
+ h(
497
+ 'button',
498
+ { style: { ...S.btn, borderColor: C.brand, color: C.brand }, onClick: save, disabled: busy },
499
+ busy ? '保存中…' : '保存(热应用)',
500
+ ),
501
+ h('button', { style: S.btn, onClick: props.onCancel, disabled: busy }, '取消'),
502
+ ),
503
+ )
504
+ }
505
+
506
+ function AddForm(props) {
507
+ const [serverName, setServerName] = useState('')
508
+ const [transport, setTransport] = useState('streamable-http')
509
+ const [url, setUrl] = useState('')
510
+ const [command, setCommand] = useState('')
511
+ const [argsText, setArgsText] = useState('')
512
+ const [envText, setEnvText] = useState('')
513
+ const [headersText, setHeadersText] = useState('')
514
+ const [timeout, setTimeoutMs] = useState('')
515
+ const [busy, setBusy] = useState(false)
516
+ const [failure, setFailure] = useState(null)
517
+
518
+ const submit = useCallback(async () => {
519
+ const server = { serverName: serverName.trim(), transport }
520
+ if (transport === 'streamable-http') {
521
+ server.url = url.trim()
522
+ const headers = parseHeaders(headersText)
523
+ if (Object.keys(headers).length > 0) server.headers = headers
524
+ } else {
525
+ server.command = command.trim()
526
+ server.args = argsText.split('\n').map((line) => line.trim()).filter(Boolean)
527
+ const env = parseEnv(envText)
528
+ if (Object.keys(env).length > 0) server.env = env
529
+ }
530
+ if (timeout.trim() !== '') server.toolCallTimeoutMs = Number(timeout)
531
+ setBusy(true)
532
+ setFailure(null)
533
+ const result = await postJson('/mcp-manager/rows', { action: 'add', server })
534
+ setBusy(false)
535
+ if (!result || !result.ok) {
536
+ setFailure((result && result.error) || '添加失败')
537
+ return
538
+ }
539
+ props.onDone()
540
+ }, [argsText, command, envText, headersText, props, serverName, timeout, transport, url])
541
+
542
+ const isHttp = transport === 'streamable-http'
543
+ return h(
544
+ 'div',
545
+ { style: { ...S.card, marginTop: '2px' } },
546
+ h('div', { style: { ...S.cardHead, cursor: 'default', fontWeight: 620 } }, '添加 MCP 服务器'),
547
+ h(
548
+ 'div',
549
+ { style: S.form },
550
+ field(
551
+ 'serverName(工具将命名为 mcp__<serverName>__*)',
552
+ h('input', { style: S.input, value: serverName, onChange: (e) => setServerName(e.target.value), spellCheck: false }),
553
+ ),
554
+ field(
555
+ 'transport',
556
+ h(
557
+ 'select',
558
+ {
559
+ style: S.input,
560
+ value: transport,
561
+ onChange: (e) => setTransport(e.target.value),
562
+ },
563
+ h('option', { value: 'streamable-http' }, 'streamable-http'),
564
+ h('option', { value: 'stdio' }, 'stdio'),
565
+ ),
566
+ ),
567
+ isHttp
568
+ ? h(
569
+ React.Fragment,
570
+ null,
571
+ field('url', h('input', { style: S.input, value: url, onChange: (e) => setUrl(e.target.value), spellCheck: false })),
572
+ field(
573
+ 'headers(每行 Name: Value,可留空)',
574
+ h('textarea', {
575
+ style: { ...S.input, minHeight: '58px', resize: 'vertical' },
576
+ value: headersText,
577
+ onChange: (e) => setHeadersText(e.target.value),
578
+ spellCheck: false,
579
+ placeholder: 'Authorization: Bearer xxx',
580
+ }),
581
+ ),
582
+ )
583
+ : h(
584
+ React.Fragment,
585
+ null,
586
+ field(
587
+ 'command',
588
+ h('input', { style: S.input, value: command, onChange: (e) => setCommand(e.target.value), spellCheck: false }),
589
+ ),
590
+ field(
591
+ 'args(每行一个)',
592
+ h('textarea', {
593
+ style: { ...S.input, minHeight: '58px', resize: 'vertical' },
594
+ value: argsText,
595
+ onChange: (e) => setArgsText(e.target.value),
596
+ spellCheck: false,
597
+ }),
598
+ ),
599
+ field(
600
+ 'env(每行 KEY=VALUE,可留空)',
601
+ h('textarea', {
602
+ style: { ...S.input, minHeight: '58px', resize: 'vertical' },
603
+ value: envText,
604
+ onChange: (e) => setEnvText(e.target.value),
605
+ spellCheck: false,
606
+ }),
607
+ ),
608
+ ),
609
+ field(
610
+ 'toolCallTimeoutMs(可留空)',
611
+ h('input', {
612
+ style: S.input,
613
+ value: timeout,
614
+ onChange: (e) => setTimeoutMs(e.target.value),
615
+ inputMode: 'numeric',
616
+ spellCheck: false,
617
+ }),
618
+ ),
619
+ failure ? h(Notice, { error: true }, failure) : null,
620
+ h(
621
+ 'div',
622
+ { style: S.row },
623
+ h(
624
+ 'button',
625
+ { style: { ...S.btn, borderColor: C.brand, color: C.brand }, onClick: submit, disabled: busy },
626
+ busy ? '写入中…' : '写入 patch',
627
+ ),
628
+ h('button', { style: S.btn, onClick: props.onCancel, disabled: busy }, '取消'),
629
+ ),
630
+ h(Notice, null, '新行会追加到 profile patch 末尾,保存后通常几秒内自动挂载。'),
631
+ ),
632
+ )
633
+ }
634
+
635
+ const PAGE_SIZES = [5, 10, 20, 50, 100]
636
+ const AUTO_REFRESH_MS = 4000
637
+
638
+ /** `1 … 4 5 6 … 20` — a window around the current page plus both ends. */
639
+ function pageItems(current, total) {
640
+ const items = [1]
641
+ const start = Math.max(2, current - 1)
642
+ const end = Math.min(total - 1, current + 1)
643
+ if (start > 2) items.push('…')
644
+ for (let page = start; page <= end; page += 1) items.push(page)
645
+ if (end < total - 1) items.push('…')
646
+ if (total > 1) items.push(total)
647
+ return items
648
+ }
649
+
650
+ function pageButton(label, enabled, onClick) {
651
+ return h(
652
+ 'button',
653
+ {
654
+ style: { ...S.btn, padding: '2px 8px', fontSize: '12px', opacity: enabled ? 1 : 0.45 },
655
+ disabled: !enabled,
656
+ onClick,
657
+ },
658
+ label,
659
+ )
660
+ }
661
+
662
+ function ServerCard(props) {
663
+ const server = props.server
664
+ const open = props.open
665
+ const status = statusOf(server)
666
+ const removable = server.inPatch
667
+ const [page, setPage] = useState(1)
668
+ const [pageSize, setPageSize] = useState(PAGE_SIZES[0])
669
+ const [toolQuery, setToolQuery] = useState('')
670
+
671
+ const tools = useMemo(() => {
672
+ const needle = toolQuery.trim().toLowerCase()
673
+ if (!needle) return server.tools
674
+ return server.tools.filter(
675
+ (tool) =>
676
+ tool.wire.toLowerCase().includes(needle) || tool.description.toLowerCase().includes(needle),
677
+ )
678
+ }, [server.tools, toolQuery])
679
+
680
+ const totalPages = Math.max(1, Math.ceil(tools.length / pageSize))
681
+ const current = Math.min(page, totalPages)
682
+ const from = (current - 1) * pageSize
683
+ const visible = tools.slice(from, from + pageSize)
684
+
685
+ return h(
686
+ 'div',
687
+ { style: S.card },
688
+ h(
689
+ 'button',
690
+ { style: S.cardHead, onClick: props.onToggle, 'aria-expanded': open },
691
+ h('span', { style: { ...S.dot, background: status.color }, title: status.label }),
692
+ h('span', { style: S.name }, server.serverName || server.id),
693
+ h(Chip, null, server.transport || '—'),
694
+ h('span', { style: S.endpoint, title: endpointOf(server) }, endpointOf(server)),
695
+ h('span', { style: S.count }, server.tools.length + ' 工具'),
696
+ h('span', { style: { color: C.dim, fontSize: '11px' } }, open ? '收起' : '展开'),
697
+ ),
698
+ open
699
+ ? h(
700
+ 'div',
701
+ { style: S.body },
702
+ h(
703
+ 'div',
704
+ { style: S.meta },
705
+ h('span', null, 'id: ' + server.id),
706
+ server.mounted === false ? h('span', null, '(已写入,等待挂载…)') : null,
707
+ server.cwd ? h('span', null, 'cwd: ' + server.cwd) : null,
708
+ server.toolCallTimeoutMs === null
709
+ ? null
710
+ : h('span', null, '超时: ' + server.toolCallTimeoutMs + 'ms'),
711
+ server.envKeys.length ? h('span', null, 'env: ' + server.envKeys.join(', ')) : null,
712
+ server.headerKeys.length ? h('span', null, 'headers: ' + server.headerKeys.join(', ')) : null,
713
+ server.failOnStartupError ? h('span', null, 'failOnStartupError') : null,
714
+ server.inPatch ? null : h('span', null, '(不在本 profile 的 patch 里)'),
715
+ ),
716
+ server.tools.length === 0
717
+ ? h(
718
+ 'div',
719
+ { style: S.empty },
720
+ h(
721
+ 'div',
722
+ null,
723
+ '这个服务器没有向工具注册表贡献任何 mcp__ 工具:可能没连上、握手失败,或它本身不提供 tools。',
724
+ ),
725
+ props.probe && props.probe.state === 'running'
726
+ ? h('div', { style: { marginTop: '6px' } }, '正在探测…')
727
+ : props.probe && props.probe.state === 'error'
728
+ ? h('div', { style: { marginTop: '6px', color: C.err } }, props.probe.detail)
729
+ : props.probe && props.probe.state === 'done'
730
+ ? h(
731
+ 'div',
732
+ {
733
+ style: {
734
+ marginTop: '6px',
735
+ color:
736
+ props.probe.verdict === 'handshake-ok' || props.probe.verdict === 'files-ok'
737
+ ? C.ok
738
+ : props.probe.verdict === 'handshake-failed'
739
+ ? C.warn
740
+ : C.err,
741
+ },
742
+ },
743
+ verdictLabel(props.probe.verdict) + ' · ' + props.probe.detail,
744
+ )
745
+ : null,
746
+ )
747
+ : [
748
+ server.tools.length > 12
749
+ ? h('input', {
750
+ key: 'tool-filter',
751
+ style: { ...S.input, margin: '2px 8px 6px' },
752
+ placeholder: '在本服务器的 ' + server.tools.length + ' 个工具里筛选…',
753
+ value: toolQuery,
754
+ onChange: (event) => {
755
+ setToolQuery(event.target.value)
756
+ setPage(1)
757
+ },
758
+ spellCheck: false,
759
+ })
760
+ : null,
761
+ tools.length === 0
762
+ ? h('div', { key: 'no-match', style: S.empty }, '没有匹配的工具。')
763
+ : visible.map((tool) =>
764
+ h(
765
+ 'div',
766
+ { key: tool.name },
767
+ h(
768
+ 'button',
769
+ { style: S.tool, onClick: () => props.onToggleTool(tool.name) },
770
+ h('span', { style: S.toolName }, tool.wire),
771
+ h(
772
+ 'span',
773
+ { style: S.toolDesc, title: tool.description },
774
+ tool.description || '(无描述)',
775
+ ),
776
+ h(
777
+ 'span',
778
+ { style: { color: C.dim, fontSize: '11px' } },
779
+ props.openTool === tool.name ? '收回' : '参数',
780
+ ),
781
+ ),
782
+ props.openTool === tool.name
783
+ ? h(SchemaBlock, { entry: props.schemas[tool.name] })
784
+ : null,
785
+ ),
786
+ ),
787
+ tools.length > 0
788
+ ? h(
789
+ 'div',
790
+ {
791
+ key: 'pager',
792
+ style: {
793
+ ...S.row,
794
+ gap: '4px',
795
+ padding: '8px 8px 2px',
796
+ borderTop: `1px solid ${C.border}`,
797
+ marginTop: '6px',
798
+ },
799
+ },
800
+ h(
801
+ 'span',
802
+ { style: { color: C.dim, fontSize: '11.5px' } },
803
+ '共 ' + tools.length + ' 个',
804
+ ),
805
+ h('span', { style: S.spacer }),
806
+ totalPages > 1
807
+ ? h(
808
+ React.Fragment,
809
+ null,
810
+ pageButton('首页', current > 1, () => setPage(1), S),
811
+ pageButton('上一页', current > 1, () => setPage(current - 1), S),
812
+ pageItems(current, totalPages).map((item, index) =>
813
+ item === '…'
814
+ ? h(
815
+ 'span',
816
+ {
817
+ key: 'gap' + index,
818
+ style: { color: C.dim, padding: '0 3px', fontSize: '12px' },
819
+ },
820
+ '…',
821
+ )
822
+ : h(
823
+ 'button',
824
+ {
825
+ key: 'p' + item,
826
+ style: {
827
+ ...S.btn,
828
+ padding: '2px 8px',
829
+ minWidth: '26px',
830
+ fontSize: '12px',
831
+ ...(item === current
832
+ ? {
833
+ borderColor: C.brand,
834
+ color: C.brand,
835
+ fontWeight: 700,
836
+ textDecoration: 'underline',
837
+ }
838
+ : {}),
839
+ },
840
+ onClick: () => setPage(item),
841
+ },
842
+ String(item),
843
+ ),
844
+ ),
845
+ pageButton('下一页', current < totalPages, () => setPage(current + 1), S),
846
+ pageButton('尾页', current < totalPages, () => setPage(totalPages), S),
847
+ )
848
+ : null,
849
+ h(
850
+ 'span',
851
+ { style: { color: C.dim, fontSize: '11.5px', marginLeft: '6px' } },
852
+ '每页',
853
+ ),
854
+ h(
855
+ 'select',
856
+ {
857
+ style: { ...S.btn, padding: '1px 4px', fontSize: '11.5px' },
858
+ value: String(pageSize),
859
+ onChange: (event) => {
860
+ setPageSize(Number(event.target.value))
861
+ setPage(1)
862
+ },
863
+ },
864
+ PAGE_SIZES.map((size) => h('option', { key: size, value: String(size) }, String(size))),
865
+ ),
866
+ h('span', { style: { color: C.dim, fontSize: '11.5px' } }, '个'),
867
+ )
868
+ : null,
869
+ ],
870
+ h(
871
+ 'div',
872
+ { style: { ...S.row, padding: '8px 6px 2px' } },
873
+ h(
874
+ 'button',
875
+ {
876
+ style: S.btn,
877
+ onClick: props.onEdit,
878
+ disabled: server.mounted === false,
879
+ title:
880
+ server.mounted === false
881
+ ? '这一行还没挂载,configEditor 改不到它;等它挂上(通常几秒)再编辑'
882
+ : '通过 configEditor 修改,即时生效',
883
+ },
884
+ props.editing ? '正在编辑' : '编辑配置',
885
+ ),
886
+ h('button', { style: S.btn, onClick: () => copyText(endpointOf(server)) }, '复制端点'),
887
+ h(
888
+ 'button',
889
+ {
890
+ style: S.btn,
891
+ onClick: props.onProbe,
892
+ disabled: props.probe && props.probe.state === 'running',
893
+ },
894
+ props.probe && props.probe.state === 'running' ? '探测中…' : '探测连通性',
895
+ ),
896
+ h('span', { style: S.spacer }),
897
+ h(
898
+ 'button',
899
+ {
900
+ style: S.btn,
901
+ onClick: props.onDisable,
902
+ disabled: !removable || props.busy,
903
+ title: removable ? '从 patch 里摘掉并暂存,可随时恢复' : '这一行不在本 profile 的 patch 文件里',
904
+ },
905
+ '停用',
906
+ ),
907
+ props.confirm
908
+ ? h(
909
+ React.Fragment,
910
+ null,
911
+ h(
912
+ 'button',
913
+ { style: { ...S.btn, ...S.danger }, onClick: props.onDelete, disabled: props.busy },
914
+ props.busy ? '删除中…' : '确认删除',
915
+ ),
916
+ h('button', { style: S.btn, onClick: props.onCancelDelete, disabled: props.busy }, '取消'),
917
+ )
918
+ : h(
919
+ 'button',
920
+ {
921
+ style: S.btn,
922
+ onClick: props.onAskDelete,
923
+ disabled: !removable || props.busy,
924
+ title: removable ? '从 patch 里永久删除这一行' : '这一行不在本 profile 的 patch 文件里',
925
+ },
926
+ '删除',
927
+ ),
928
+ ),
929
+ )
930
+ : null,
931
+ props.editing ? h(EditForm, { server, onCancel: props.onCancelEdit, onSaved: props.onSaved }) : null,
932
+ )
933
+ }
934
+
935
+ function ConsoleSection() {
936
+ const [data, setData] = useState(null)
937
+ const [failure, setFailure] = useState(null)
938
+ const [loading, setLoading] = useState(true)
939
+ const [query, setQuery] = useState('')
940
+ const [openIds, setOpenIds] = useState({})
941
+ const [openTool, setOpenTool] = useState(null)
942
+ const [schemas, setSchemas] = useState({})
943
+ const [editingId, setEditingId] = useState(null)
944
+ const [adding, setAdding] = useState(false)
945
+ const [confirmId, setConfirmId] = useState(null)
946
+ const [busyId, setBusyId] = useState(null)
947
+ const [probes, setProbes] = useState({})
948
+ const [auto, setAuto] = useState(true)
949
+ const [syncedAt, setSyncedAt] = useState(null)
950
+ const inflight = React.useRef(false)
951
+
952
+ const refresh = useCallback(async (silent) => {
953
+ if (inflight.current) return
954
+ inflight.current = true
955
+ if (!silent) setLoading(true)
956
+ const result = await getJson('/mcp-manager/overview')
957
+ inflight.current = false
958
+ if (!silent) setLoading(false)
959
+ if (!result || !result.ok) {
960
+ // A background tick must not wipe a working panel over one bad read.
961
+ if (!silent) setFailure((result && result.error) || '读取失败')
962
+ return
963
+ }
964
+ setFailure(null)
965
+ setData(result)
966
+ setSyncedAt(new Date())
967
+ }, [])
968
+
969
+ useEffect(() => {
970
+ refresh(false)
971
+ }, [refresh])
972
+
973
+ // The Loader applies patch edits through HMR within a few seconds, so the
974
+ // panel re-reads on its own instead of making the user hunt for 刷新.
975
+ useEffect(() => {
976
+ if (!auto) return undefined
977
+ const timer = setInterval(() => {
978
+ if (document.hidden) return
979
+ refresh(true)
980
+ }, AUTO_REFRESH_MS)
981
+ return () => clearInterval(timer)
982
+ }, [auto, refresh])
983
+
984
+ const runProbe = useCallback(async (server) => {
985
+ setProbes((prev) => ({ ...prev, [server.id]: { state: 'running' } }))
986
+ const result = await postJson('/mcp-manager/probe', { id: server.id })
987
+ if (!result || !result.ok) {
988
+ setProbes((prev) => ({
989
+ ...prev,
990
+ [server.id]: { state: 'error', detail: (result && result.error) || '探测失败' },
991
+ }))
992
+ return
993
+ }
994
+ setProbes((prev) => ({
995
+ ...prev,
996
+ [server.id]: { state: 'done', verdict: result.verdict, detail: result.detail },
997
+ }))
998
+ }, [])
999
+
1000
+ const loadSchema = useCallback(async (name) => {
1001
+ setSchemas((prev) =>
1002
+ prev[name] && prev[name].state === 'ok' ? prev : { ...prev, [name]: { state: 'loading' } },
1003
+ )
1004
+ const result = await getJson('/mcp-manager/schema?tool=' + encodeURIComponent(name))
1005
+ if (!result || !result.ok) {
1006
+ setSchemas((prev) => ({
1007
+ ...prev,
1008
+ [name]: { state: 'error', error: (result && result.error) || '读取 schema 失败' },
1009
+ }))
1010
+ return
1011
+ }
1012
+ setSchemas((prev) => ({
1013
+ ...prev,
1014
+ [name]: { state: 'ok', parameters: result.parameters, description: result.description },
1015
+ }))
1016
+ }, [])
1017
+
1018
+ const toggleTool = useCallback(
1019
+ (name) => {
1020
+ setOpenTool((current) => {
1021
+ if (current === name) return null
1022
+ loadSchema(name)
1023
+ return name
1024
+ })
1025
+ },
1026
+ [loadSchema],
1027
+ )
1028
+
1029
+ const toggleServer = useCallback(
1030
+ (server) => {
1031
+ const willOpen = openIds[server.id] !== true
1032
+ setOpenIds((prev) => ({ ...prev, [server.id]: willOpen }))
1033
+ // A server with no tools is exactly the case where the user wants a
1034
+ // verdict instead of a guess, so ask once on expand.
1035
+ if (willOpen && server.tools.length === 0 && !probes[server.id]) runProbe(server)
1036
+ },
1037
+ [openIds, probes, runProbe],
1038
+ )
1039
+
1040
+ const exportJson = useCallback(async () => {
1041
+ const result = await getJson('/mcp-manager/export')
1042
+ if (!result || !result.ok) {
1043
+ setFailure((result && result.error) || '导出失败')
1044
+ return
1045
+ }
1046
+ copyText(JSON.stringify(result, null, 2))
1047
+ }, [])
1048
+
1049
+ const act = useCallback(
1050
+ async (action, payload) => {
1051
+ const key = payload && payload.id ? payload.id : 'add'
1052
+ setBusyId(key)
1053
+ setFailure(null)
1054
+ const result = await postJson('/mcp-manager/rows', { action, ...payload })
1055
+ setBusyId(null)
1056
+ if (!result || !result.ok) {
1057
+ setFailure((result && result.error) || '操作失败')
1058
+ return false
1059
+ }
1060
+ setEditingId(null)
1061
+ setConfirmId(null)
1062
+ setSchemas({})
1063
+ setOpenTool(null)
1064
+ await refresh()
1065
+ return true
1066
+ },
1067
+ [refresh],
1068
+ )
1069
+
1070
+ const servers = (data && data.servers) || []
1071
+ const parked = (data && data.parked) || []
1072
+
1073
+ const filtered = useMemo(() => {
1074
+ const needle = query.trim().toLowerCase()
1075
+ if (!needle) return servers
1076
+ return servers
1077
+ .map((server) => {
1078
+ const nameHit =
1079
+ server.serverName.toLowerCase().includes(needle) ||
1080
+ server.id.toLowerCase().includes(needle) ||
1081
+ endpointOf(server).toLowerCase().includes(needle)
1082
+ const tools = server.tools.filter(
1083
+ (tool) =>
1084
+ tool.wire.toLowerCase().includes(needle) || tool.description.toLowerCase().includes(needle),
1085
+ )
1086
+ if (nameHit) return server
1087
+ if (tools.length) return { ...server, tools }
1088
+ return null
1089
+ })
1090
+ .filter(Boolean)
1091
+ }, [query, servers])
1092
+
1093
+ const style = h(
1094
+ 'style',
1095
+ null,
1096
+ '.mc-card-btn:hover{background:var(--dsw-alias-bg-layer-2)}',
1097
+ )
1098
+
1099
+ const header = h(
1100
+ 'div',
1101
+ { style: S.head },
1102
+ h(
1103
+ 'div',
1104
+ null,
1105
+ h('h3', { style: S.title }, 'MCP 控制台'),
1106
+ h(
1107
+ 'p',
1108
+ { style: S.sub },
1109
+ loading && !data
1110
+ ? '读取中…'
1111
+ : data
1112
+ ? data.serverTotal + ' 个服务器 · ' + data.toolTotal + ' 个工具' +
1113
+ (parked.length ? ' · ' + parked.length + ' 个已停用' : '') +
1114
+ (syncedAt ? ' · 已同步 ' + syncedAt.toLocaleTimeString('zh-CN', { hour12: false }) : '')
1115
+ : '未获取到数据',
1116
+ ),
1117
+ ),
1118
+ h('span', { style: S.spacer }),
1119
+ h('button', { style: S.btn, onClick: () => setAdding((v) => !v) }, adding ? '收起添加' : '+ 添加'),
1120
+ h(
1121
+ 'label',
1122
+ {
1123
+ style: { ...S.btn, display: 'inline-flex', alignItems: 'center', gap: '5px', cursor: 'pointer' },
1124
+ title: 'HMR 会在几秒内应用 patch 改动,自动刷新让你不用手动点',
1125
+ },
1126
+ h('input', {
1127
+ type: 'checkbox',
1128
+ checked: auto,
1129
+ onChange: (event) => setAuto(event.target.checked),
1130
+ style: { margin: 0 },
1131
+ }),
1132
+ '自动刷新',
1133
+ h(
1134
+ 'span',
1135
+ { style: { color: C.dim, fontSize: '11px' } },
1136
+ auto ? '(' + AUTO_REFRESH_MS / 1000 + 's)' : '',
1137
+ ),
1138
+ ),
1139
+ h('button', { style: S.btn, onClick: () => refresh(false), disabled: loading }, loading ? '刷新中…' : '刷新'),
1140
+ h('button', { style: S.btn, onClick: exportJson }, '复制 JSON'),
1141
+ )
1142
+
1143
+ const patchLine =
1144
+ data && data.patchPath
1145
+ ? h(
1146
+ 'div',
1147
+ { style: { ...S.row, gap: '6px' } },
1148
+ h('span', { style: { color: C.dim, fontSize: '11.5px' } }, '配置来源'),
1149
+ h(
1150
+ 'code',
1151
+ {
1152
+ style: {
1153
+ fontFamily: MONO,
1154
+ fontSize: '11.5px',
1155
+ color: C.dim,
1156
+ overflow: 'hidden',
1157
+ textOverflow: 'ellipsis',
1158
+ whiteSpace: 'nowrap',
1159
+ },
1160
+ title: data.patchPath,
1161
+ },
1162
+ data.patchPath,
1163
+ ),
1164
+ h(
1165
+ 'button',
1166
+ { style: { ...S.btn, padding: '1px 8px', fontSize: '11px' }, onClick: () => copyText(data.patchPath) },
1167
+ '复制',
1168
+ ),
1169
+ )
1170
+ : null
1171
+
1172
+ const pending =
1173
+ data && data.pendingRestart
1174
+ ? h(
1175
+ Notice,
1176
+ null,
1177
+ '已写入 profile patch:HMR 通常几秒内自动生效。若长时间停在“尚未挂载”,才需要重启 DSH。',
1178
+ )
1179
+ : null
1180
+
1181
+ return h(
1182
+ 'div',
1183
+ { style: S.root },
1184
+ style,
1185
+ header,
1186
+ patchLine,
1187
+ pending,
1188
+ adding ? h(AddForm, { onDone: () => { setAdding(false); refresh() }, onCancel: () => setAdding(false) }) : null,
1189
+ h('input', {
1190
+ style: S.input,
1191
+ placeholder: '搜索服务器名、端点或工具名…',
1192
+ value: query,
1193
+ onChange: (event) => setQuery(event.target.value),
1194
+ spellCheck: false,
1195
+ }),
1196
+ failure ? h(Notice, { error: true }, failure) : null,
1197
+ data && data.error ? h(Notice, null, data.error) : null,
1198
+ loading && !data
1199
+ ? h('div', { style: S.empty }, '正在读取 MCP 配置与工具注册表…')
1200
+ : filtered.length === 0
1201
+ ? h('div', { style: S.empty }, query ? '没有匹配的服务器或工具。' : '这个 profile 里没有 MCP 服务器。')
1202
+ : filtered.map((server) =>
1203
+ h(ServerCard, {
1204
+ key: server.id,
1205
+ server,
1206
+ open: openIds[server.id] === true,
1207
+ openTool,
1208
+ schemas,
1209
+ editing: editingId === server.id,
1210
+ confirm: confirmId === server.id,
1211
+ busy: busyId === server.id,
1212
+ searched: query.trim() !== '',
1213
+ probe: probes[server.id],
1214
+ onProbe: () => runProbe(server),
1215
+ onToggle: () => toggleServer(server),
1216
+ onToggleTool: toggleTool,
1217
+ onEdit: () => setEditingId(server.id),
1218
+ onCancelEdit: () => setEditingId(null),
1219
+ onAskDelete: () => setConfirmId(server.id),
1220
+ onCancelDelete: () => setConfirmId(null),
1221
+ onDelete: () => act('remove', { id: server.patchId }),
1222
+ onDisable: () => act('disable', { id: server.patchId }),
1223
+ onSaved: () => {
1224
+ setEditingId(null)
1225
+ setSchemas({})
1226
+ setOpenTool(null)
1227
+ refresh()
1228
+ },
1229
+ }),
1230
+ ),
1231
+ parked.length > 0
1232
+ ? h(
1233
+ 'div',
1234
+ { style: { display: 'grid', gap: '8px' } },
1235
+ h(
1236
+ 'div',
1237
+ { style: { ...S.row, gap: '8px' } },
1238
+ h('span', { style: { fontWeight: 620 } }, '已停用'),
1239
+ h('span', { style: { color: C.dim, fontSize: '11.5px' } }, '暂存在本 profile 的 mcp-manager/disabled.json,可恢复'),
1240
+ ),
1241
+ parked.map((item) =>
1242
+ h(
1243
+ 'div',
1244
+ { key: item.id, style: { ...S.card, padding: '10px 13px', display: 'flex', gap: '10px', alignItems: 'center' } },
1245
+ h('span', { style: { ...S.dot, background: C.idle } }),
1246
+ h('span', { style: S.name }, item.serverName || item.id),
1247
+ h(Chip, null, item.transport || '—'),
1248
+ item.stillMounted ? h(Chip, null, '进程内仍挂着 · 待卸载') : null,
1249
+ h('span', { style: S.endpoint, title: item.url }, item.url || item.id),
1250
+ h('span', { style: S.spacer }),
1251
+ h('span', { style: { color: C.dim, fontSize: '11.5px' } }, item.savedAt ? item.savedAt.slice(0, 19).replace('T', ' ') : ''),
1252
+ h(
1253
+ 'button',
1254
+ { style: S.btn, onClick: () => act('enable', { id: item.id }), disabled: busyId === item.id },
1255
+ busyId === item.id ? '恢复中…' : '启用',
1256
+ ),
1257
+ h(
1258
+ 'button',
1259
+ { style: { ...S.btn, ...S.danger }, onClick: () => act('forget', { id: item.id }), disabled: busyId === item.id },
1260
+ '彻底删除',
1261
+ ),
1262
+ ),
1263
+ ),
1264
+ )
1265
+ : null,
1266
+ )
1267
+ }
1268
+
1269
+ return {
1270
+ name: '@sunt2049/dsh-mcp-manager',
1271
+ inject: ['slots'],
1272
+ apply(ctx) {
1273
+ const slots = ctx.get('slots')
1274
+ if (slots === undefined) return
1275
+ slots.inject('settings.section', () =>
1276
+ slots.register(
1277
+ {
1278
+ name: 'settings.section',
1279
+ id: 'mcp-manager',
1280
+ order: 26,
1281
+ label: () => 'MCP 控制台',
1282
+ },
1283
+ () => ConsoleSection(),
1284
+ ),
1285
+ )
1286
+ },
1287
+ }
1288
+ },
1289
+ })