@lolkda/dsh-chrome-devtools-mcp 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.
@@ -0,0 +1,544 @@
1
+ /**
2
+ * Browser half: the "Chrome DevTools" settings section.
3
+ *
4
+ * It deliberately uses plain React elements and the DSH design tokens instead of
5
+ * importing a UI primitives package. The page must keep rendering even when one
6
+ * endpoint is down, so every data source has its own error and retry path.
7
+ */
8
+
9
+ window.__ModuleLoader__.load({
10
+ id: '@lolkda/dsh-chrome-devtools-mcp',
11
+ factory: (require) => {
12
+ const React = require('react')
13
+ const h = React.createElement
14
+ const { useCallback, useEffect, useMemo, useState } = React
15
+
16
+ const ROUTE = '/api/dsh-chrome-devtools-mcp'
17
+ const SECTION_ORDER = 64
18
+ const STYLE_ID = 'dsh-chrome-devtools-mcp'
19
+ const SSH_USER_KEY = 'dsh-chrome-devtools-mcp.ssh-user'
20
+ const DEFAULT_REMOTE_PORT = 9222
21
+
22
+ const CHROME_DEBUG_PS = '.\\chrome.exe --remote-debugging-port=9222 --user-data-dir="$env:LOCALAPPDATA\\ChromeDebug" --no-first-run --no-default-browser-check'
23
+ const CHROME_DEBUG_CMD = '.\\chrome.exe --remote-debugging-port=9222 --user-data-dir=%LOCALAPPDATA%\\ChromeDebug --no-first-run --no-default-browser-check'
24
+ const CHROME_PATH_HINT = '在 Chrome 地址栏打开 chrome://version,查看“可执行文件路径”,然后 cd 到该目录。'
25
+
26
+ const FALLBACK_WINDOWS_CLIENT = [
27
+ { label: '检查 OpenSSH Client', command: "Get-WindowsCapability -Online | Where-Object Name -like 'OpenSSH.Client*'" },
28
+ { label: '安装 OpenSSH Client', command: 'Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0' },
29
+ ]
30
+ const FALLBACK_WINDOWS_SERVER = [
31
+ { label: '检查 OpenSSH Server', command: "Get-WindowsCapability -Online | Where-Object Name -like 'OpenSSH.Server*'" },
32
+ { label: '安装 OpenSSH Server', command: 'Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0' },
33
+ { label: '启动并设为开机自启', command: 'Start-Service sshd\nSet-Service -Name sshd -StartupType Automatic' },
34
+ { label: '放行 22 端口', command: "New-NetFirewallRule -Name sshd -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22" },
35
+ ]
36
+
37
+ function errorMessage(error) {
38
+ if (!error) return '未知错误'
39
+ if (error.name === 'AbortError') return ''
40
+ return error instanceof Error ? error.message : String(error)
41
+ }
42
+
43
+ async function request(path, { signal, method = 'GET', body } = {}) {
44
+ const response = await fetch(`${ROUTE}${path}`, {
45
+ method,
46
+ credentials: 'same-origin',
47
+ headers: body === undefined ? undefined : { 'content-type': 'application/json' },
48
+ body: body === undefined ? undefined : JSON.stringify(body),
49
+ signal,
50
+ })
51
+ const payload = await response.json().catch(() => null)
52
+ if (!payload) {
53
+ if (response.status === 401) throw new Error('浏览器会话未认证(HTTP 401),请刷新页面后重试')
54
+ if (response.status === 403) throw new Error('宿主拒绝了请求(HTTP 403),请使用 DSH 启动时给出的地址访问')
55
+ throw new Error(`接口返回了非 JSON 响应(HTTP ${response.status})`)
56
+ }
57
+ if (!response.ok || payload.ok === false) throw new Error(payload.error || `请求失败(HTTP ${response.status})`)
58
+ return payload
59
+ }
60
+
61
+ function injectStyle() {
62
+ const existing = document.getElementById(STYLE_ID)
63
+ if (existing) return null
64
+ const tag = document.createElement('style')
65
+ tag.id = STYLE_ID
66
+ tag.textContent = CSS
67
+ document.head.appendChild(tag)
68
+ return tag
69
+ }
70
+
71
+ function formatTime(value) {
72
+ if (!value) return '—'
73
+ const date = new Date(value)
74
+ return Number.isFinite(date.getTime()) ? date.toLocaleString() : value
75
+ }
76
+
77
+ function specLabel(spec) {
78
+ if (!spec) return '—'
79
+ return spec.startsWith('chrome-devtools-mcp@') ? spec.slice('chrome-devtools-mcp@'.length) : spec
80
+ }
81
+
82
+ function configuredLabel(configured) {
83
+ if (!configured) return '—'
84
+ return configured.mode === 'latest' ? 'latest(跟随最新)' : configured.version
85
+ }
86
+
87
+ function statusBadge(status) {
88
+ if (!status) return { tone: 'muted', text: '读取中' }
89
+ if (status.cdp?.reachable) return { tone: 'ok', text: '已连接' }
90
+ if (status.running?.state === 'running') return { tone: 'warn', text: 'Chrome 未连接' }
91
+ return { tone: 'bad', text: '未连接' }
92
+ }
93
+
94
+ function sourceLabel(versions) {
95
+ if (!versions) return ''
96
+ if (versions.source === 'github') return 'GitHub Releases'
97
+ if (versions.source === 'npm') return 'npm 回退'
98
+ if (versions.source === 'cache') return versions.upstreamSource === 'github' ? '缓存(GitHub)' : versions.upstreamSource === 'npm' ? '缓存(npm)' : '缓存'
99
+ return versions.source
100
+ }
101
+
102
+ class Boundary extends React.Component {
103
+ constructor(props) {
104
+ super(props)
105
+ this.state = { error: null }
106
+ }
107
+
108
+ static getDerivedStateFromError(error) {
109
+ return { error }
110
+ }
111
+
112
+ render() {
113
+ if (this.state.error) {
114
+ return h('div', { className: 'dscd-error', role: 'alert' }, `设置页渲染失败:${errorMessage(this.state.error)}`)
115
+ }
116
+ return this.props.children
117
+ }
118
+ }
119
+
120
+ function Row({ label, children, tone }) {
121
+ return h('div', { className: 'dscd-row' },
122
+ h('span', { className: 'dscd-row-label' }, label),
123
+ h('span', { className: tone ? `dscd-row-value dscd-tone-${tone}` : 'dscd-row-value' }, children),
124
+ )
125
+ }
126
+
127
+ function Commands({ title, items, prefix, copied, onCopy }) {
128
+ return h('div', { className: 'dscd-commands' },
129
+ h('h4', { className: 'dscd-subhead' }, title),
130
+ items.map((item, index) => {
131
+ const key = `${prefix}-${index}`
132
+ return h('div', { className: 'dscd-command', key },
133
+ h('div', { className: 'dscd-command-head' },
134
+ h('span', null, item.label),
135
+ h('button', {
136
+ type: 'button',
137
+ className: 'dscd-button dscd-button--ghost',
138
+ onClick: () => onCopy(item.command, key),
139
+ }, copied === key ? '已复制' : '复制'),
140
+ ),
141
+ h('pre', { className: 'dscd-code' }, item.command),
142
+ )
143
+ }),
144
+ )
145
+ }
146
+
147
+ function ChromeDevtoolsSection() {
148
+ const [status, setStatus] = useState(null)
149
+ const [versions, setVersions] = useState(null)
150
+ const [tunnel, setTunnel] = useState(null)
151
+ const [loading, setLoading] = useState(true)
152
+ const [refreshing, setRefreshing] = useState(false)
153
+ const [versionLoading, setVersionLoading] = useState(false)
154
+ const [error, setError] = useState('')
155
+ const [versionError, setVersionError] = useState('')
156
+ const [actionError, setActionError] = useState('')
157
+ const [notice, setNotice] = useState('')
158
+ const [selection, setSelection] = useState(null)
159
+ const [manualVersion, setManualVersion] = useState('')
160
+ const [saving, setSaving] = useState(false)
161
+ const [copied, setCopied] = useState('')
162
+ const [sshUser, setSshUser] = useState(() => {
163
+ try {
164
+ return localStorage.getItem(SSH_USER_KEY) || ''
165
+ } catch {
166
+ return ''
167
+ }
168
+ })
169
+
170
+ useEffect(() => {
171
+ const controller = new AbortController()
172
+ let cancelled = false
173
+ async function load() {
174
+ setLoading(true)
175
+ const results = await Promise.allSettled([
176
+ request('/status', { signal: controller.signal }),
177
+ request('/versions', { signal: controller.signal }),
178
+ request('/tunnel', { signal: controller.signal }),
179
+ ])
180
+ if (cancelled) return
181
+ const [statusResult, versionsResult, tunnelResult] = results
182
+ if (statusResult.status === 'fulfilled') {
183
+ setStatus(statusResult.value)
184
+ setError('')
185
+ } else {
186
+ const message = errorMessage(statusResult.reason)
187
+ if (message) setError(message)
188
+ }
189
+ if (versionsResult.status === 'fulfilled') {
190
+ setVersions(versionsResult.value)
191
+ setVersionError('')
192
+ } else {
193
+ const message = errorMessage(versionsResult.reason)
194
+ if (message) setVersionError(message)
195
+ }
196
+ if (tunnelResult.status === 'fulfilled') setTunnel(tunnelResult.value)
197
+ setLoading(false)
198
+ }
199
+ void load()
200
+ return () => {
201
+ cancelled = true
202
+ controller.abort()
203
+ }
204
+ }, [])
205
+
206
+ useEffect(() => {
207
+ if (selection !== null || !status) return
208
+ setSelection(status.configured.mode === 'latest' ? 'latest' : status.configured.version)
209
+ }, [status, selection])
210
+
211
+ useEffect(() => {
212
+ try {
213
+ localStorage.setItem(SSH_USER_KEY, sshUser)
214
+ } catch {
215
+ // Private browsing may reject localStorage; the command still works.
216
+ }
217
+ }, [sshUser])
218
+
219
+ useEffect(() => {
220
+ if (!copied) return undefined
221
+ const timer = setTimeout(() => setCopied(''), 1800)
222
+ return () => clearTimeout(timer)
223
+ }, [copied])
224
+
225
+ const options = useMemo(() => {
226
+ const list = (versions?.items ?? []).slice()
227
+ const configuredVersion = status?.configured?.mode === 'pinned' ? status.configured.version : null
228
+ if (configuredVersion && !list.some((item) => item.version === configuredVersion)) {
229
+ list.unshift({ version: configuredVersion, tag: `v${configuredVersion}`, publishedAt: null })
230
+ }
231
+ return list
232
+ }, [versions, status])
233
+
234
+ const needsManual = Boolean(versionError) || options.length === 0
235
+ const targetVersion = manualVersion.trim() || (selection && selection !== 'latest' ? selection : '')
236
+ const canApply = !saving && (selection === 'latest' || targetVersion !== '')
237
+ const pendingRestart = Boolean(
238
+ status
239
+ && status.running?.state === 'running'
240
+ && (
241
+ status.configured.mode === 'latest'
242
+ ? status.running.spec !== 'chrome-devtools-mcp@latest'
243
+ : status.running.spec !== `chrome-devtools-mcp@${status.configured.version}`
244
+ ),
245
+ )
246
+
247
+ const refreshStatus = useCallback(async () => {
248
+ setRefreshing(true)
249
+ try {
250
+ const data = await request('/status')
251
+ setStatus(data)
252
+ setError('')
253
+ } catch (failure) {
254
+ setError(errorMessage(failure))
255
+ } finally {
256
+ setRefreshing(false)
257
+ }
258
+ }, [])
259
+
260
+ const refreshVersions = useCallback(async () => {
261
+ setVersionLoading(true)
262
+ try {
263
+ const data = await request('/versions?refresh=1')
264
+ setVersions(data)
265
+ setVersionError('')
266
+ } catch (failure) {
267
+ setVersionError(errorMessage(failure))
268
+ } finally {
269
+ setVersionLoading(false)
270
+ }
271
+ }, [])
272
+
273
+ const applyVersion = useCallback(async () => {
274
+ if (!canApply) return
275
+ const body = selection === 'latest' || (!manualVersion.trim() && selection === 'latest')
276
+ ? { mode: 'latest' }
277
+ : { mode: 'pinned', version: manualVersion.trim() || selection }
278
+ if (body.mode === 'pinned' && !/^\d+\.\d+\.\d+$/.test(body.version)) {
279
+ setActionError('固定版本必须是 x.y.z 形式')
280
+ return
281
+ }
282
+ setSaving(true)
283
+ setActionError('')
284
+ setNotice('')
285
+ try {
286
+ const data = await request('/version', { method: 'POST', body })
287
+ setStatus((current) => current
288
+ ? { ...current, configured: data.configured }
289
+ : { configured: data.configured, running: { state: 'unknown' }, cdp: { reachable: false }, browserUrl: 'http://127.0.0.1:9222' })
290
+ setSelection(data.configured.mode === 'latest' ? 'latest' : data.configured.version)
291
+ setManualVersion('')
292
+ setNotice('已保存。重启 DSH 进程后生效;刷新页面不会切换正在运行的 MCP。')
293
+ } catch (failure) {
294
+ setActionError(errorMessage(failure))
295
+ } finally {
296
+ setSaving(false)
297
+ }
298
+ }, [canApply, manualVersion, selection])
299
+
300
+ const copyText = useCallback(async (text, key) => {
301
+ try {
302
+ await navigator.clipboard.writeText(text)
303
+ setCopied(key)
304
+ return
305
+ } catch {
306
+ // Fall through to the textarea fallback.
307
+ }
308
+ try {
309
+ const area = document.createElement('textarea')
310
+ area.value = text
311
+ area.setAttribute('readonly', 'readonly')
312
+ area.style.position = 'fixed'
313
+ area.style.opacity = '0'
314
+ document.body.appendChild(area)
315
+ area.select()
316
+ const ok = document.execCommand('copy')
317
+ area.remove()
318
+ if (ok) setCopied(key)
319
+ else setActionError('复制失败,请手动选择命令文本')
320
+ } catch {
321
+ setActionError('复制失败,请手动选择命令文本')
322
+ }
323
+ }, [])
324
+
325
+ const pageHost = typeof window !== 'undefined' ? window.location.hostname : ''
326
+ const tunnelHost = pageHost && !['localhost', '127.0.0.1', '::1'].includes(pageHost)
327
+ ? pageHost
328
+ : tunnel?.host || pageHost || '127.0.0.1'
329
+ const sshPort = tunnel?.sshPort ?? 22
330
+ const remotePort = tunnel?.remotePort ?? DEFAULT_REMOTE_PORT
331
+ const sshAccount = sshUser.trim() || '<user>'
332
+ const portArgument = sshPort === 22 ? '' : ` -p ${sshPort}`
333
+ const sshCommand = `ssh -N -o ServerAliveInterval=30 -o ExitOnForwardFailure=yes${portArgument} -R ${remotePort}:127.0.0.1:${remotePort} ${sshAccount}@${tunnelHost}`
334
+ const windowsClient = tunnel?.windowsClient ?? FALLBACK_WINDOWS_CLIENT
335
+ const windowsServer = tunnel?.windowsServer ?? FALLBACK_WINDOWS_SERVER
336
+ const badge = statusBadge(status)
337
+ const source = sourceLabel(versions)
338
+
339
+ if (loading) {
340
+ return h('section', { className: 'dscd-section' },
341
+ h('h2', { className: 'dscd-title' }, 'Chrome DevTools'),
342
+ h('p', { className: 'dscd-hint' }, '正在读取 MCP 状态…'),
343
+ )
344
+ }
345
+
346
+ return h('section', { className: 'dscd-section' },
347
+ h('header', { className: 'dscd-header' },
348
+ h('div', null,
349
+ h('h2', { className: 'dscd-title' }, 'Chrome DevTools'),
350
+ h('p', { className: 'dscd-subtitle' }, 'MCP 进程、Chrome CDP 连接和上游版本。'),
351
+ ),
352
+ h('span', { className: `dscd-badge dscd-badge--${badge.tone}` }, badge.text),
353
+ ),
354
+
355
+ error ? h('div', { className: 'dscd-error', role: 'alert' },
356
+ h('span', null, error),
357
+ h('button', { type: 'button', className: 'dscd-button dscd-button--ghost', onClick: refreshStatus }, '重试'),
358
+ ) : null,
359
+
360
+ h('div', { className: 'dscd-card' },
361
+ h('div', { className: 'dscd-card-head' },
362
+ h('h3', null, '状态'),
363
+ h('button', { type: 'button', className: 'dscd-button dscd-button--ghost', onClick: refreshStatus, disabled: refreshing }, refreshing ? '刷新中…' : '刷新'),
364
+ ),
365
+ h('div', { className: 'dscd-grid' },
366
+ h(Row, { label: 'MCP 进程', tone: status?.running?.state === 'running' ? 'ok' : 'muted' },
367
+ status?.running?.state === 'running'
368
+ ? `运行中 · PID ${status.running.pid}`
369
+ : status?.running?.state === 'stopped' ? '已退出' : '未知',
370
+ ),
371
+ h(Row, { label: '运行版本' }, specLabel(status?.running?.spec)),
372
+ h(Row, { label: 'Chrome CDP', tone: status?.cdp?.reachable ? 'ok' : 'bad' },
373
+ status?.cdp?.reachable ? (status.cdp.browser || '已连接') : (status?.cdp?.error ? `无法连接:${status.cdp.error}` : '不可达'),
374
+ ),
375
+ h(Row, { label: '配置版本' }, configuredLabel(status?.configured)),
376
+ h(Row, { label: '最新版本' }, versions?.latest || '—'),
377
+ h(Row, { label: 'CDP 端点' }, status?.browserUrl || '—'),
378
+ ),
379
+ pendingRestart ? h('p', { className: 'dscd-notice', role: 'status' }, '配置版本与运行版本不一致:重启 DSH 进程后生效。') : null,
380
+ status?.running?.startedAt ? h('p', { className: 'dscd-foot' }, `启动于 ${formatTime(status.running.startedAt)}`) : null,
381
+ status?.configuredWarning ? h('p', { className: 'dscd-error' }, status.configuredWarning) : null,
382
+ ),
383
+
384
+ h('div', { className: 'dscd-card' },
385
+ h('div', { className: 'dscd-card-head' },
386
+ h('h3', null, '版本'),
387
+ h('span', { className: 'dscd-meta' }, source || '未加载'),
388
+ ),
389
+ h('label', { className: 'dscd-field' },
390
+ h('span', null, '上游版本'),
391
+ h('select', {
392
+ className: 'dscd-select',
393
+ value: selection ?? '',
394
+ onChange: (event) => { setSelection(event.target.value); setManualVersion('') },
395
+ },
396
+ h('option', { value: 'latest' }, 'latest(跟随最新)'),
397
+ options.map((item) => h('option', { key: item.version, value: item.version },
398
+ `${item.version}${item.publishedAt ? ` · ${new Date(item.publishedAt).toLocaleDateString()}` : ''}`,
399
+ )),
400
+ ),
401
+ ),
402
+ versions?.prereleaseCount ? h('p', { className: 'dscd-hint' }, `已隐藏 ${versions.prereleaseCount} 个预发布版本。`) : null,
403
+ versionError ? h('div', { className: 'dscd-error' },
404
+ h('span', null, versionError),
405
+ h('button', { type: 'button', className: 'dscd-button dscd-button--ghost', onClick: refreshVersions }, '重试'),
406
+ ) : null,
407
+ needsManual ? h('label', { className: 'dscd-field' },
408
+ h('span', null, '手动输入版本(GitHub 不可用时)'),
409
+ h('input', {
410
+ className: 'dscd-input',
411
+ value: manualVersion,
412
+ placeholder: '1.12.0',
413
+ onChange: (event) => setManualVersion(event.target.value),
414
+ }),
415
+ ) : null,
416
+ versions?.warning ? h('p', { className: 'dscd-hint' }, versions.warning) : null,
417
+ h('div', { className: 'dscd-actions' },
418
+ h('button', { type: 'button', className: 'dscd-button dscd-button--primary', onClick: applyVersion, disabled: !canApply }, saving ? '保存中…' : '应用'),
419
+ h('button', { type: 'button', className: 'dscd-button dscd-button--ghost', onClick: refreshVersions, disabled: versionLoading }, versionLoading ? '刷新中…' : '刷新版本列表'),
420
+ ),
421
+ notice ? h('p', { className: 'dscd-notice', role: 'status' }, notice) : null,
422
+ actionError ? h('p', { className: 'dscd-error', role: 'alert' }, actionError) : null,
423
+ h('p', { className: 'dscd-foot' }, '保存只改变下次启动使用的版本。当前运行的 MCP 不会被热替换。'),
424
+ ),
425
+
426
+ h('div', { className: 'dscd-card' },
427
+ h('div', { className: 'dscd-card-head' },
428
+ h('h3', null, '启动 Chrome'),
429
+ h('span', { className: 'dscd-meta' }, '远程调试'),
430
+ ),
431
+ h('p', { className: 'dscd-hint' }, '先打开 PowerShell,进入 Chrome 安装目录,然后执行:'),
432
+ h('pre', { className: 'dscd-code' }, CHROME_DEBUG_PS),
433
+ h('div', { className: 'dscd-actions' },
434
+ h('button', { type: 'button', className: 'dscd-button dscd-button--primary', onClick: () => copyText(CHROME_DEBUG_PS, 'chrome-ps') }, copied === 'chrome-ps' ? '已复制' : '复制启动命令'),
435
+ ),
436
+ h('details', { className: 'dscd-details' },
437
+ h('summary', null, '如何找到安装目录 / CMD 版本'),
438
+ h('p', { className: 'dscd-hint' }, CHROME_PATH_HINT),
439
+ h('p', { className: 'dscd-hint' }, 'CMD 版本:'),
440
+ h('pre', { className: 'dscd-code' }, CHROME_DEBUG_CMD),
441
+ h('div', { className: 'dscd-actions' },
442
+ h('button', { type: 'button', className: 'dscd-button dscd-button--ghost', onClick: () => copyText(CHROME_DEBUG_CMD, 'chrome-cmd') }, copied === 'chrome-cmd' ? '已复制' : '复制 CMD 命令'),
443
+ ),
444
+ ),
445
+ h('p', { className: 'dscd-foot' }, '调试 profile 与日常 Chrome 分开;首次启动需要重新登录。'),
446
+ ),
447
+
448
+ h('div', { className: 'dscd-card' },
449
+ h('div', { className: 'dscd-card-head' },
450
+ h('h3', null, 'SSH 反向隧道'),
451
+ h('span', { className: 'dscd-meta' }, '只生成命令,不执行'),
452
+ ),
453
+ h('label', { className: 'dscd-field' },
454
+ h('span', null, 'SSH 用户名(DSH 主机账号)'),
455
+ h('input', {
456
+ className: 'dscd-input',
457
+ value: sshUser,
458
+ placeholder: 'user',
459
+ onChange: (event) => setSshUser(event.target.value),
460
+ }),
461
+ ),
462
+ h('p', { className: 'dscd-hint' }, '在 Windows PowerShell 中执行,保持窗口常驻。OpenSSH Server 的安装与防火墙命令需要管理员权限。'),
463
+ h('pre', { className: 'dscd-code' }, sshCommand),
464
+ h('div', { className: 'dscd-actions' },
465
+ h('button', { type: 'button', className: 'dscd-button dscd-button--primary', onClick: () => copyText(sshCommand, 'ssh') }, copied === 'ssh' ? '已复制' : '复制命令'),
466
+ ),
467
+ h('details', { className: 'dscd-details' },
468
+ h('summary', null, 'Windows OpenSSH 安装与开启'),
469
+ h('p', { className: 'dscd-hint' }, '反向隧道只需要 OpenSSH Client。OpenSSH Server 是可选能力,只有需要从 DSH 主动 SSH 到 Windows 时才需要。'),
470
+ h(Commands, { title: 'OpenSSH Client', items: windowsClient, prefix: 'client', copied, onCopy: copyText }),
471
+ h(Commands, { title: 'OpenSSH Server(可选)', items: windowsServer, prefix: 'server', copied, onCopy: copyText }),
472
+ ),
473
+ h('p', { className: 'dscd-foot' }, '命令中的 127.0.0.1:9222 是 DSH 主机侧地址;隧道把 Windows 的 Chrome CDP 映射到这里。'),
474
+ ),
475
+ )
476
+ }
477
+
478
+ const inject = ['slots']
479
+
480
+ function apply(ctx) {
481
+ ctx.effect(() => {
482
+ const tag = injectStyle()
483
+ return () => {
484
+ if (tag !== null) tag.remove()
485
+ }
486
+ }, 'chrome-devtools-mcp: settings styles')
487
+
488
+ ctx.slots.inject('settings.section', () => ctx.slots.register({
489
+ name: 'settings.section',
490
+ id: 'dsh-chrome-devtools-mcp',
491
+ order: SECTION_ORDER,
492
+ label: () => 'Chrome MCP',
493
+ inject: () => ({}),
494
+ }, () => h(Boundary, null, h(ChromeDevtoolsSection))))
495
+ }
496
+
497
+ const CSS = `
498
+ .dscd-section{max-width:760px;min-width:0;display:flex;flex-direction:column;gap:12px;padding-bottom:12px;color:var(--dsw-alias-label-primary)}
499
+ .dscd-header{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;min-width:0}
500
+ .dscd-title{margin:0;font-size:16px;line-height:24px;font-weight:600}
501
+ .dscd-subtitle{margin:2px 0 0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}
502
+ .dscd-badge{flex:none;display:inline-flex;align-items:center;height:24px;padding:0 10px;border-radius:12px;font-size:12px;line-height:24px;white-space:nowrap}
503
+ .dscd-badge--ok{background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 14%,transparent);color:var(--dsw-alias-state-success-primary)}
504
+ .dscd-badge--warn{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 14%,transparent);color:var(--dsw-alias-state-warn-primary)}
505
+ .dscd-badge--bad{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent);color:var(--dsw-alias-state-error-primary)}
506
+ .dscd-badge--muted{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-tertiary)}
507
+ .dscd-card{display:flex;flex-direction:column;gap:10px;min-width:0;padding:14px;border:.5px solid var(--dsw-alias-border-l1);border-radius:16px;background:var(--dsw-alias-bg-base)}
508
+ .dscd-card-head{display:flex;align-items:center;justify-content:space-between;gap:8px;min-width:0}
509
+ .dscd-card-head h3{margin:0;font-size:14px;line-height:22px;font-weight:600}
510
+ .dscd-meta{font-size:11px;line-height:16px;color:var(--dsw-alias-label-quaternary)}
511
+ .dscd-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px 18px;min-width:0}
512
+ .dscd-row{display:flex;flex-direction:column;gap:2px;min-width:0;padding:6px 0;border-bottom:.5px solid var(--dsw-alias-border-l1)}
513
+ .dscd-row-label{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}
514
+ .dscd-row-value{font-size:13px;line-height:20px;overflow-wrap:anywhere}
515
+ .dscd-tone-ok{color:var(--dsw-alias-state-success-primary)}
516
+ .dscd-tone-bad{color:var(--dsw-alias-state-error-primary)}
517
+ .dscd-tone-muted{color:var(--dsw-alias-label-tertiary)}
518
+ .dscd-field{display:flex;flex-direction:column;gap:6px;min-width:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}
519
+ .dscd-select,.dscd-input{box-sizing:border-box;width:100%;height:34px;padding:0 10px;border:.5px solid var(--dsw-alias-border-l3);border-radius:9px;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px}
520
+ .dscd-select:focus,.dscd-input:focus{outline:none;border-color:var(--dsw-alias-state-business-primary)}
521
+ .dscd-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-width:0}
522
+ .dscd-button{display:inline-flex;align-items:center;justify-content:center;min-height:32px;padding:0 12px;border:.5px solid var(--dsw-alias-border-l3);border-radius:16px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;line-height:20px;cursor:pointer;white-space:nowrap}
523
+ .dscd-button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}
524
+ .dscd-button:disabled{color:var(--dsw-alias-label-quaternary);border-color:var(--dsw-alias-border-l4);cursor:default}
525
+ .dscd-button--primary{border-color:transparent;background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}
526
+ .dscd-button--primary:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}
527
+ .dscd-button--primary:disabled{background:var(--dsw-alias-button-primary-dimmed);color:var(--dsw-alias-label-quaternary)}
528
+ .dscd-button--ghost{border-color:transparent;background:transparent;color:var(--dsw-alias-label-secondary)}
529
+ .dscd-hint,.dscd-foot{margin:0;font-size:11px;line-height:17px;color:var(--dsw-alias-label-quaternary);overflow-wrap:anywhere}
530
+ .dscd-error{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;margin:0;padding:8px 10px;border:.5px solid color-mix(in srgb,var(--dsw-alias-state-error-primary) 35%,transparent);border-radius:10px;background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 7%,transparent);color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px}
531
+ .dscd-notice{margin:0;padding:8px 10px;border-radius:10px;background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 8%,transparent);color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
532
+ .dscd-code{margin:0;padding:10px 12px;border:.5px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-family:var(--ds-font-family-code,ui-monospace,SFMono-Regular,Consolas,monospace);font-size:11px;line-height:17px;white-space:pre-wrap;overflow-wrap:anywhere;overflow:auto;max-height:220px}
533
+ .dscd-details{border-top:.5px solid var(--dsw-alias-border-l1);padding-top:8px}
534
+ .dscd-details summary{cursor:pointer;font-size:12px;line-height:20px;color:var(--dsw-alias-label-secondary)}
535
+ .dscd-subhead{margin:8px 0 4px;font-size:12px;line-height:18px;font-weight:600;color:var(--dsw-alias-label-secondary)}
536
+ .dscd-command{display:flex;flex-direction:column;gap:4px;margin:6px 0}
537
+ .dscd-command-head{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}
538
+ .dscd-command-head .dscd-button{min-height:26px;padding:0 8px;font-size:11px}
539
+ @media(max-width:640px){.dscd-grid{grid-template-columns:1fr}.dscd-header{flex-direction:column}.dscd-badge{align-self:flex-start}}
540
+ `
541
+
542
+ return { name: 'dsh-chrome-devtools-mcp', inject, apply }
543
+ },
544
+ })
@@ -0,0 +1,28 @@
1
+ # Chrome DevTools MCP + settings page.
2
+ #
3
+ # The first row mounts the Host/Client halves. The second row starts the stable
4
+ # launcher, which reads the selected upstream version from
5
+ # $DSH_HOME/dsh-chrome-devtools-mcp/state.json and then runs
6
+ # `npx chrome-devtools-mcp@<latest|version>`.
7
+ #
8
+ # The MCP server still connects to an already-running debuggable Chrome; it does
9
+ # not launch Chrome itself.
10
+
11
+ - insert:
12
+ - id: dsh-chrome-devtools-mcp
13
+ name: '@lolkda/dsh-chrome-devtools-mcp'
14
+
15
+ - id: chrome-devtools-mcp
16
+ name: '@deepseek-ai/dsh-mcp-client'
17
+ config:
18
+ serverName: chrome-devtools
19
+ transport: stdio
20
+ command: !!js process.execPath
21
+ args:
22
+ - !!js decodeURIComponent(new URL('./node_modules/@lolkda/dsh-chrome-devtools-mcp/bin/launcher.mjs', baseUrl).pathname).replace(/^\/([A-Za-z]:)/, '$1')
23
+ - "--dsh-home"
24
+ - !!js process.env.DSH_HOME || decodeURIComponent(new URL('../../', baseUrl).pathname).replace(/^\/([A-Za-z]:)/, '$1').replace(/\/$/, '')
25
+ - "--browser-url=http://127.0.0.1:9222"
26
+ - "--no-usage-statistics"
27
+ failOnStartupError: false
28
+ toolCallTimeoutMs: 120000
Binary file