@sunt2049/dsh-mcp-manager 0.2.0 → 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 +139 -7
- package/index.js +30 -1
- package/package.json +1 -1
- package/patch-doc.js +6 -0
package/client.js
CHANGED
|
@@ -80,6 +80,21 @@ window.__ModuleLoader__.load({
|
|
|
80
80
|
return env
|
|
81
81
|
}
|
|
82
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
|
+
|
|
83
98
|
function verdictLabel(verdict) {
|
|
84
99
|
switch (verdict) {
|
|
85
100
|
case 'port-closed':
|
|
@@ -275,6 +290,8 @@ window.__ModuleLoader__.load({
|
|
|
275
290
|
return h('div', { style: S.field }, h('span', { style: S.label }, label), control)
|
|
276
291
|
}
|
|
277
292
|
|
|
293
|
+
const HEADER_NAME = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/
|
|
294
|
+
|
|
278
295
|
function EditForm(props) {
|
|
279
296
|
const server = props.server
|
|
280
297
|
const isHttp = server.transport === 'streamable-http'
|
|
@@ -285,6 +302,8 @@ window.__ModuleLoader__.load({
|
|
|
285
302
|
server.toolCallTimeoutMs === null ? '' : String(server.toolCallTimeoutMs),
|
|
286
303
|
)
|
|
287
304
|
const [failFast, setFailFast] = useState(server.failOnStartupError === true)
|
|
305
|
+
const [headerRows, setHeaderRows] = useState([{ name: '', value: '' }])
|
|
306
|
+
const [dropped, setDropped] = useState([])
|
|
288
307
|
const [busy, setBusy] = useState(false)
|
|
289
308
|
const [failure, setFailure] = useState(null)
|
|
290
309
|
|
|
@@ -304,6 +323,19 @@ window.__ModuleLoader__.load({
|
|
|
304
323
|
return
|
|
305
324
|
}
|
|
306
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
|
|
307
339
|
} else {
|
|
308
340
|
if (!command.trim()) {
|
|
309
341
|
setFailure('command 不能为空')
|
|
@@ -324,7 +356,7 @@ window.__ModuleLoader__.load({
|
|
|
324
356
|
return
|
|
325
357
|
}
|
|
326
358
|
props.onSaved()
|
|
327
|
-
}, [argsText, command, failFast, isHttp, props, timeout, url])
|
|
359
|
+
}, [argsText, command, dropped, failFast, headerRows, isHttp, props, timeout, url])
|
|
328
360
|
|
|
329
361
|
return h(
|
|
330
362
|
'div',
|
|
@@ -356,6 +388,88 @@ window.__ModuleLoader__.load({
|
|
|
356
388
|
}),
|
|
357
389
|
),
|
|
358
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,
|
|
359
473
|
field(
|
|
360
474
|
'toolCallTimeoutMs(留空表示用默认 60000)',
|
|
361
475
|
h('input', {
|
|
@@ -372,8 +486,8 @@ window.__ModuleLoader__.load({
|
|
|
372
486
|
h('input', { type: 'checkbox', checked: failFast, onChange: (e) => setFailFast(e.target.checked) }),
|
|
373
487
|
'连接失败时拒绝激活(failOnStartupError)',
|
|
374
488
|
),
|
|
375
|
-
server.envKeys.length
|
|
376
|
-
? h(Notice, null, 'env
|
|
489
|
+
server.envKeys.length
|
|
490
|
+
? h(Notice, null, 'env 的值不在页面回显,以免泄露密钥;要改 env 请直接改 patch。')
|
|
377
491
|
: null,
|
|
378
492
|
failure ? h(Notice, { error: true }, failure) : null,
|
|
379
493
|
h(
|
|
@@ -396,14 +510,18 @@ window.__ModuleLoader__.load({
|
|
|
396
510
|
const [command, setCommand] = useState('')
|
|
397
511
|
const [argsText, setArgsText] = useState('')
|
|
398
512
|
const [envText, setEnvText] = useState('')
|
|
513
|
+
const [headersText, setHeadersText] = useState('')
|
|
399
514
|
const [timeout, setTimeoutMs] = useState('')
|
|
400
515
|
const [busy, setBusy] = useState(false)
|
|
401
516
|
const [failure, setFailure] = useState(null)
|
|
402
517
|
|
|
403
518
|
const submit = useCallback(async () => {
|
|
404
519
|
const server = { serverName: serverName.trim(), transport }
|
|
405
|
-
if (transport === 'streamable-http')
|
|
406
|
-
|
|
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 {
|
|
407
525
|
server.command = command.trim()
|
|
408
526
|
server.args = argsText.split('\n').map((line) => line.trim()).filter(Boolean)
|
|
409
527
|
const env = parseEnv(envText)
|
|
@@ -419,7 +537,7 @@ window.__ModuleLoader__.load({
|
|
|
419
537
|
return
|
|
420
538
|
}
|
|
421
539
|
props.onDone()
|
|
422
|
-
}, [argsText, command, envText, props, serverName, timeout, transport, url])
|
|
540
|
+
}, [argsText, command, envText, headersText, props, serverName, timeout, transport, url])
|
|
423
541
|
|
|
424
542
|
const isHttp = transport === 'streamable-http'
|
|
425
543
|
return h(
|
|
@@ -447,7 +565,21 @@ window.__ModuleLoader__.load({
|
|
|
447
565
|
),
|
|
448
566
|
),
|
|
449
567
|
isHttp
|
|
450
|
-
?
|
|
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
|
+
)
|
|
451
583
|
: h(
|
|
452
584
|
React.Fragment,
|
|
453
585
|
null,
|
package/index.js
CHANGED
|
@@ -35,6 +35,9 @@ import {
|
|
|
35
35
|
const MCP_PLUGIN = '@deepseek-ai/dsh-mcp-client'
|
|
36
36
|
const SERVER_NAME = /^[A-Za-z0-9_-]{1,32}$/
|
|
37
37
|
|
|
38
|
+
/** RFC 7230 token, the only thing a header name may contain. */
|
|
39
|
+
const HEADER_NAME = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/
|
|
40
|
+
|
|
38
41
|
/** Config keys this console is willing to write back through configEditor. */
|
|
39
42
|
const EDITABLE = new Set([
|
|
40
43
|
'serverName',
|
|
@@ -376,6 +379,18 @@ function readNewServer(input) {
|
|
|
376
379
|
return out
|
|
377
380
|
}
|
|
378
381
|
next.url = url
|
|
382
|
+
const headers = asObject(config.headers)
|
|
383
|
+
const names = Object.keys(headers)
|
|
384
|
+
if (names.length > 0) {
|
|
385
|
+
next.headers = {}
|
|
386
|
+
for (const name of names) {
|
|
387
|
+
if (!HEADER_NAME.test(name)) {
|
|
388
|
+
out.error = `请求头名不合法:${name}`
|
|
389
|
+
return out
|
|
390
|
+
}
|
|
391
|
+
if (typeof headers[name] === 'string') next.headers[name] = headers[name]
|
|
392
|
+
}
|
|
393
|
+
}
|
|
379
394
|
} else {
|
|
380
395
|
const command = text(config.command).trim()
|
|
381
396
|
if (!command) {
|
|
@@ -692,7 +707,21 @@ export function apply(ctx) {
|
|
|
692
707
|
}
|
|
693
708
|
await hostCtx.configEditor.edit(entry, (current) => {
|
|
694
709
|
const next = { ...asObject(current) }
|
|
695
|
-
for (const key of Object.keys(clean))
|
|
710
|
+
for (const key of Object.keys(clean)) {
|
|
711
|
+
if (key === 'headers') continue
|
|
712
|
+
next[key] = clean[key]
|
|
713
|
+
}
|
|
714
|
+
// The panel never reads header values back, so a header patch is a
|
|
715
|
+
// set of writes and deletes merged over what the row already has.
|
|
716
|
+
// An empty string deletes that header.
|
|
717
|
+
if (clean.headers) {
|
|
718
|
+
const merged = { ...asObject(next.headers) }
|
|
719
|
+
for (const [name, value] of Object.entries(clean.headers)) {
|
|
720
|
+
if (value === '') delete merged[name]
|
|
721
|
+
else merged[name] = value
|
|
722
|
+
}
|
|
723
|
+
next.headers = merged
|
|
724
|
+
}
|
|
696
725
|
return next
|
|
697
726
|
})
|
|
698
727
|
sendJson(response, 200, { ok: true, id, applied: Object.keys(clean), live: true })
|
package/package.json
CHANGED
package/patch-doc.js
CHANGED
|
@@ -241,6 +241,12 @@ export function buildRowLines(id, config) {
|
|
|
241
241
|
|
|
242
242
|
if (config.transport === 'streamable-http') {
|
|
243
243
|
field('url', quote(config.url))
|
|
244
|
+
const headers = config.headers && typeof config.headers === 'object' ? config.headers : {}
|
|
245
|
+
const names = Object.keys(headers)
|
|
246
|
+
if (names.length > 0) {
|
|
247
|
+
lines.push(' headers:')
|
|
248
|
+
for (const name of names) lines.push(` ${name}: ${quote(headers[name])}`)
|
|
249
|
+
}
|
|
244
250
|
} else {
|
|
245
251
|
field('command', quote(config.command))
|
|
246
252
|
const args = Array.isArray(config.args) ? config.args : []
|