dsh-mcp-skills 0.2.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 +86 -0
- package/cordis.patch.yml +6 -0
- package/images/mcp.png +0 -0
- package/images/skills.png +0 -0
- package/lib/client.js +1019 -0
- package/lib/index.js +722 -0
- package/package.json +59 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,1019 @@
|
|
|
1
|
+
// DeepSeek Harness plugin "mcp-skills" — browser half.
|
|
2
|
+
//
|
|
3
|
+
// Registers two settings sections ("Skills" and "MCP") through the standard
|
|
4
|
+
// settings.section slot, and talks to the host through plain fetch() against
|
|
5
|
+
// the plugin's own /dsh-mcp-skills routes (no dependency on the core api
|
|
6
|
+
// client, so no core patching is needed to install this plugin).
|
|
7
|
+
window.__ModuleLoader__.load({
|
|
8
|
+
id: 'dsh-mcp-skills',
|
|
9
|
+
factory: (require) => {
|
|
10
|
+
var module = { exports: {} }
|
|
11
|
+
var exports = module.exports
|
|
12
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
13
|
+
let react_jsx_runtime = require('react/jsx-runtime')
|
|
14
|
+
let react = require('react')
|
|
15
|
+
let _deepseek_ai_dsh_client_ui_primitives = require('@deepseek-ai/dsh-client-ui-primitives')
|
|
16
|
+
|
|
17
|
+
// ── styles ────────────────────────────────────────────────────────────────
|
|
18
|
+
var manageStylesId = 'dsh-mcp-skills/manage.module.css'
|
|
19
|
+
var manageCss =
|
|
20
|
+
'.smg_section{box-sizing:border-box;flex-direction:column;gap:12px;width:100%;min-height:0;display:flex}.smg_toolbar{flex:none;justify-content:space-between;align-items:center;gap:8px;display:flex}.smg_actions{flex:none;align-items:center;gap:4px;display:flex}.smg_title{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:500;line-height:22px}.smg_hint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}.smg_list{flex-direction:column;gap:8px;min-height:0;padding:2px;display:flex;overflow:auto}.smg_card{box-sizing:border-box;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;flex-direction:column;gap:6px;padding:10px 12px;display:flex}.smg_cardHead{flex:none;align-items:center;justify-content:space-between;gap:8px;min-width:0;display:flex}.smg_cardName{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500;line-height:20px;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.smg_cardDesc{color:var(--dsw-alias-label-secondary);font-size:13px;line-height:19px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.smg_cardMeta{flex-wrap:wrap;gap:6px;align-items:center;display:flex}.smg_pill{box-sizing:border-box;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover);border-radius:999px;padding:2px 8px;font-size:11px;line-height:16px;white-space:nowrap}.smg_field{flex-direction:column;gap:4px;min-width:0;display:flex}.smg_fieldLabel{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}.smg_input{box-sizing:border-box;width:100%;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 8px;font-family:inherit;font-size:13px;line-height:20px}.smg_textarea{box-sizing:border-box;width:100%;min-height:110px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 8px;font-family:inherit;font-size:13px;line-height:20px;resize:vertical}.smg_form{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;flex-direction:column;gap:10px;padding:12px;display:flex}.smg_formRow{flex:none;align-items:flex-end;gap:8px;display:flex}.smg_check{align-items:center;gap:6px;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px;display:flex}.smg_error{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px}.smg_notice{color:var(--dsw-alias-state-success-primary);font-size:12px;line-height:18px}'
|
|
21
|
+
if (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css=' + JSON.stringify(manageStylesId) + ']') === null) {
|
|
22
|
+
var tag = document.createElement('style')
|
|
23
|
+
tag.dataset.plugin = 'dsh-mcp-skills'
|
|
24
|
+
tag.dataset.pluginCss = manageStylesId
|
|
25
|
+
tag.textContent = manageCss
|
|
26
|
+
document.head.appendChild(tag)
|
|
27
|
+
}
|
|
28
|
+
var manageStyles = {
|
|
29
|
+
section: 'smg_section',
|
|
30
|
+
toolbar: 'smg_toolbar',
|
|
31
|
+
actions: 'smg_actions',
|
|
32
|
+
title: 'smg_title',
|
|
33
|
+
hint: 'smg_hint',
|
|
34
|
+
list: 'smg_list',
|
|
35
|
+
card: 'smg_card',
|
|
36
|
+
cardHead: 'smg_cardHead',
|
|
37
|
+
cardName: 'smg_cardName',
|
|
38
|
+
cardDesc: 'smg_cardDesc',
|
|
39
|
+
cardMeta: 'smg_cardMeta',
|
|
40
|
+
pill: 'smg_pill',
|
|
41
|
+
field: 'smg_field',
|
|
42
|
+
fieldLabel: 'smg_fieldLabel',
|
|
43
|
+
input: 'smg_input',
|
|
44
|
+
textarea: 'smg_textarea',
|
|
45
|
+
form: 'smg_form',
|
|
46
|
+
formRow: 'smg_formRow',
|
|
47
|
+
check: 'smg_check',
|
|
48
|
+
error: 'smg_error',
|
|
49
|
+
notice: 'smg_notice',
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
var { useState: useManageState, useEffect: useManageEffect, useCallback: useManageCallback, useRef: useManageRef } = react
|
|
53
|
+
|
|
54
|
+
/** Human message of a thrown value. */
|
|
55
|
+
function manageMessageOf(error) {
|
|
56
|
+
return error instanceof Error ? error.message : String(error)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** One labeled field row inside a management form. */
|
|
60
|
+
function ManageField({ label, children }) {
|
|
61
|
+
return react_jsx_runtime.jsxs('label', {
|
|
62
|
+
className: manageStyles.field,
|
|
63
|
+
children: [
|
|
64
|
+
react_jsx_runtime.jsx('span', { className: manageStyles.fieldLabel, children: label }),
|
|
65
|
+
children,
|
|
66
|
+
],
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* POST one /dsh-mcp-skills route and shape the reply like the core api
|
|
72
|
+
* client ({ result: { ok, value | error } }) so the section components stay
|
|
73
|
+
* transport-agnostic.
|
|
74
|
+
*/
|
|
75
|
+
async function callHost(path, payload) {
|
|
76
|
+
const response = await fetch('/dsh-mcp-skills/' + path, {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: { 'content-type': 'application/json' },
|
|
79
|
+
body: JSON.stringify({ payload: payload ?? {} }),
|
|
80
|
+
})
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
const body = await response.json().catch(() => ({}))
|
|
83
|
+
throw new Error((body.error && body.error.message) || 'HTTP ' + response.status)
|
|
84
|
+
}
|
|
85
|
+
return { result: await response.json() }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The api-shaped face the sections consume. */
|
|
89
|
+
function buildManageApi() {
|
|
90
|
+
return {
|
|
91
|
+
skills: {
|
|
92
|
+
manage: {
|
|
93
|
+
list: () => callHost('skill.list', {}),
|
|
94
|
+
install: (payload) => callHost('skill.install', payload),
|
|
95
|
+
enable: (payload) => callHost('skill.enable', payload),
|
|
96
|
+
remove: (payload) => callHost('skill.remove', payload),
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
mcp: {
|
|
100
|
+
list: () => callHost('mcp.list', {}),
|
|
101
|
+
upsert: (payload) => callHost('mcp.upsert', payload),
|
|
102
|
+
remove: (payload) => callHost('mcp.remove', payload),
|
|
103
|
+
test: (payload) => callHost('mcp.test', payload),
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Default per-call timeout, matching @deepseek-ai/dsh-mcp-client. */
|
|
109
|
+
var MANAGE_DEFAULT_TIMEOUT_MS = 60000
|
|
110
|
+
|
|
111
|
+
/** Split textarea lines into an array of non-empty trimmed lines. */
|
|
112
|
+
function manageLines(text) {
|
|
113
|
+
return text.split('\n').map((line) => line.trim()).filter((line) => line !== '')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Parse textarea lines into a record. Separators are matched by earliest
|
|
118
|
+
* position, so a value may contain the other one; with `bareAsKey` a line
|
|
119
|
+
* without any separator becomes a key with an empty value (env form).
|
|
120
|
+
*/
|
|
121
|
+
function managePairs(text, separators, bareAsKey) {
|
|
122
|
+
var result = {}
|
|
123
|
+
var lines = manageLines(text)
|
|
124
|
+
for (var i = 0; i < lines.length; i++) {
|
|
125
|
+
var line = lines[i]
|
|
126
|
+
var index = -1
|
|
127
|
+
for (var s = 0; s < separators.length; s++) {
|
|
128
|
+
var at = line.indexOf(separators[s])
|
|
129
|
+
if (at !== -1 && (index === -1 || at < index)) index = at
|
|
130
|
+
}
|
|
131
|
+
if (index === -1) {
|
|
132
|
+
if (bareAsKey) result[line] = ''
|
|
133
|
+
continue
|
|
134
|
+
}
|
|
135
|
+
if (index === 0) continue
|
|
136
|
+
var key = line.slice(0, index).trim()
|
|
137
|
+
var value = line.slice(index + 1).trim()
|
|
138
|
+
if (key !== '') result[key] = value
|
|
139
|
+
}
|
|
140
|
+
return result
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Environment variables: `KEY=VALUE` per line. */
|
|
144
|
+
function manageKeyValue(text) {
|
|
145
|
+
return managePairs(text, ['='], true)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** HTTP headers: `Name: Value` per line (`Name=Value` kept for compatibility). */
|
|
149
|
+
function manageHeaders(text) {
|
|
150
|
+
return managePairs(text, [':', '='], false)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Render one stored header back into the textarea's `Name: Value` form. */
|
|
154
|
+
function manageHeaderLine(key, value) {
|
|
155
|
+
if (value === '') {
|
|
156
|
+
// Written by the earlier version that split on `=`: the whole
|
|
157
|
+
// `Name: Value` line was stored as the key. Recover the pair.
|
|
158
|
+
var index = key.indexOf(':')
|
|
159
|
+
if (index > 0) return key.slice(0, index).trim() + ': ' + key.slice(index + 1).trim()
|
|
160
|
+
}
|
|
161
|
+
return key + ': ' + value
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Build the mcp.upsert server object from a form draft. */
|
|
165
|
+
function mcpDraftToServer(draft) {
|
|
166
|
+
return {
|
|
167
|
+
name: draft.name,
|
|
168
|
+
enabled: draft.enabled,
|
|
169
|
+
transport: draft.transport,
|
|
170
|
+
toolCallTimeoutMs: Number(draft.toolCallTimeoutMs) || MANAGE_DEFAULT_TIMEOUT_MS,
|
|
171
|
+
failOnStartupError: draft.failOnStartupError,
|
|
172
|
+
...(draft.transport === 'stdio'
|
|
173
|
+
? { command: draft.command, args: manageLines(draft.argsText), env: manageKeyValue(draft.envText), cwd: draft.cwd }
|
|
174
|
+
: { url: draft.url, headers: manageHeaders(draft.headersText) }),
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Build a form draft from a server object (for edit + JSON→form sync). */
|
|
179
|
+
function mcpServerToDraft(server) {
|
|
180
|
+
return {
|
|
181
|
+
name: server.name ?? '',
|
|
182
|
+
enabled: server.enabled !== false,
|
|
183
|
+
transport: server.transport ?? 'stdio',
|
|
184
|
+
command: server.command ?? '',
|
|
185
|
+
argsText: (server.args ?? []).join('\n'),
|
|
186
|
+
envText: Object.entries(server.env ?? {}).map(([key, value]) => key + '=' + value).join('\n'),
|
|
187
|
+
cwd: server.cwd ?? '',
|
|
188
|
+
url: server.url ?? '',
|
|
189
|
+
headersText: Object.entries(server.headers ?? {}).map(([key, value]) => manageHeaderLine(key, value)).join('\n'),
|
|
190
|
+
toolCallTimeoutMs: String(server.toolCallTimeoutMs ?? MANAGE_DEFAULT_TIMEOUT_MS),
|
|
191
|
+
failOnStartupError: server.failOnStartupError === true,
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Read a browser File as a base64 string (for zip upload). */
|
|
196
|
+
function readFileAsBase64(file) {
|
|
197
|
+
return new Promise((resolvePromise, reject) => {
|
|
198
|
+
const reader = new FileReader()
|
|
199
|
+
reader.onload = () => {
|
|
200
|
+
const result = reader.result
|
|
201
|
+
if (typeof result !== 'string') {
|
|
202
|
+
reject(new Error('failed to read file'))
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
const comma = result.indexOf(',')
|
|
206
|
+
resolvePromise(comma === -1 ? result : result.slice(comma + 1))
|
|
207
|
+
}
|
|
208
|
+
reader.onerror = () => {
|
|
209
|
+
reject(reader.error instanceof Error ? reader.error : new Error('failed to read file'))
|
|
210
|
+
}
|
|
211
|
+
reader.readAsDataURL(file)
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* The Skills settings page: user-owned skill catalog with zip import and
|
|
217
|
+
* delete; import opens the file picker directly (no inline editing).
|
|
218
|
+
*/
|
|
219
|
+
function SkillsSection({ api, t }) {
|
|
220
|
+
var manageApi = api.skills.manage
|
|
221
|
+
var [skills, setSkills] = useManageState([])
|
|
222
|
+
var [status, setStatus] = useManageState('loading')
|
|
223
|
+
var [error, setError] = useManageState(null)
|
|
224
|
+
var [notice, setNotice] = useManageState(null)
|
|
225
|
+
var [busy, setBusy] = useManageState(false)
|
|
226
|
+
var fileInputRef = useManageRef(null)
|
|
227
|
+
|
|
228
|
+
var load = useManageCallback(async () => {
|
|
229
|
+
setStatus('loading')
|
|
230
|
+
setError(null)
|
|
231
|
+
try {
|
|
232
|
+
var response = await manageApi.list({})
|
|
233
|
+
if (!response.result.ok) throw new Error(response.result.error.message)
|
|
234
|
+
setSkills(response.result.value.skills)
|
|
235
|
+
setStatus('ready')
|
|
236
|
+
} catch (cause) {
|
|
237
|
+
setError(manageMessageOf(cause))
|
|
238
|
+
setStatus('error')
|
|
239
|
+
}
|
|
240
|
+
}, [manageApi])
|
|
241
|
+
useManageEffect(() => {
|
|
242
|
+
load()
|
|
243
|
+
}, [load])
|
|
244
|
+
|
|
245
|
+
var handleImportFile = useManageCallback(async (file) => {
|
|
246
|
+
setBusy(true)
|
|
247
|
+
setError(null)
|
|
248
|
+
setNotice(null)
|
|
249
|
+
try {
|
|
250
|
+
var data = await readFileAsBase64(file)
|
|
251
|
+
var response = await manageApi.install({ data, filename: file.name })
|
|
252
|
+
if (!response.result.ok) throw new Error(response.result.error.message)
|
|
253
|
+
var importedNames = response.result.value.names ?? []
|
|
254
|
+
var skippedNames = response.result.value.skipped ?? []
|
|
255
|
+
if (importedNames.length === 0) {
|
|
256
|
+
setNotice(t('skills.importedNone').replace('{count}', String(skippedNames.length)))
|
|
257
|
+
} else if (skippedNames.length > 0) {
|
|
258
|
+
setNotice(
|
|
259
|
+
t('skills.importedSome').replace('{count}', String(importedNames.length)).replace('{skipped}', String(skippedNames.length)),
|
|
260
|
+
)
|
|
261
|
+
} else if (importedNames.length > 1) {
|
|
262
|
+
setNotice(t('skills.importedMany').replace('{count}', String(importedNames.length)))
|
|
263
|
+
} else {
|
|
264
|
+
setNotice(t('skills.imported'))
|
|
265
|
+
}
|
|
266
|
+
await load()
|
|
267
|
+
} catch (cause) {
|
|
268
|
+
setError(manageMessageOf(cause))
|
|
269
|
+
} finally {
|
|
270
|
+
setBusy(false)
|
|
271
|
+
}
|
|
272
|
+
}, [manageApi, load, t])
|
|
273
|
+
|
|
274
|
+
var remove = useManageCallback(async (name) => {
|
|
275
|
+
if (!window.confirm(t('skills.removeConfirm').replace('{name}', name))) return
|
|
276
|
+
setBusy(true)
|
|
277
|
+
setError(null)
|
|
278
|
+
setNotice(null)
|
|
279
|
+
try {
|
|
280
|
+
var response = await manageApi.remove({ name })
|
|
281
|
+
if (!response.result.ok) throw new Error(response.result.error.message)
|
|
282
|
+
setNotice(t('skills.removed'))
|
|
283
|
+
await load()
|
|
284
|
+
} catch (cause) {
|
|
285
|
+
setError(manageMessageOf(cause))
|
|
286
|
+
} finally {
|
|
287
|
+
setBusy(false)
|
|
288
|
+
}
|
|
289
|
+
}, [manageApi, load, t])
|
|
290
|
+
|
|
291
|
+
var toggleEnabled = useManageCallback(async (name, enabled) => {
|
|
292
|
+
setBusy(true)
|
|
293
|
+
setError(null)
|
|
294
|
+
setNotice(null)
|
|
295
|
+
try {
|
|
296
|
+
var response = await manageApi.enable({ name, enabled })
|
|
297
|
+
if (!response.result.ok) throw new Error(response.result.error.message)
|
|
298
|
+
await load()
|
|
299
|
+
} catch (cause) {
|
|
300
|
+
setError(manageMessageOf(cause))
|
|
301
|
+
} finally {
|
|
302
|
+
setBusy(false)
|
|
303
|
+
}
|
|
304
|
+
}, [manageApi, load])
|
|
305
|
+
|
|
306
|
+
return react_jsx_runtime.jsxs('div', {
|
|
307
|
+
className: manageStyles.section,
|
|
308
|
+
children: [
|
|
309
|
+
react_jsx_runtime.jsxs('div', {
|
|
310
|
+
className: manageStyles.toolbar,
|
|
311
|
+
children: [
|
|
312
|
+
react_jsx_runtime.jsx('div', { className: manageStyles.title, children: t('skills.title') }),
|
|
313
|
+
react_jsx_runtime.jsxs('div', {
|
|
314
|
+
className: manageStyles.actions,
|
|
315
|
+
children: [
|
|
316
|
+
react_jsx_runtime.jsx(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
317
|
+
variant: 'ghost',
|
|
318
|
+
size: 'sm',
|
|
319
|
+
icon: react_jsx_runtime.jsx(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutline16, { size: 14 }),
|
|
320
|
+
disabled: busy,
|
|
321
|
+
onClick: () => {
|
|
322
|
+
load()
|
|
323
|
+
},
|
|
324
|
+
children: t('skills.refresh'),
|
|
325
|
+
}),
|
|
326
|
+
react_jsx_runtime.jsx(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
327
|
+
variant: 'primary',
|
|
328
|
+
size: 'sm',
|
|
329
|
+
icon: react_jsx_runtime.jsx(_deepseek_ai_dsh_client_ui_primitives.IconPlusOutline16, { size: 14 }),
|
|
330
|
+
disabled: busy,
|
|
331
|
+
onClick: () => {
|
|
332
|
+
if (fileInputRef.current !== null) fileInputRef.current.click()
|
|
333
|
+
},
|
|
334
|
+
children: t('skills.new'),
|
|
335
|
+
}),
|
|
336
|
+
],
|
|
337
|
+
}),
|
|
338
|
+
],
|
|
339
|
+
}),
|
|
340
|
+
notice === null ? null : react_jsx_runtime.jsx('div', { className: manageStyles.notice, children: notice }),
|
|
341
|
+
error === null ? null : react_jsx_runtime.jsx('div', { className: manageStyles.error, children: error }),
|
|
342
|
+
react_jsx_runtime.jsx('input', {
|
|
343
|
+
ref: fileInputRef,
|
|
344
|
+
type: 'file',
|
|
345
|
+
accept: '.zip,application/zip',
|
|
346
|
+
style: { display: 'none' },
|
|
347
|
+
onChange: (event) => {
|
|
348
|
+
var file = event.target.files && event.target.files[0] ? event.target.files[0] : null
|
|
349
|
+
if (file !== null) handleImportFile(file)
|
|
350
|
+
event.target.value = ''
|
|
351
|
+
},
|
|
352
|
+
}),
|
|
353
|
+
react_jsx_runtime.jsx('div', {
|
|
354
|
+
className: manageStyles.list,
|
|
355
|
+
children:
|
|
356
|
+
status === 'loading'
|
|
357
|
+
? react_jsx_runtime.jsx('div', { className: manageStyles.hint, children: t('skills.loading') })
|
|
358
|
+
: skills.length === 0
|
|
359
|
+
? react_jsx_runtime.jsx('div', { className: manageStyles.hint, children: t('skills.empty') })
|
|
360
|
+
: skills.map((skill) =>
|
|
361
|
+
react_jsx_runtime.jsxs(
|
|
362
|
+
'div',
|
|
363
|
+
{
|
|
364
|
+
className: manageStyles.card,
|
|
365
|
+
children: [
|
|
366
|
+
react_jsx_runtime.jsxs('div', {
|
|
367
|
+
className: manageStyles.cardHead,
|
|
368
|
+
children: [
|
|
369
|
+
react_jsx_runtime.jsx('span', { className: manageStyles.cardName, children: skill.name }),
|
|
370
|
+
react_jsx_runtime.jsxs('div', {
|
|
371
|
+
className: manageStyles.actions,
|
|
372
|
+
children: [
|
|
373
|
+
react_jsx_runtime.jsxs('label', {
|
|
374
|
+
className: manageStyles.check,
|
|
375
|
+
children: [
|
|
376
|
+
react_jsx_runtime.jsx('input', {
|
|
377
|
+
type: 'checkbox',
|
|
378
|
+
checked: skill.modelInvocable,
|
|
379
|
+
disabled: busy,
|
|
380
|
+
onChange: (event) => {
|
|
381
|
+
toggleEnabled(skill.name, event.target.checked)
|
|
382
|
+
},
|
|
383
|
+
}),
|
|
384
|
+
t('skills.enabled'),
|
|
385
|
+
],
|
|
386
|
+
}),
|
|
387
|
+
react_jsx_runtime.jsx(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
388
|
+
variant: 'ghost',
|
|
389
|
+
size: 'sm',
|
|
390
|
+
icon: react_jsx_runtime.jsx(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, { size: 14 }),
|
|
391
|
+
disabled: busy,
|
|
392
|
+
onClick: () => {
|
|
393
|
+
remove(skill.name)
|
|
394
|
+
},
|
|
395
|
+
children: t('skills.remove'),
|
|
396
|
+
}),
|
|
397
|
+
],
|
|
398
|
+
}),
|
|
399
|
+
],
|
|
400
|
+
}),
|
|
401
|
+
react_jsx_runtime.jsx('div', { className: manageStyles.cardDesc, title: skill.description, children: skill.description }),
|
|
402
|
+
react_jsx_runtime.jsxs('div', {
|
|
403
|
+
className: manageStyles.cardMeta,
|
|
404
|
+
children: [
|
|
405
|
+
skill.whenToUse !== undefined ? react_jsx_runtime.jsx('span', { className: manageStyles.pill, children: skill.whenToUse }) : null,
|
|
406
|
+
],
|
|
407
|
+
}),
|
|
408
|
+
],
|
|
409
|
+
},
|
|
410
|
+
skill.name,
|
|
411
|
+
),
|
|
412
|
+
),
|
|
413
|
+
}),
|
|
414
|
+
],
|
|
415
|
+
})
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* The MCP settings page: managed server catalog with add / edit / remove
|
|
420
|
+
* and a per-server connection test.
|
|
421
|
+
*/
|
|
422
|
+
function McpSection({ api, t }) {
|
|
423
|
+
var mcpApi = api.mcp
|
|
424
|
+
var [servers, setServers] = useManageState([])
|
|
425
|
+
var [status, setStatus] = useManageState('loading')
|
|
426
|
+
var [error, setError] = useManageState(null)
|
|
427
|
+
var [notice, setNotice] = useManageState(null)
|
|
428
|
+
var [editing, setEditing] = useManageState(null)
|
|
429
|
+
var [busy, setBusy] = useManageState(false)
|
|
430
|
+
var [testing, setTesting] = useManageState(null)
|
|
431
|
+
var [draft, setDraft] = useManageState(null)
|
|
432
|
+
var [jsonMode, setJsonMode] = useManageState(false)
|
|
433
|
+
var [jsonText, setJsonText] = useManageState('')
|
|
434
|
+
|
|
435
|
+
var load = useManageCallback(async () => {
|
|
436
|
+
setStatus('loading')
|
|
437
|
+
setError(null)
|
|
438
|
+
try {
|
|
439
|
+
var response = await mcpApi.list({})
|
|
440
|
+
if (!response.result.ok) throw new Error(response.result.error.message)
|
|
441
|
+
setServers(response.result.value.servers)
|
|
442
|
+
setStatus('ready')
|
|
443
|
+
} catch (cause) {
|
|
444
|
+
setError(manageMessageOf(cause))
|
|
445
|
+
setStatus('error')
|
|
446
|
+
}
|
|
447
|
+
}, [mcpApi])
|
|
448
|
+
useManageEffect(() => {
|
|
449
|
+
load()
|
|
450
|
+
}, [load])
|
|
451
|
+
|
|
452
|
+
var openForm = useManageCallback((server) => {
|
|
453
|
+
setDraft(mcpServerToDraft(server ?? { name: '', enabled: true, transport: 'stdio' }))
|
|
454
|
+
setEditing(server ?? null)
|
|
455
|
+
setJsonMode(false)
|
|
456
|
+
setJsonText('')
|
|
457
|
+
setError(null)
|
|
458
|
+
setNotice(null)
|
|
459
|
+
}, [])
|
|
460
|
+
|
|
461
|
+
var setDraftField = useManageCallback((field, value) => {
|
|
462
|
+
setDraft((previous) => (previous === null ? previous : { ...previous, [field]: value }))
|
|
463
|
+
}, [])
|
|
464
|
+
|
|
465
|
+
var toggleJsonMode = useManageCallback(() => {
|
|
466
|
+
if (jsonMode) {
|
|
467
|
+
try {
|
|
468
|
+
var server = JSON.parse(jsonText)
|
|
469
|
+
if (server !== null && typeof server === 'object' && !Array.isArray(server)) {
|
|
470
|
+
setDraft(mcpServerToDraft(server))
|
|
471
|
+
}
|
|
472
|
+
} catch {}
|
|
473
|
+
setJsonMode(false)
|
|
474
|
+
} else {
|
|
475
|
+
if (draft !== null) setJsonText(JSON.stringify(mcpDraftToServer(draft), null, 2))
|
|
476
|
+
setJsonMode(true)
|
|
477
|
+
}
|
|
478
|
+
}, [jsonMode, draft, jsonText])
|
|
479
|
+
|
|
480
|
+
var submit = useManageCallback(async () => {
|
|
481
|
+
if (draft === null) return
|
|
482
|
+
setBusy(true)
|
|
483
|
+
setError(null)
|
|
484
|
+
setNotice(null)
|
|
485
|
+
try {
|
|
486
|
+
var server
|
|
487
|
+
if (jsonMode) {
|
|
488
|
+
try {
|
|
489
|
+
server = JSON.parse(jsonText)
|
|
490
|
+
} catch (parseError) {
|
|
491
|
+
throw new Error(t('mcp.form.jsonError').replace('{message}', parseError instanceof Error ? parseError.message : String(parseError)))
|
|
492
|
+
}
|
|
493
|
+
} else {
|
|
494
|
+
server = mcpDraftToServer(draft)
|
|
495
|
+
}
|
|
496
|
+
var response = await mcpApi.upsert({ server })
|
|
497
|
+
if (!response.result.ok) throw new Error(response.result.error.message)
|
|
498
|
+
setEditing(null)
|
|
499
|
+
setDraft(null)
|
|
500
|
+
setJsonMode(false)
|
|
501
|
+
setJsonText('')
|
|
502
|
+
setNotice(t('mcp.saved'))
|
|
503
|
+
await load()
|
|
504
|
+
} catch (cause) {
|
|
505
|
+
setError(manageMessageOf(cause))
|
|
506
|
+
} finally {
|
|
507
|
+
setBusy(false)
|
|
508
|
+
}
|
|
509
|
+
}, [draft, jsonMode, jsonText, mcpApi, load, t])
|
|
510
|
+
|
|
511
|
+
var remove = useManageCallback(async (name) => {
|
|
512
|
+
if (!window.confirm(t('mcp.removeConfirm').replace('{name}', name))) return
|
|
513
|
+
setBusy(true)
|
|
514
|
+
setError(null)
|
|
515
|
+
setNotice(null)
|
|
516
|
+
try {
|
|
517
|
+
var response = await mcpApi.remove({ name })
|
|
518
|
+
if (!response.result.ok) throw new Error(response.result.error.message)
|
|
519
|
+
setNotice(t('mcp.removed'))
|
|
520
|
+
await load()
|
|
521
|
+
} catch (cause) {
|
|
522
|
+
setError(manageMessageOf(cause))
|
|
523
|
+
} finally {
|
|
524
|
+
setBusy(false)
|
|
525
|
+
}
|
|
526
|
+
}, [mcpApi, load, t])
|
|
527
|
+
|
|
528
|
+
var test = useManageCallback(async (name) => {
|
|
529
|
+
setTesting(name)
|
|
530
|
+
setError(null)
|
|
531
|
+
setNotice(null)
|
|
532
|
+
try {
|
|
533
|
+
var response = await mcpApi.test({ name })
|
|
534
|
+
if (!response.result.ok) throw new Error(response.result.error.message)
|
|
535
|
+
setNotice(t('mcp.test.ok').replace('{count}', String(response.result.value.tools.length)))
|
|
536
|
+
} catch (cause) {
|
|
537
|
+
setError(t('mcp.test.fail').replace('{message}', manageMessageOf(cause)))
|
|
538
|
+
} finally {
|
|
539
|
+
setTesting(null)
|
|
540
|
+
}
|
|
541
|
+
}, [mcpApi, t])
|
|
542
|
+
|
|
543
|
+
var closeForm = useManageCallback(() => {
|
|
544
|
+
setEditing(null)
|
|
545
|
+
setDraft(null)
|
|
546
|
+
}, [])
|
|
547
|
+
|
|
548
|
+
return react_jsx_runtime.jsxs('div', {
|
|
549
|
+
className: manageStyles.section,
|
|
550
|
+
children: [
|
|
551
|
+
react_jsx_runtime.jsxs('div', {
|
|
552
|
+
className: manageStyles.toolbar,
|
|
553
|
+
children: [
|
|
554
|
+
react_jsx_runtime.jsx('div', { className: manageStyles.title, children: t('mcp.title') }),
|
|
555
|
+
react_jsx_runtime.jsxs('div', {
|
|
556
|
+
className: manageStyles.actions,
|
|
557
|
+
children: [
|
|
558
|
+
react_jsx_runtime.jsx(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
559
|
+
variant: 'ghost',
|
|
560
|
+
size: 'sm',
|
|
561
|
+
icon: react_jsx_runtime.jsx(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutline16, { size: 14 }),
|
|
562
|
+
disabled: busy,
|
|
563
|
+
onClick: () => {
|
|
564
|
+
load()
|
|
565
|
+
},
|
|
566
|
+
children: t('mcp.refresh'),
|
|
567
|
+
}),
|
|
568
|
+
react_jsx_runtime.jsx(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
569
|
+
variant: 'primary',
|
|
570
|
+
size: 'sm',
|
|
571
|
+
icon: react_jsx_runtime.jsx(_deepseek_ai_dsh_client_ui_primitives.IconPlusOutline16, { size: 14 }),
|
|
572
|
+
onClick: () => {
|
|
573
|
+
openForm(null)
|
|
574
|
+
},
|
|
575
|
+
children: t('mcp.new'),
|
|
576
|
+
}),
|
|
577
|
+
],
|
|
578
|
+
}),
|
|
579
|
+
],
|
|
580
|
+
}),
|
|
581
|
+
react_jsx_runtime.jsx('div', { className: manageStyles.hint, children: t('mcp.restartHint') }),
|
|
582
|
+
notice === null ? null : react_jsx_runtime.jsx('div', { className: manageStyles.notice, children: notice }),
|
|
583
|
+
error === null ? null : react_jsx_runtime.jsx('div', { className: manageStyles.error, children: error }),
|
|
584
|
+
draft !== null
|
|
585
|
+
? react_jsx_runtime.jsxs('div', {
|
|
586
|
+
className: manageStyles.form,
|
|
587
|
+
children: [
|
|
588
|
+
react_jsx_runtime.jsxs('div', {
|
|
589
|
+
className: manageStyles.toolbar,
|
|
590
|
+
children: [
|
|
591
|
+
react_jsx_runtime.jsx('div', { className: manageStyles.title, children: editing === null ? t('mcp.new') : t('mcp.edit') }),
|
|
592
|
+
react_jsx_runtime.jsxs('div', {
|
|
593
|
+
className: manageStyles.actions,
|
|
594
|
+
children: [
|
|
595
|
+
react_jsx_runtime.jsx(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
596
|
+
variant: 'ghost',
|
|
597
|
+
size: 'sm',
|
|
598
|
+
disabled: busy,
|
|
599
|
+
onClick: closeForm,
|
|
600
|
+
children: t('mcp.cancel'),
|
|
601
|
+
}),
|
|
602
|
+
react_jsx_runtime.jsx(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
603
|
+
variant: 'primary',
|
|
604
|
+
size: 'sm',
|
|
605
|
+
disabled: busy || (jsonMode ? jsonText.trim() === '' : draft.name.trim() === '' || (draft.transport === 'stdio' ? draft.command.trim() === '' : draft.url.trim() === '')),
|
|
606
|
+
onClick: () => {
|
|
607
|
+
submit()
|
|
608
|
+
},
|
|
609
|
+
children: editing === null ? t('mcp.form.add') : t('mcp.form.save'),
|
|
610
|
+
}),
|
|
611
|
+
],
|
|
612
|
+
}),
|
|
613
|
+
],
|
|
614
|
+
}),
|
|
615
|
+
react_jsx_runtime.jsxs('div', {
|
|
616
|
+
className: manageStyles.formRow,
|
|
617
|
+
children: [
|
|
618
|
+
react_jsx_runtime.jsxs('label', {
|
|
619
|
+
className: manageStyles.check,
|
|
620
|
+
children: [
|
|
621
|
+
react_jsx_runtime.jsx('input', {
|
|
622
|
+
type: 'checkbox',
|
|
623
|
+
checked: jsonMode,
|
|
624
|
+
onChange: toggleJsonMode,
|
|
625
|
+
}),
|
|
626
|
+
t('mcp.form.jsonMode'),
|
|
627
|
+
],
|
|
628
|
+
}),
|
|
629
|
+
jsonMode
|
|
630
|
+
? null
|
|
631
|
+
: react_jsx_runtime.jsxs('label', {
|
|
632
|
+
className: manageStyles.check,
|
|
633
|
+
children: [
|
|
634
|
+
react_jsx_runtime.jsx('input', {
|
|
635
|
+
type: 'checkbox',
|
|
636
|
+
checked: draft.enabled,
|
|
637
|
+
onChange: (event) => {
|
|
638
|
+
setDraftField('enabled', event.target.checked)
|
|
639
|
+
},
|
|
640
|
+
}),
|
|
641
|
+
t('mcp.form.enabled'),
|
|
642
|
+
],
|
|
643
|
+
}),
|
|
644
|
+
],
|
|
645
|
+
}),
|
|
646
|
+
jsonMode
|
|
647
|
+
? [
|
|
648
|
+
react_jsx_runtime.jsx('textarea', {
|
|
649
|
+
className: manageStyles.textarea,
|
|
650
|
+
value: jsonText,
|
|
651
|
+
onChange: (event) => {
|
|
652
|
+
setJsonText(event.target.value)
|
|
653
|
+
},
|
|
654
|
+
}),
|
|
655
|
+
]
|
|
656
|
+
: [
|
|
657
|
+
react_jsx_runtime.jsx(ManageField, {
|
|
658
|
+
label: t('mcp.form.name'),
|
|
659
|
+
children: react_jsx_runtime.jsx('input', {
|
|
660
|
+
className: manageStyles.input,
|
|
661
|
+
value: draft.name,
|
|
662
|
+
placeholder: 'my-server',
|
|
663
|
+
disabled: editing !== null,
|
|
664
|
+
onChange: (event) => {
|
|
665
|
+
setDraftField('name', event.target.value)
|
|
666
|
+
},
|
|
667
|
+
}),
|
|
668
|
+
}),
|
|
669
|
+
react_jsx_runtime.jsx(ManageField, {
|
|
670
|
+
label: t('mcp.form.transport'),
|
|
671
|
+
children: react_jsx_runtime.jsxs(
|
|
672
|
+
'select',
|
|
673
|
+
{
|
|
674
|
+
className: manageStyles.input,
|
|
675
|
+
value: draft.transport,
|
|
676
|
+
onChange: (event) => {
|
|
677
|
+
setDraftField('transport', event.target.value)
|
|
678
|
+
},
|
|
679
|
+
children: [
|
|
680
|
+
react_jsx_runtime.jsx('option', { value: 'stdio', children: t('mcp.stdio') }),
|
|
681
|
+
react_jsx_runtime.jsx('option', { value: 'streamable-http', children: t('mcp.streamable-http') }),
|
|
682
|
+
],
|
|
683
|
+
},
|
|
684
|
+
),
|
|
685
|
+
}),
|
|
686
|
+
draft.transport === 'stdio'
|
|
687
|
+
? react_jsx_runtime.jsxs(react_jsx_runtime.Fragment, {
|
|
688
|
+
children: [
|
|
689
|
+
react_jsx_runtime.jsx(ManageField, {
|
|
690
|
+
label: t('mcp.form.command'),
|
|
691
|
+
children: react_jsx_runtime.jsx('input', {
|
|
692
|
+
className: manageStyles.input,
|
|
693
|
+
value: draft.command,
|
|
694
|
+
placeholder: 'npx',
|
|
695
|
+
onChange: (event) => {
|
|
696
|
+
setDraftField('command', event.target.value)
|
|
697
|
+
},
|
|
698
|
+
}),
|
|
699
|
+
}),
|
|
700
|
+
react_jsx_runtime.jsx(ManageField, {
|
|
701
|
+
label: t('mcp.form.args'),
|
|
702
|
+
children: react_jsx_runtime.jsx('textarea', {
|
|
703
|
+
className: manageStyles.textarea,
|
|
704
|
+
value: draft.argsText,
|
|
705
|
+
onChange: (event) => {
|
|
706
|
+
setDraftField('argsText', event.target.value)
|
|
707
|
+
},
|
|
708
|
+
}),
|
|
709
|
+
}),
|
|
710
|
+
react_jsx_runtime.jsx(ManageField, {
|
|
711
|
+
label: t('mcp.form.env'),
|
|
712
|
+
children: react_jsx_runtime.jsx('textarea', {
|
|
713
|
+
className: manageStyles.textarea,
|
|
714
|
+
value: draft.envText,
|
|
715
|
+
onChange: (event) => {
|
|
716
|
+
setDraftField('envText', event.target.value)
|
|
717
|
+
},
|
|
718
|
+
}),
|
|
719
|
+
}),
|
|
720
|
+
react_jsx_runtime.jsx(ManageField, {
|
|
721
|
+
label: t('mcp.form.cwd'),
|
|
722
|
+
children: react_jsx_runtime.jsx('input', {
|
|
723
|
+
className: manageStyles.input,
|
|
724
|
+
value: draft.cwd,
|
|
725
|
+
onChange: (event) => {
|
|
726
|
+
setDraftField('cwd', event.target.value)
|
|
727
|
+
},
|
|
728
|
+
}),
|
|
729
|
+
}),
|
|
730
|
+
],
|
|
731
|
+
})
|
|
732
|
+
: react_jsx_runtime.jsxs(react_jsx_runtime.Fragment, {
|
|
733
|
+
children: [
|
|
734
|
+
react_jsx_runtime.jsx(ManageField, {
|
|
735
|
+
label: t('mcp.form.url'),
|
|
736
|
+
children: react_jsx_runtime.jsx('input', {
|
|
737
|
+
className: manageStyles.input,
|
|
738
|
+
value: draft.url,
|
|
739
|
+
placeholder: 'http://127.0.0.1:9000/mcp',
|
|
740
|
+
onChange: (event) => {
|
|
741
|
+
setDraftField('url', event.target.value)
|
|
742
|
+
},
|
|
743
|
+
}),
|
|
744
|
+
}),
|
|
745
|
+
react_jsx_runtime.jsx(ManageField, {
|
|
746
|
+
label: t('mcp.form.headers'),
|
|
747
|
+
children: react_jsx_runtime.jsx('textarea', {
|
|
748
|
+
className: manageStyles.textarea,
|
|
749
|
+
value: draft.headersText,
|
|
750
|
+
onChange: (event) => {
|
|
751
|
+
setDraftField('headersText', event.target.value)
|
|
752
|
+
},
|
|
753
|
+
}),
|
|
754
|
+
}),
|
|
755
|
+
],
|
|
756
|
+
}),
|
|
757
|
+
react_jsx_runtime.jsx(ManageField, {
|
|
758
|
+
label: t('mcp.form.timeout'),
|
|
759
|
+
children: react_jsx_runtime.jsx('input', {
|
|
760
|
+
className: manageStyles.input,
|
|
761
|
+
value: draft.toolCallTimeoutMs,
|
|
762
|
+
type: 'number',
|
|
763
|
+
min: '1',
|
|
764
|
+
onChange: (event) => {
|
|
765
|
+
setDraftField('toolCallTimeoutMs', event.target.value)
|
|
766
|
+
},
|
|
767
|
+
}),
|
|
768
|
+
}),
|
|
769
|
+
],
|
|
770
|
+
],
|
|
771
|
+
})
|
|
772
|
+
: null,
|
|
773
|
+
react_jsx_runtime.jsx('div', {
|
|
774
|
+
className: manageStyles.list,
|
|
775
|
+
children:
|
|
776
|
+
status === 'loading'
|
|
777
|
+
? react_jsx_runtime.jsx('div', { className: manageStyles.hint, children: t('mcp.loading') })
|
|
778
|
+
: servers.length === 0
|
|
779
|
+
? react_jsx_runtime.jsx('div', { className: manageStyles.hint, children: t('mcp.empty') })
|
|
780
|
+
: servers.map((server) =>
|
|
781
|
+
react_jsx_runtime.jsxs(
|
|
782
|
+
'div',
|
|
783
|
+
{
|
|
784
|
+
className: manageStyles.card,
|
|
785
|
+
children: [
|
|
786
|
+
react_jsx_runtime.jsxs('div', {
|
|
787
|
+
className: manageStyles.cardHead,
|
|
788
|
+
children: [
|
|
789
|
+
react_jsx_runtime.jsx('span', { className: manageStyles.cardName, children: server.name }),
|
|
790
|
+
react_jsx_runtime.jsxs('div', {
|
|
791
|
+
className: manageStyles.cardMeta,
|
|
792
|
+
children: [
|
|
793
|
+
react_jsx_runtime.jsx('span', {
|
|
794
|
+
className: manageStyles.pill,
|
|
795
|
+
children: server.transport === 'stdio' ? t('mcp.stdio') : t('mcp.streamable-http'),
|
|
796
|
+
}),
|
|
797
|
+
react_jsx_runtime.jsx('span', {
|
|
798
|
+
className: manageStyles.pill,
|
|
799
|
+
children: server.enabled === false ? t('mcp.disabled') : t('mcp.enabled'),
|
|
800
|
+
}),
|
|
801
|
+
],
|
|
802
|
+
}),
|
|
803
|
+
react_jsx_runtime.jsxs('div', {
|
|
804
|
+
className: manageStyles.actions,
|
|
805
|
+
children: [
|
|
806
|
+
react_jsx_runtime.jsx(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
807
|
+
variant: 'ghost',
|
|
808
|
+
size: 'sm',
|
|
809
|
+
icon:
|
|
810
|
+
testing === server.name
|
|
811
|
+
? react_jsx_runtime.jsx(_deepseek_ai_dsh_client_ui_primitives.IconLoadingOutline16, { size: 14 })
|
|
812
|
+
: react_jsx_runtime.jsx(_deepseek_ai_dsh_client_ui_primitives.IconPlayOutline16, { size: 14 }),
|
|
813
|
+
disabled: busy || testing !== null,
|
|
814
|
+
onClick: () => {
|
|
815
|
+
test(server.name)
|
|
816
|
+
},
|
|
817
|
+
children: testing === server.name ? t('mcp.testing') : t('mcp.test'),
|
|
818
|
+
}),
|
|
819
|
+
react_jsx_runtime.jsx(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
820
|
+
variant: 'ghost',
|
|
821
|
+
size: 'sm',
|
|
822
|
+
icon: react_jsx_runtime.jsx(_deepseek_ai_dsh_client_ui_primitives.IconEditOutline16, { size: 14 }),
|
|
823
|
+
disabled: busy || testing !== null,
|
|
824
|
+
onClick: () => {
|
|
825
|
+
openForm(server)
|
|
826
|
+
},
|
|
827
|
+
children: t('mcp.edit'),
|
|
828
|
+
}),
|
|
829
|
+
react_jsx_runtime.jsx(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
830
|
+
variant: 'ghost',
|
|
831
|
+
size: 'sm',
|
|
832
|
+
icon: react_jsx_runtime.jsx(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, { size: 14 }),
|
|
833
|
+
disabled: busy || testing !== null,
|
|
834
|
+
onClick: () => {
|
|
835
|
+
remove(server.name)
|
|
836
|
+
},
|
|
837
|
+
children: t('mcp.remove'),
|
|
838
|
+
}),
|
|
839
|
+
],
|
|
840
|
+
}),
|
|
841
|
+
],
|
|
842
|
+
}),
|
|
843
|
+
react_jsx_runtime.jsx('div', {
|
|
844
|
+
className: manageStyles.cardDesc,
|
|
845
|
+
title: server.transport === 'stdio' ? server.command + ' ' + (server.args ?? []).join(' ') : server.url,
|
|
846
|
+
children: server.transport === 'stdio' ? server.command + ' ' + (server.args ?? []).join(' ') : server.url,
|
|
847
|
+
}),
|
|
848
|
+
],
|
|
849
|
+
},
|
|
850
|
+
server.name,
|
|
851
|
+
),
|
|
852
|
+
),
|
|
853
|
+
}),
|
|
854
|
+
],
|
|
855
|
+
})
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// ── copy dictionaries ──────────────────────────────────────────────────────
|
|
859
|
+
var MANAGE_NS = 'settings-mcp-skills'
|
|
860
|
+
var manageZh = {
|
|
861
|
+
'skills.nav': 'Skills',
|
|
862
|
+
'skills.title': 'Skills管理',
|
|
863
|
+
'skills.refresh': '刷新',
|
|
864
|
+
'skills.new': '导入技能',
|
|
865
|
+
'skills.loading': '加载中…',
|
|
866
|
+
'skills.empty': '还没有技能。点击“导入技能”导入第一个。',
|
|
867
|
+
'skills.remove': '删除',
|
|
868
|
+
'skills.removeConfirm': '确定删除技能 {name} 吗?该操作会删除其目录。',
|
|
869
|
+
'skills.imported': '已导入技能。',
|
|
870
|
+
'skills.importedMany': '已导入 {count} 个技能。',
|
|
871
|
+
'skills.importedSome': '已导入 {count} 个技能,跳过 {skipped} 个(已存在或不支持)。',
|
|
872
|
+
'skills.importedNone': '未导入任何技能:{count} 个已存在或不支持。',
|
|
873
|
+
'skills.removed': '已删除。',
|
|
874
|
+
'skills.enabled': '允许模型调用',
|
|
875
|
+
'mcp.nav': 'MCP',
|
|
876
|
+
'mcp.title': 'MCP 服务器',
|
|
877
|
+
'mcp.refresh': '刷新',
|
|
878
|
+
'mcp.new': '添加服务器',
|
|
879
|
+
'mcp.loading': '加载中…',
|
|
880
|
+
'mcp.empty': '还没有配置 MCP 服务器。',
|
|
881
|
+
'mcp.enabled': '已启用',
|
|
882
|
+
'mcp.disabled': '已停用',
|
|
883
|
+
'mcp.stdio': 'stdio',
|
|
884
|
+
'mcp.streamable-http': 'Streamable HTTP',
|
|
885
|
+
'mcp.test': '测试',
|
|
886
|
+
'mcp.testing': '测试中…',
|
|
887
|
+
'mcp.edit': '编辑',
|
|
888
|
+
'mcp.remove': '删除',
|
|
889
|
+
'mcp.removeConfirm': '确定删除 MCP 服务器 {name} 吗?',
|
|
890
|
+
'mcp.restartHint': '新增或修改的服务器会在重启 dsh 后自动连接并注册工具。',
|
|
891
|
+
'mcp.form.name': '名称(仅字母数字与 _-,最长 32)',
|
|
892
|
+
'mcp.form.transport': '传输方式',
|
|
893
|
+
'mcp.form.command': '命令',
|
|
894
|
+
'mcp.form.args': '参数(每行一个)',
|
|
895
|
+
'mcp.form.env': '环境变量(KEY=VALUE,每行一个)',
|
|
896
|
+
'mcp.form.cwd': '工作目录(可选)',
|
|
897
|
+
'mcp.form.url': 'URL',
|
|
898
|
+
'mcp.form.headers': '请求头(Name: Value,每行一个)',
|
|
899
|
+
'mcp.form.timeout': '单次调用超时(毫秒)',
|
|
900
|
+
'mcp.form.enabled': '启用',
|
|
901
|
+
'mcp.form.jsonMode': 'JSON 模式',
|
|
902
|
+
'mcp.form.jsonError': 'JSON 解析失败:{message}',
|
|
903
|
+
'mcp.form.add': '添加',
|
|
904
|
+
'mcp.form.save': '保存',
|
|
905
|
+
'mcp.cancel': '取消',
|
|
906
|
+
'mcp.test.ok': '连接成功,发现 {count} 个工具',
|
|
907
|
+
'mcp.test.fail': '连接失败:{message}',
|
|
908
|
+
'mcp.saved': '已保存。',
|
|
909
|
+
'mcp.removed': '已删除。',
|
|
910
|
+
}
|
|
911
|
+
var manageEn = {
|
|
912
|
+
'skills.nav': 'Skills',
|
|
913
|
+
'skills.title': 'Skills Management',
|
|
914
|
+
'skills.refresh': 'Refresh',
|
|
915
|
+
'skills.new': 'Import skill',
|
|
916
|
+
'skills.loading': 'Loading…',
|
|
917
|
+
'skills.empty': 'No skills yet. Click “Import skill” to import one.',
|
|
918
|
+
'skills.remove': 'Remove',
|
|
919
|
+
'skills.removeConfirm': 'Remove skill {name}? Its directory will be deleted.',
|
|
920
|
+
'skills.imported': 'Skill imported.',
|
|
921
|
+
'skills.importedMany': 'Imported {count} skills.',
|
|
922
|
+
'skills.importedSome': 'Imported {count} skill(s); skipped {skipped} (already installed or unsupported).',
|
|
923
|
+
'skills.importedNone': 'Nothing imported: {count} skill(s) already exist or are unsupported.',
|
|
924
|
+
'skills.removed': 'Removed.',
|
|
925
|
+
'skills.enabled': 'Model invocation',
|
|
926
|
+
'mcp.nav': 'MCP',
|
|
927
|
+
'mcp.title': 'MCP Servers',
|
|
928
|
+
'mcp.refresh': 'Refresh',
|
|
929
|
+
'mcp.new': 'Add server',
|
|
930
|
+
'mcp.loading': 'Loading…',
|
|
931
|
+
'mcp.empty': 'No MCP servers configured yet.',
|
|
932
|
+
'mcp.enabled': 'Enabled',
|
|
933
|
+
'mcp.disabled': 'Disabled',
|
|
934
|
+
'mcp.stdio': 'stdio',
|
|
935
|
+
'mcp.streamable-http': 'Streamable HTTP',
|
|
936
|
+
'mcp.test': 'Test',
|
|
937
|
+
'mcp.testing': 'Testing…',
|
|
938
|
+
'mcp.edit': 'Edit',
|
|
939
|
+
'mcp.remove': 'Remove',
|
|
940
|
+
'mcp.removeConfirm': 'Remove MCP server {name}?',
|
|
941
|
+
'mcp.restartHint': 'New or changed servers connect automatically after a dsh restart.',
|
|
942
|
+
'mcp.form.name': 'Name (alphanumeric, _ and - only, max 32)',
|
|
943
|
+
'mcp.form.transport': 'Transport',
|
|
944
|
+
'mcp.form.command': 'Command',
|
|
945
|
+
'mcp.form.args': 'Arguments (one per line)',
|
|
946
|
+
'mcp.form.env': 'Environment (KEY=VALUE, one per line)',
|
|
947
|
+
'mcp.form.cwd': 'Working directory (optional)',
|
|
948
|
+
'mcp.form.url': 'URL',
|
|
949
|
+
'mcp.form.headers': 'Headers (Name: Value, one per line)',
|
|
950
|
+
'mcp.form.timeout': 'Per-call timeout (ms)',
|
|
951
|
+
'mcp.form.enabled': 'Enabled',
|
|
952
|
+
'mcp.form.jsonMode': 'JSON mode',
|
|
953
|
+
'mcp.form.jsonError': 'Invalid JSON: {message}',
|
|
954
|
+
'mcp.form.add': 'Add',
|
|
955
|
+
'mcp.form.save': 'Save',
|
|
956
|
+
'mcp.cancel': 'Cancel',
|
|
957
|
+
'mcp.test.ok': 'Connected; found {count} tools',
|
|
958
|
+
'mcp.test.fail': 'Connection failed: {message}',
|
|
959
|
+
'mcp.saved': 'Saved.',
|
|
960
|
+
'mcp.removed': 'Removed.',
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/** Required services (cordis fiber inject). */
|
|
964
|
+
var inject = ['slots', 'locale']
|
|
965
|
+
|
|
966
|
+
/**
|
|
967
|
+
* Register the Skills and MCP sections once the `settings.section`
|
|
968
|
+
* declaration is on the ledger.
|
|
969
|
+
* @param ctx - client root context.
|
|
970
|
+
*/
|
|
971
|
+
function apply(ctx) {
|
|
972
|
+
ctx.effect(
|
|
973
|
+
() =>
|
|
974
|
+
ctx.locale.register(MANAGE_NS, {
|
|
975
|
+
zh: manageZh,
|
|
976
|
+
en: manageEn,
|
|
977
|
+
}),
|
|
978
|
+
'mcp-skills: dictionaries',
|
|
979
|
+
)
|
|
980
|
+
var t = ctx.locale.bind(MANAGE_NS)
|
|
981
|
+
var api = buildManageApi()
|
|
982
|
+
var injected = () => ({ api, t })
|
|
983
|
+
ctx.slots.inject(
|
|
984
|
+
'settings.section',
|
|
985
|
+
() =>
|
|
986
|
+
ctx.slots.register(
|
|
987
|
+
{
|
|
988
|
+
name: 'settings.section',
|
|
989
|
+
id: 'skills',
|
|
990
|
+
order: 30,
|
|
991
|
+
label: () => t('skills.nav'),
|
|
992
|
+
locale: MANAGE_NS,
|
|
993
|
+
inject: injected,
|
|
994
|
+
},
|
|
995
|
+
SkillsSection,
|
|
996
|
+
),
|
|
997
|
+
)
|
|
998
|
+
ctx.slots.inject(
|
|
999
|
+
'settings.section',
|
|
1000
|
+
() =>
|
|
1001
|
+
ctx.slots.register(
|
|
1002
|
+
{
|
|
1003
|
+
name: 'settings.section',
|
|
1004
|
+
id: 'mcp',
|
|
1005
|
+
order: 40,
|
|
1006
|
+
label: () => t('mcp.nav'),
|
|
1007
|
+
locale: MANAGE_NS,
|
|
1008
|
+
inject: injected,
|
|
1009
|
+
},
|
|
1010
|
+
McpSection,
|
|
1011
|
+
),
|
|
1012
|
+
)
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
exports.apply = apply
|
|
1016
|
+
exports.inject = inject
|
|
1017
|
+
return module.exports
|
|
1018
|
+
},
|
|
1019
|
+
})
|