@xxxyz/dsh-mcp-manager 2.0.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/lib/client.js ADDED
@@ -0,0 +1,303 @@
1
+ // dsh-mcp-manager — durable client half (web bundle, ModuleLoader CJS format).
2
+ // Registers the "MCP 管理" settings page. Talks to the host half through the
3
+ // exact-path HTTP route /dsh-mcp-manager/api (same origin) instead of the
4
+ // dynamic-only host.call channel.
5
+ window.__ModuleLoader__.load({
6
+ id: 'dsh-mcp-manager',
7
+ factory: (require) => {
8
+ var module = { exports: {} }
9
+ var exports = module.exports
10
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
11
+ const React = require('react')
12
+
13
+ const CSS =
14
+ '.mcpm-wrap{font-size:13px;line-height:1.6;display:flex;flex-direction:column;gap:14px;padding:4px 2px;color:inherit}' +
15
+ '.mcpm-wrap h2{margin:0;font-size:16px}' +
16
+ '.mcpm-sub{opacity:.75;font-size:12px}' +
17
+ '.mcpm-msg{padding:8px 10px;border-radius:6px;font-size:12px}' +
18
+ '.mcpm-msg.ok{background:rgba(46,160,67,.15);border:1px solid rgba(46,160,67,.4)}' +
19
+ '.mcpm-msg.err{background:rgba(248,81,73,.15);border:1px solid rgba(248,81,73,.4)}' +
20
+ '.mcpm-msg.info{background:rgba(88,166,255,.15);border:1px solid rgba(88,166,255,.5)}' +
21
+ '.mcpm-path{font-family:monospace;font-size:11px;opacity:.7;word-break:break-all}' +
22
+ '.mcpm-btn{font-size:12px;padding:3px 10px;border-radius:5px;border:1px solid rgba(128,128,128,.5);background:transparent;color:inherit;cursor:pointer;white-space:nowrap}' +
23
+ '.mcpm-btn:disabled{opacity:.5;cursor:default}' +
24
+ '.mcpm-btn.primary{border-color:rgba(88,166,255,.7)}' +
25
+ '.mcpm-btn.danger{border-color:rgba(248,81,73,.7)}' +
26
+ '.mcpm-backup{display:flex;flex-direction:column;gap:8px;border:1px dashed rgba(128,128,128,.45);border-radius:8px;padding:10px 12px}' +
27
+ '.mcpm-backup .row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}' +
28
+ '.mcpm-json{width:100%;box-sizing:border-box;font-family:monospace;font-size:11px;min-height:70px;background:transparent;color:inherit;border:1px solid rgba(128,128,128,.4);border-radius:5px;padding:6px;white-space:pre;overflow:auto}' +
29
+ '.mcpm-report{font-family:monospace;font-size:11px;opacity:.85;word-break:break-all}' +
30
+ '.mcpm-warn{font-size:11px;color:rgba(248,81,73,.95)}' +
31
+ '.mcpm-form{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px 12px;border:1px solid rgba(128,128,128,.35);border-radius:8px;padding:12px}' +
32
+ '.mcpm-form h3{margin:0;grid-column:1/-1;font-size:13px}' +
33
+ '.mcpm-form label{display:flex;flex-direction:column;gap:3px;font-size:12px}' +
34
+ '.mcpm-form .full{grid-column:1/-1}' +
35
+ '.mcpm-form input,.mcpm-form select,.mcpm-form textarea{font-size:12px;padding:4px 6px;border-radius:5px;border:1px solid rgba(128,128,128,.5);background:transparent;color:inherit}' +
36
+ '.mcpm-form textarea{font-family:monospace;resize:vertical;min-height:44px}' +
37
+ '.mcpm-row{border:1px solid rgba(128,128,128,.35);border-radius:8px;padding:10px 12px;display:flex;flex-direction:column;gap:8px}' +
38
+ '.mcpm-row-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}' +
39
+ '.mcpm-name{font-weight:600}' +
40
+ '.mcpm-chip{font-size:11px;padding:1px 8px;border-radius:999px;border:1px solid rgba(128,128,128,.5);white-space:nowrap}' +
41
+ '.mcpm-chip.on{background:rgba(46,160,67,.18);border-color:rgba(46,160,67,.55)}' +
42
+ '.mcpm-chip.off{background:rgba(128,128,128,.15)}' +
43
+ '.mcpm-chip.live{background:rgba(88,166,255,.15);border-color:rgba(88,166,255,.5)}' +
44
+ '.mcpm-chip.bad{background:rgba(248,81,73,.18);border-color:rgba(248,81,73,.6)}' +
45
+ '.mcpm-chip.warn{background:rgba(219,154,4,.18);border-color:rgba(219,154,4,.6)}' +
46
+ '.mcpm-row-actions{display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end}' +
47
+ '.mcpm-detail{font-family:monospace;font-size:11px;opacity:.75;word-break:break-all}' +
48
+ '.mcpm-mask{position:fixed;inset:0;background:transparent;display:flex;align-items:center;justify-content:center;z-index:99999}' +
49
+ '.mcpm-dialog{background:color-mix(in srgb, currentColor 8%, transparent);backdrop-filter:blur(14px) saturate(1.2);border:1px solid rgba(128,128,128,.5);border-radius:10px;padding:16px 18px;min-width:300px;max-width:440px;box-shadow:0 10px 34px rgba(0,0,0,.35);color:inherit}' +
50
+ '.mcpm-dialog-title{font-size:14px;font-weight:600;margin-bottom:8px}' +
51
+ '.mcpm-dialog-body{font-size:12px;opacity:.88;line-height:1.7;margin-bottom:16px;word-break:break-all}' +
52
+ '.mcpm-dialog-actions{display:flex;justify-content:flex-end;gap:8px}'
53
+
54
+ function ensureCss() {
55
+ if (typeof document === 'undefined') return
56
+ const id = 'dsh-mcp-manager'
57
+ if (document.querySelector('style[data-plugin-css="' + id + '"]')) return
58
+ const tag = document.createElement('style')
59
+ tag.dataset.plugin = id
60
+ tag.dataset.pluginCss = id
61
+ tag.textContent = CSS
62
+ document.head.appendChild(tag)
63
+ }
64
+
65
+ function apiCall(op, args) {
66
+ return fetch('/dsh-mcp-manager/api', {
67
+ method: 'POST',
68
+ headers: { 'content-type': 'application/json' },
69
+ body: JSON.stringify({ op, args: args || {} }),
70
+ }).then((r) => r.json()).catch((e) => ({ ok: false, error: String((e && e.message) || e) }))
71
+ }
72
+
73
+ module.exports = {
74
+ name: 'dsh-mcp-manager-client',
75
+ inject: ['timer'],
76
+ apply(ctx) {
77
+ ensureCss()
78
+ const slots = ctx.get('slots')
79
+ if (slots === undefined) return
80
+
81
+ const LEVEL_LABEL = { project: '项目级', global: '全局', loader: '已加载(loader)' }
82
+ const emptyForm = () => ({ serverName: '', transport: 'streamable-http', url: '', command: '', args: '', headers: '', env: '', level: 'project' })
83
+ const kvToLines = (obj) => (obj ? Object.keys(obj).map((k) => k + '=' + obj[k]).join('\n') : '')
84
+
85
+ function MCPPage() {
86
+ const [state, setState] = React.useState({ loading: true, error: null, rows: [], paths: null, errors: [] })
87
+ const [formOpen, setFormOpen] = React.useState(false)
88
+ const [editing, setEditing] = React.useState(null)
89
+ const [busy, setBusy] = React.useState(null)
90
+ const [msg, setMsg] = React.useState(null)
91
+ const [form, setForm] = React.useState(emptyForm())
92
+ const [restartInfo, setRestartInfo] = React.useState(null)
93
+ const [tick, setTick] = React.useState(0)
94
+ const [backup, setBackup] = React.useState(null)
95
+ const [importText, setImportText] = React.useState('')
96
+ const [confirmRow, setConfirmRow] = React.useState(null)
97
+
98
+ const refresh = () => {
99
+ apiCall('mcpm-list', {}).then((res) => {
100
+ setState({ loading: false, error: res && res.ok ? null : ((res && res.error) || '加载失败'), rows: (res && res.rows) || [], paths: (res && res.paths) || null, errors: (res && res.errors) || [] })
101
+ }).catch((e) => setState({ loading: false, error: String((e && e.message) || e), rows: [], paths: null, errors: [] }))
102
+ }
103
+
104
+ React.useEffect(() => { refresh() }, [])
105
+ React.useEffect(() => ctx.interval(() => setTick((t) => t + 1), 1000), [])
106
+ React.useEffect(() => ctx.interval(() => refresh(), 3000), [])
107
+ React.useEffect(() => {
108
+ if (!msg) return
109
+ const d = ctx.timeout(() => setMsg(null), 3000)
110
+ return d
111
+ }, [msg])
112
+
113
+ const run = (method, args, label, onOk) => {
114
+ setMsg(null)
115
+ setBusy(label)
116
+ apiCall(method, args).then((res) => {
117
+ if (res && res.ok) { setMsg({ kind: 'ok', text: '操作成功' }); refresh(); if (onOk) onOk() }
118
+ else setMsg({ kind: 'err', text: (res && res.error) || '操作失败' })
119
+ }).catch((e) => setMsg({ kind: 'err', text: String((e && e.message) || e) })).then(() => { setBusy(null); setRestartInfo(null) })
120
+ }
121
+
122
+ const doExport = () => {
123
+ setMsg(null)
124
+ apiCall('mcpm-export', {}).then((res) => {
125
+ if (res && res.ok) { setBackup({ mode: 'export', json: res.json, savedTo: res.savedTo }); setMsg({ kind: 'ok', text: '已导出' }) }
126
+ else setMsg({ kind: 'err', text: (res && res.error) || '导出失败' })
127
+ }).catch((e) => setMsg({ kind: 'err', text: String((e && e.message) || e) }))
128
+ }
129
+ const doImport = () => {
130
+ setMsg(null)
131
+ apiCall('mcpm-import', { json: importText }).then((res) => {
132
+ if (res && res.ok) {
133
+ setBackup({ mode: 'import', added: res.added || [], skipped: res.skipped || [] })
134
+ setImportText('')
135
+ setMsg({ kind: 'ok', text: '导入完成:新增 ' + (res.added || []).length + ',跳过 ' + (res.skipped || []).length })
136
+ refresh()
137
+ } else setMsg({ kind: 'err', text: (res && res.error) || '导入失败' })
138
+ }).catch((e) => setMsg({ kind: 'err', text: String((e && e.message) || e) }))
139
+ }
140
+
141
+ const setF = (k) => (ev) => setForm(Object.assign({}, form, { [k]: ev.target.value }))
142
+
143
+ const openAdd = () => { setEditing(null); setForm(emptyForm()); setFormOpen(true) }
144
+ const openEdit = (row) => {
145
+ setEditing({ id: row.id, level: row.level })
146
+ setForm({
147
+ serverName: row.serverName || '',
148
+ transport: row.transport === 'stdio' ? 'stdio' : 'streamable-http',
149
+ url: row.url || '',
150
+ command: row.command || '',
151
+ args: (row.args || []).join('\n'),
152
+ headers: kvToLines(row.headers),
153
+ env: kvToLines(row.env),
154
+ level: row.level === 'global' ? 'global' : 'project',
155
+ })
156
+ setFormOpen(true)
157
+ }
158
+ const cancelForm = () => { setFormOpen(false); setEditing(null) }
159
+
160
+ const submitForm = () => {
161
+ const payload = Object.assign({}, form)
162
+ if (payload.transport === 'stdio') { payload.url = ''; payload.headers = '' }
163
+ else { payload.command = ''; payload.args = ''; payload.env = '' }
164
+ const onOk = () => { setFormOpen(false); setEditing(null) }
165
+ if (editing) run('mcpm-edit', Object.assign({ id: editing.id }, payload), 'form', onOk)
166
+ else run('mcpm-add', payload, 'form', onOk)
167
+ }
168
+
169
+ const rowsLocked = formOpen || busy !== null
170
+ const toggleRow = (row) => run('mcpm-set-enabled', { id: row.id, level: row.level, enabled: row.disabled }, row.id + ':toggle')
171
+ const restartRow = (row) => {
172
+ setTick(0)
173
+ setRestartInfo({ id: row.id, name: row.serverName, startedAt: Date.now() })
174
+ run('mcpm-restart', { id: row.id, level: row.level }, row.id + ':restart')
175
+ }
176
+ const removeRow = (row) => setConfirmRow(row)
177
+ const confirmDelete = () => {
178
+ const row = confirmRow
179
+ setConfirmRow(null)
180
+ if (row) run('mcpm-remove', { id: row.id, level: row.level }, row.id + ':remove')
181
+ }
182
+
183
+ const rows = state.rows || []
184
+ const maskKv = (obj) => {
185
+ if (!obj) return null
186
+ return Object.keys(obj).map((k) => k + ': ' + String(obj[k]).replace(/^(.{4}).*$/, '$1****')).join(' ')
187
+ }
188
+
189
+ const liveChip = (row) => {
190
+ if (!row.live) return null
191
+ const phase = row.live.phase
192
+ if (phase === 'failed') {
193
+ return React.createElement('span', { className: 'mcpm-chip bad' }, 'loader: 失败')
194
+ }
195
+ if (!row.live.enabled) {
196
+ return React.createElement('span', { className: 'mcpm-chip live' }, 'loader: off')
197
+ }
198
+ if (phase && phase !== 'active') {
199
+ return React.createElement('span', { className: 'mcpm-chip live' }, 'loader: on · ' + phase)
200
+ }
201
+ const tc = typeof row.toolCount === 'number' ? row.toolCount : null
202
+ const cls = tc === 0 ? 'mcpm-chip warn' : 'mcpm-chip live'
203
+ return React.createElement('span', { className: cls }, 'loader: on · active' + (tc === null ? '' : ' · ' + tc + ' 工具'))
204
+ }
205
+ const liveHint = (row) => {
206
+ if (!row.live) return null
207
+ if (row.live.phase === 'failed') {
208
+ return React.createElement('div', { className: 'mcpm-warn' }, '⚠ loader 启动失败:请检查 URL / 命令 / 凭证 / 网络,然后点“重启”重试')
209
+ }
210
+ if (row.live.phase === 'active' && typeof row.toolCount === 'number' && row.toolCount === 0) {
211
+ return React.createElement('div', { className: 'mcpm-warn' }, '⚠ 已连接但未注册任何工具:服务端可能未就绪或工具列表为空')
212
+ }
213
+ return null
214
+ }
215
+
216
+ return React.createElement('div', { className: 'mcpm-wrap' },
217
+ React.createElement('h2', null, 'MCP 服务管理'),
218
+ React.createElement('div', { className: 'mcpm-sub' },
219
+ '管理 dsh-mcp-client 服务:写入 项目级(web profile) / 全局(home) 的 cordis.patch.yml,经 HMR 实时生效;重启 DSH 后由 Loader 自动加载。'),
220
+ restartInfo && React.createElement('div', { className: 'mcpm-msg info' },
221
+ '重启中… ' + restartInfo.name + '(已等待 ' + Math.floor((Date.now() - restartInfo.startedAt) / 1000) + ' 秒,完成后自动刷新)'),
222
+ msg && React.createElement('div', { className: 'mcpm-msg ' + msg.kind }, msg.text),
223
+ state.error && React.createElement('div', { className: 'mcpm-msg err' }, state.error),
224
+ state.errors && state.errors.length > 0 && React.createElement('div', { className: 'mcpm-msg err' }, '读取补丁告警: ' + state.errors.join('; ')),
225
+ state.paths && React.createElement('div', { className: 'mcpm-path' },
226
+ '项目级: ' + state.paths.project + '\n全局: ' + state.paths.global),
227
+ React.createElement('div', { className: 'mcpm-backup' },
228
+ React.createElement('div', { className: 'row' },
229
+ React.createElement('span', { className: 'mcpm-sub' }, '备份 / 恢复'),
230
+ React.createElement('button', { className: 'mcpm-btn', disabled: busy !== null, onClick: doExport }, '导出配置'),
231
+ React.createElement('button', { className: 'mcpm-btn', disabled: busy !== null, onClick: () => { setBackup({ mode: 'import', added: [], skipped: [] }); setImportText('') } }, '导入配置')),
232
+ backup && backup.mode === 'export' && React.createElement('div', null,
233
+ React.createElement('div', { className: 'mcpm-report' }, '已同时保存到: ' + (backup.savedTo || '(未落盘)')),
234
+ React.createElement('textarea', { className: 'mcpm-json', readOnly: true, value: backup.json })),
235
+ backup && backup.mode === 'import' && React.createElement('div', null,
236
+ React.createElement('textarea', { className: 'mcpm-json', placeholder: '粘贴导出的 JSON(数组或 { rows: [...] })', value: importText, onChange: (ev) => setImportText(ev.target.value) }),
237
+ React.createElement('div', { className: 'row' },
238
+ React.createElement('button', { className: 'mcpm-btn primary', disabled: busy !== null || !importText.trim(), onClick: doImport }, '执行导入'),
239
+ React.createElement('span', { className: 'mcpm-report' }, '新增: ' + (backup.added || []).join(', ') + ' 跳过: ' + (backup.skipped || []).map((s) => s.id + '(' + s.reason + ')').join(', ')))),
240
+ (!backup || backup.mode === 'export') && React.createElement('div', { className: 'mcpm-sub' }, '提示:导入会合并新增,已存在的条目自动跳过;先“导出配置”可获得模板。')),
241
+ React.createElement('div', null,
242
+ React.createElement('button', { className: 'mcpm-btn primary', onClick: () => (formOpen ? cancelForm() : openAdd()) },
243
+ formOpen ? '收起表单' : '+ 新增 MCP 服务')),
244
+ formOpen && React.createElement('div', { className: 'mcpm-form' },
245
+ React.createElement('h3', null, editing ? '编辑 MCP 服务:' + editing.id : '新增 MCP 服务'),
246
+ React.createElement('label', null, '服务名称 serverName(唯一,1-32 位)', React.createElement('input', { value: form.serverName, onChange: setF('serverName'), placeholder: 'e.g. github' })),
247
+ React.createElement('label', null, '传输方式', React.createElement('select', { value: form.transport, onChange: setF('transport') },
248
+ React.createElement('option', { value: 'streamable-http' }, 'streamable-http'),
249
+ React.createElement('option', { value: 'stdio' }, 'stdio'))),
250
+ React.createElement('label', { className: 'full' }, '级别', React.createElement('select', { value: form.level, onChange: setF('level') },
251
+ React.createElement('option', { value: 'project' }, '项目级(本 profile:profiles/web/cordis.patch.yml)'),
252
+ React.createElement('option', { value: 'global' }, '全局(所有 profile:~/.dsh/cordis.patch.yml)'))),
253
+ form.transport === 'streamable-http'
254
+ ? React.createElement('label', { className: 'full' }, '服务 URL', React.createElement('input', { value: form.url, onChange: setF('url'), placeholder: 'https://host/mcp' }))
255
+ : React.createElement('label', { className: 'full' }, '启动命令', React.createElement('input', { value: form.command, onChange: setF('command'), placeholder: 'npx -y @modelcontextprotocol/server-github' })),
256
+ form.transport === 'stdio' && React.createElement('label', { className: 'full' }, '参数(空格/换行分隔)', React.createElement('textarea', { value: form.args, onChange: setF('args'), placeholder: '-y\n@modelcontextprotocol/server-github' })),
257
+ form.transport === 'stdio' && React.createElement('label', { className: 'full' }, '环境变量(每行 key=value)', React.createElement('textarea', { value: form.env, onChange: setF('env'), placeholder: 'GITHUB_TOKEN=xxx' })),
258
+ form.transport === 'streamable-http' && React.createElement('label', { className: 'full' }, '请求头(每行 key=value)', React.createElement('textarea', { value: form.headers, onChange: setF('headers'), placeholder: 'Authorization=Bearer xxx' })),
259
+ React.createElement('label', { className: 'full' }, ' ',
260
+ React.createElement('div', { style: { display: 'flex', gap: '8px' } },
261
+ React.createElement('button', { className: 'mcpm-btn primary', disabled: busy === 'form', onClick: submitForm }, editing ? '保存' : '添加'),
262
+ React.createElement('button', { className: 'mcpm-btn', onClick: cancelForm }, '取消')))),
263
+ state.loading
264
+ ? React.createElement('div', { className: 'mcpm-sub' }, '加载中…')
265
+ : rows.length === 0
266
+ ? React.createElement('div', { className: 'mcpm-sub' }, '暂无 MCP 服务。点击“+ 新增 MCP 服务”添加。')
267
+ : rows.map((row) => React.createElement('div', { key: row.id, className: 'mcpm-row' },
268
+ React.createElement('div', { className: 'mcpm-row-head' },
269
+ React.createElement('span', { className: 'mcpm-name' }, row.serverName),
270
+ React.createElement('span', { className: 'mcpm-chip ' + (row.disabled ? 'off' : 'on') }, row.disabled ? '已禁用' : '已启用'),
271
+ React.createElement('span', { className: 'mcpm-chip' }, LEVEL_LABEL[row.level] || row.level),
272
+ liveChip(row),
273
+ !row.managed && row.level !== 'loader' && React.createElement('span', { className: 'mcpm-chip off' }, '手动添加')),
274
+ liveHint(row),
275
+ React.createElement('div', { className: 'mcpm-detail' },
276
+ 'id: ' + row.id + ' · ' + (row.transport || '?') + (row.url ? ' · ' + row.url : '') + (row.command ? ' · ' + row.command + ((row.args && row.args.length) ? ' ' + row.args.join(' ') : '') : '')),
277
+ (row.headers || row.env) && React.createElement('div', { className: 'mcpm-detail' },
278
+ (row.headers ? 'headers: ' + maskKv(row.headers) + ' ' : '') + (row.env ? 'env: ' + Object.keys(row.env).map((k) => k + ': ****').join(' ') : '')),
279
+ React.createElement('div', { className: 'mcpm-row-actions' },
280
+ row.level !== 'loader' && React.createElement('button', { className: 'mcpm-btn', disabled: rowsLocked, onClick: () => openEdit(row) }, '编辑'),
281
+ React.createElement('button', { className: 'mcpm-btn', disabled: rowsLocked, onClick: () => toggleRow(row) }, row.disabled ? '启用' : '禁用'),
282
+ React.createElement('button', { className: 'mcpm-btn', disabled: rowsLocked, onClick: () => restartRow(row) }, '重启'),
283
+ row.level !== 'loader' && React.createElement('button', { className: 'mcpm-btn danger', disabled: rowsLocked, onClick: () => removeRow(row) }, '删除')))),
284
+ confirmRow && React.createElement('div', { className: 'mcpm-mask', onClick: () => setConfirmRow(null) },
285
+ React.createElement('div', { className: 'mcpm-dialog', onClick: (ev) => ev.stopPropagation() },
286
+ React.createElement('div', { className: 'mcpm-dialog-title' }, '删除 MCP 服务'),
287
+ React.createElement('div', { className: 'mcpm-dialog-body' },
288
+ '确定要删除“' + confirmRow.serverName + '”吗?\n删除后该服务的配置将从补丁文件中移除,相关工具立即下线,此操作不可撤销。'),
289
+ React.createElement('div', { className: 'mcpm-dialog-actions' },
290
+ React.createElement('button', { className: 'mcpm-btn', onClick: () => setConfirmRow(null) }, '取消'),
291
+ React.createElement('button', { className: 'mcpm-btn danger', onClick: confirmDelete }, '删除'))))
292
+ )
293
+ }
294
+
295
+ slots.inject('settings.section', () => slots.register(
296
+ { name: 'settings.section', id: 'mcp-manager', order: 16, label: 'MCP 管理' },
297
+ () => React.createElement(MCPPage)
298
+ ))
299
+ },
300
+ }
301
+ return module.exports
302
+ },
303
+ })