dsh-mcp-panel 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"wire.d.ts","sourceRoot":"","sources":["../../src/wire.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"wire.d.ts","sourceRoot":"","sources":["../../src/wire.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,11 +1,16 @@
1
1
  {
2
2
  "name": "dsh-mcp-panel",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Read-only runtime management panel for the official DeepSeek Harness MCP client: /mcp command, a Settings page MCP tab with connection status, recent errors, reconnect counts and tool inventory (sanitized for display), controlled enable/disable patch suggestions, and an optional one-shot Streamable HTTP connectivity probe.",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/PerryLink/dsh-mcp-panel.git"
8
8
  },
9
+ "homepage": "https://github.com/PerryLink/dsh-mcp-panel#readme",
10
+ "bugs": {
11
+ "url": "https://github.com/PerryLink/dsh-mcp-panel/issues"
12
+ },
13
+ "author": "PerryLink",
9
14
  "private": false,
10
15
  "type": "module",
11
16
  "main": "./lib/index.js",
@@ -28,9 +33,15 @@
28
33
  "files": [
29
34
  "lib",
30
35
  "src",
36
+ "docs",
31
37
  "cordis.patch.yml",
38
+ "CHANGELOG.md",
39
+ "THIRD_PARTY_NOTICES.md",
32
40
  "README.md",
33
41
  "README.zh.md",
42
+ "README.es.md",
43
+ "README.pt.md",
44
+ "README.hi.md",
34
45
  "LICENSE"
35
46
  ],
36
47
  "dsh": {
@@ -72,7 +83,7 @@
72
83
  },
73
84
  "dependencies": {
74
85
  "tsdown": "^0.22.14",
75
- "typescript": "^5.9.0",
86
+ "typescript": "^7.0.0",
76
87
  "zod": "^4.4.3"
77
88
  },
78
89
  "devDependencies": {
@@ -92,20 +103,19 @@
92
103
  "@deepseek-ai/dsh-typert-protocol": "0.1.0-rc.6",
93
104
  "@types/node": "^22.19.0",
94
105
  "@types/react": "18.3.31",
95
- "jsdom": "^26.1.0",
106
+ "jsdom": "^30.0.1",
96
107
  "react": "18.3.1",
97
108
  "react-dom": "18.3.1",
98
- "vitest": "^3.2.0"
109
+ "vitest": "^4.1.10"
99
110
  },
111
+ "license": "Apache-2.0",
100
112
  "scripts": {
101
113
  "build": "node scripts/prepare.mjs",
102
114
  "typecheck": "tsc --noEmit",
103
115
  "typecheck:ci": "tsc -p tsconfig.ci.json --noEmit",
104
116
  "test": "vitest run",
105
117
  "test:watch": "vitest",
106
- "prepare": "node scripts/prepare.mjs",
107
118
  "verify:self-contained": "node scripts/verify-self-contained.mjs",
108
119
  "verify:artifacts": "node scripts/verify-artifacts.mjs"
109
- },
110
- "license": "Apache-2.0"
111
- }
120
+ }
121
+ }
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useEffect, useId, useState, type ReactNode } from 'react'
4
4
  import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
5
- import { presentMcpPanel, probeBadge, type BadgeTone, type PresentedServerRow } from './present.ts'
5
+ import { filterServers, presentMcpPanel, probeBadge, summarizePanel, type BadgeTone, type PresentedServerRow } from './present.ts'
6
6
  import type { McpPanelSnapshot, McpProbeView, ProbeStarted } from '../wire.ts'
7
7
 
8
8
  /** Registration-side injected face: the unwrapped snapshot read + probe start. */
@@ -21,7 +21,7 @@ export type McpPanelTabProps =
21
21
 
22
22
  type ViewState =
23
23
  | { readonly status: 'loading' }
24
- | { readonly status: 'error' }
24
+ | { readonly status: 'error'; readonly message: string }
25
25
  | { readonly status: 'ready'; readonly snapshot: McpPanelSnapshot }
26
26
 
27
27
  /** Localized label for one server badge code. */
@@ -46,6 +46,7 @@ function probeLabel(badge: ReturnType<typeof probeBadge>['badge'], t: McpPanelTa
46
46
  case 'failed': return t('probeFailed')
47
47
  case 'killed': return t('probeKilled')
48
48
  case 'stopping': return t('probeStopping')
49
+ default: return t('probeUnknown')
49
50
  }
50
51
  }
51
52
 
@@ -60,12 +61,16 @@ function formatProbeTime(view: McpProbeView): string {
60
61
  export function McpPanelTab({ status, probe, t }: McpPanelTabProps): ReactNode {
61
62
  const listId = useId()
62
63
  const [request, setRequest] = useState(0)
63
- const [expanded, setExpanded] = useState<string | null>(null)
64
+ // Expanded card set: multiple cards may be open at once, so "expand all"
65
+ // and "collapse all" can act on the whole (filtered) list.
66
+ const [expanded, setExpanded] = useState<Record<string, true>>({})
64
67
  const [probeError, setProbeError] = useState<string | null>(null)
65
68
  const [state, setState] = useState<ViewState>({ status: 'loading' })
66
69
  // Per-card tool filter: one query per server so expanding two cards at once
67
70
  // never filters one card by the other card's search text.
68
71
  const [toolQueries, setToolQueries] = useState<Record<string, string>>({})
72
+ // Server search across names and display targets; filters the card list only.
73
+ const [serverQuery, setServerQuery] = useState('')
69
74
 
70
75
  const reload = (): void => {
71
76
  setRequest(value => value + 1)
@@ -75,7 +80,11 @@ export function McpPanelTab({ status, probe, t }: McpPanelTabProps): ReactNode {
75
80
  let current = true
76
81
  void Promise.resolve().then(() => status()).then(
77
82
  (snapshot) => { if (current) setState({ status: 'ready', snapshot }) },
78
- () => { if (current) setState({ status: 'error' }) },
83
+ (error: unknown) => {
84
+ if (current) {
85
+ setState({ status: 'error', message: error instanceof Error ? error.message : String(error) })
86
+ }
87
+ },
79
88
  )
80
89
  return () => { current = false }
81
90
  }, [status, request])
@@ -97,19 +106,34 @@ export function McpPanelTab({ status, probe, t }: McpPanelTabProps): ReactNode {
97
106
  }
98
107
  }, [intervalMs])
99
108
 
109
+ const model = state.status === 'ready' ? presentMcpPanel(state.snapshot) : undefined
110
+ const visibleServers = model !== undefined ? filterServers(model.servers, serverQuery) : []
111
+ const summary = model !== undefined ? summarizePanel(visibleServers) : null
112
+ const summaryText = summary === null ? '' : t('summary')
113
+ .replace('{total}', String(summary.total))
114
+ .replace('{connected}', String(summary.connected))
115
+ .replace('{errored}', String(summary.errored))
116
+ const probeRunning = model !== undefined && model.probes.some(probe => probe.view.status === 'running')
117
+ // While a probe runs, poll on a short fixed cadence so the probe row (and
118
+ // the disabled probe button) settles even when refreshIntervalMs is 0.
119
+ useEffect(() => {
120
+ if (!probeRunning) return undefined
121
+ const timer = setInterval(() => { if (!document.hidden) reload() }, 1500)
122
+ return () => { clearInterval(timer) }
123
+ }, [probeRunning])
124
+
100
125
  const retry = (): void => {
101
126
  setState({ status: 'loading' })
102
127
  reload()
103
128
  }
104
129
 
105
- const model = state.status === 'ready' ? presentMcpPanel(state.snapshot) : undefined
106
-
107
130
  return (
108
131
  <div className="dmcp-section" data-dsh-mcp-panel="" aria-busy={state.status === 'loading'}>
109
132
  {state.status === 'loading' ? <p className="dmcp-status">{t('loading')}</p> : null}
110
133
  {state.status === 'error' ? (
111
134
  <div className="dmcp-failure">
112
135
  <p role="alert">{t('error')}</p>
136
+ <p className="dmcp-failure-detail">{state.message}</p>
113
137
  <button type="button" onClick={retry}>{t('retry')}</button>
114
138
  </div>
115
139
  ) : null}
@@ -118,12 +142,34 @@ export function McpPanelTab({ status, probe, t }: McpPanelTabProps): ReactNode {
118
142
  {model.empty ? <p className="dmcp-status">{t('empty')}</p> : (
119
143
  <>
120
144
  <h3 className="dmcp-heading">{t('servers')}</h3>
145
+ <p className="dmcp-summary">{summaryText}</p>
146
+ <div className="dmcp-toolbar">
147
+ <input
148
+ type="search"
149
+ className="dmcp-tool-filter dmcp-server-filter"
150
+ value={serverQuery}
151
+ placeholder={t('filterServers')}
152
+ aria-label={t('filterServers')}
153
+ onChange={(event) => { setServerQuery(event.currentTarget.value) }}
154
+ />
155
+ <button
156
+ type="button"
157
+ className="dmcp-action"
158
+ onClick={() => {
159
+ setExpanded(Object.fromEntries(visibleServers.map(row => [row.view.serverName, true] as const)))
160
+ }}
161
+ >
162
+ {t('expandAll')}
163
+ </button>
164
+ <button type="button" className="dmcp-action" onClick={() => { setExpanded({}) }}>{t('collapseAll')}</button>
165
+ </div>
166
+ {visibleServers.length === 0 ? <p className="dmcp-status">{t('noMatch')}</p> : null}
121
167
  <ul className="dmcp-cards">
122
- {model.servers.map((row) => {
123
- const open = expanded === row.view.serverName
168
+ {visibleServers.map((row) => {
169
+ const open = expanded[row.view.serverName] === true
124
170
  const detailId = `${listId}-${encodeURIComponent(row.view.serverName)}`
125
171
  const toolQuery = toolQueries[row.view.serverName] ?? ''
126
- const probeRunning = model.probes.some(
172
+ const rowProbeRunning = model.probes.some(
127
173
  probe => probe.view.status === 'running' && probe.view.serverName === row.view.serverName,
128
174
  )
129
175
  return (
@@ -133,7 +179,14 @@ export function McpPanelTab({ status, probe, t }: McpPanelTabProps): ReactNode {
133
179
  className="dmcp-card-content"
134
180
  aria-expanded={open}
135
181
  aria-controls={detailId}
136
- onClick={() => { setExpanded(current => current === row.view.serverName ? null : row.view.serverName) }}
182
+ onClick={() => {
183
+ setExpanded((current) => {
184
+ const next = { ...current }
185
+ if (next[row.view.serverName] === true) delete next[row.view.serverName]
186
+ else next[row.view.serverName] = true
187
+ return next
188
+ })
189
+ }}
137
190
  >
138
191
  <strong className="dmcp-card-title">{row.view.serverName}</strong>
139
192
  <span className="dmcp-card-trailing">
@@ -171,7 +224,7 @@ export function McpPanelTab({ status, probe, t }: McpPanelTabProps): ReactNode {
171
224
  <button
172
225
  type="button"
173
226
  className="dmcp-probe-now"
174
- disabled={probeRunning}
227
+ disabled={rowProbeRunning}
175
228
  onClick={() => {
176
229
  setProbeError(null)
177
230
  void Promise.resolve().then(() => probe(row.view.serverName)).then(
@@ -180,7 +233,7 @@ export function McpPanelTab({ status, probe, t }: McpPanelTabProps): ReactNode {
180
233
  )
181
234
  }}
182
235
  >
183
- {probeRunning ? t('probeRunning') : t('probeNow')}
236
+ {rowProbeRunning ? t('probeRunning') : t('probeNow')}
184
237
  </button>
185
238
  ) : null}
186
239
  {row.view.tools.length === 0 ? (
@@ -246,10 +299,11 @@ export function McpPanelTab({ status, probe, t }: McpPanelTabProps): ReactNode {
246
299
  )
247
300
  }
248
301
 
249
- /** One tone-colored badge chip. */
302
+ /** One tone-colored badge chip. The label is visible text, so no `role="img"`
303
+ * or `aria-label`: screen readers announce it exactly once. */
250
304
  function Badge({ tone, label }: { readonly tone: BadgeTone; readonly label: string }): ReactNode {
251
305
  return (
252
- <span className="dmcp-badge" data-tone={tone} role="img" aria-label={label} title={label}>
306
+ <span className="dmcp-badge" data-tone={tone} title={label}>
253
307
  <span className="dmcp-dot" aria-hidden="true" />
254
308
  {label}
255
309
  </span>
@@ -8,6 +8,11 @@ export const zh = {
8
8
  retry: '重试',
9
9
  empty: '此 profile 未配置官方 MCP 服务器(@deepseek-ai/dsh-mcp-client 行)。',
10
10
  servers: '服务器',
11
+ summary: '服务器 {total} 台:已连接 {connected},错误 {errored}',
12
+ filterServers: '过滤服务器',
13
+ noMatch: '没有匹配的服务器。',
14
+ expandAll: '全部展开',
15
+ collapseAll: '全部折叠',
11
16
  tools: '工具',
12
17
  noTools: '未注册工具',
13
18
  status: '状态',
@@ -37,6 +42,7 @@ export const zh = {
37
42
  probeFailed: '失败',
38
43
  probeKilled: '已取消',
39
44
  probeStopping: '取消中',
45
+ probeUnknown: '未知',
40
46
  probeNow: '探测',
41
47
  probeReachable: '可达',
42
48
  probeUnreachable: '不可达',
@@ -55,6 +61,11 @@ export const en = {
55
61
  retry: 'Retry',
56
62
  empty: 'This profile configures no official MCP servers (@deepseek-ai/dsh-mcp-client rows).',
57
63
  servers: 'Servers',
64
+ summary: '{total} servers: {connected} connected, {errored} with errors',
65
+ filterServers: 'Filter servers',
66
+ noMatch: 'No servers match the filter.',
67
+ expandAll: 'Expand all',
68
+ collapseAll: 'Collapse all',
58
69
  tools: 'Tools',
59
70
  noTools: 'No tools registered',
60
71
  status: 'Status',
@@ -84,6 +95,7 @@ export const en = {
84
95
  probeFailed: 'Failed',
85
96
  probeKilled: 'Killed',
86
97
  probeStopping: 'Stopping',
98
+ probeUnknown: 'Unknown',
87
99
  probeNow: 'Probe',
88
100
  probeReachable: 'Reachable',
89
101
  probeUnreachable: 'Unreachable',
@@ -38,7 +38,17 @@ export interface PresentedProbeRow {
38
38
  /** Badge tone for the probe state. */
39
39
  readonly tone: BadgeTone
40
40
  /** Badge code for the probe state. */
41
- readonly badge: 'running' | 'completed' | 'failed' | 'killed' | 'stopping'
41
+ readonly badge: 'running' | 'completed' | 'failed' | 'killed' | 'stopping' | 'unknown'
42
+ }
43
+
44
+ /** Counts for the panel summary line. */
45
+ export interface PanelSummary {
46
+ /** Servers shown in the tab. */
47
+ readonly total: number
48
+ /** Servers whose connection badge reads `connected`. */
49
+ readonly connected: number
50
+ /** Servers whose connection badge reads `failed` or `exhausted`. */
51
+ readonly errored: number
42
52
  }
43
53
 
44
54
  /** The complete render-ready tab model. */
@@ -89,6 +99,8 @@ export function probeBadge(status: McpProbeView['status']): { badge: PresentedPr
89
99
  case 'stopping': return { badge: 'stopping', tone: 'warn' }
90
100
  case 'failed': return { badge: 'failed', tone: 'error' }
91
101
  case 'killed': return { badge: 'killed', tone: 'muted' }
102
+ // Registry states outside the panel vocabulary: never throw on render.
103
+ default: return { badge: 'unknown', tone: 'muted' }
92
104
  }
93
105
  }
94
106
 
@@ -125,3 +137,39 @@ export function presentMcpPanel(snapshot: McpPanelSnapshot, now = Date.now()): P
125
137
  refreshIntervalMs: snapshot.refreshIntervalMs,
126
138
  }
127
139
  }
140
+
141
+ /**
142
+ * Count the summary facts for the tab header line. Counting is derived from
143
+ * the same badge codes the cards show, so the line can never disagree with
144
+ * the rows beneath it.
145
+ *
146
+ * @param servers - the presented server rows (typically the filtered list).
147
+ * @returns the summary counts.
148
+ */
149
+ export function summarizePanel(servers: readonly PresentedServerRow[]): PanelSummary {
150
+ let connected = 0
151
+ let errored = 0
152
+ for (const row of servers) {
153
+ if (row.badge === 'connected') connected += 1
154
+ else if (row.badge === 'failed' || row.badge === 'exhausted') errored += 1
155
+ }
156
+ return { total: servers.length, connected, errored }
157
+ }
158
+
159
+ /**
160
+ * Filter server rows by a case-insensitive substring match against the
161
+ * server name or its display target. An empty query returns the rows
162
+ * unchanged (stable identity, no copies).
163
+ *
164
+ * @param servers - the presented server rows.
165
+ * @param query - the raw filter text.
166
+ * @returns the matching rows.
167
+ */
168
+ export function filterServers(servers: readonly PresentedServerRow[], query: string): readonly PresentedServerRow[] {
169
+ const needle = query.trim().toLocaleLowerCase()
170
+ if (needle === '') return servers
171
+ return servers.filter(row =>
172
+ row.view.serverName.toLocaleLowerCase().includes(needle)
173
+ || row.view.target.toLocaleLowerCase().includes(needle),
174
+ )
175
+ }
@@ -41,6 +41,11 @@ const PANEL_CSS = `
41
41
  font: inherit;
42
42
  cursor: pointer;
43
43
  }
44
+ [data-dsh-mcp-panel] .dmcp-failure-detail {
45
+ margin: 0;
46
+ color: var(--dsw-alias-label-secondary);
47
+ overflow-wrap: anywhere;
48
+ }
44
49
  [data-dsh-mcp-panel] .dmcp-panel {
45
50
  display: flex;
46
51
  flex-direction: column;
@@ -52,6 +57,33 @@ const PANEL_CSS = `
52
57
  font-size: 1em;
53
58
  color: var(--dsw-alias-label-primary);
54
59
  }
60
+ [data-dsh-mcp-panel] .dmcp-summary {
61
+ margin: 0;
62
+ color: var(--dsw-alias-label-secondary);
63
+ }
64
+ [data-dsh-mcp-panel] .dmcp-toolbar {
65
+ display: flex;
66
+ align-items: center;
67
+ gap: 8px;
68
+ flex-wrap: wrap;
69
+ }
70
+ [data-dsh-mcp-panel] .dmcp-server-filter {
71
+ flex: 1 1 180px;
72
+ min-width: 0;
73
+ }
74
+ [data-dsh-mcp-panel] .dmcp-action {
75
+ font: inherit;
76
+ cursor: pointer;
77
+ color: var(--dsw-alias-label-primary);
78
+ background: var(--dsw-alias-bg-layer-1);
79
+ border: 1px solid var(--dsw-alias-border-l3);
80
+ border-radius: 4px;
81
+ padding: 4px 10px;
82
+ }
83
+ [data-dsh-mcp-panel] .dmcp-action:focus-visible {
84
+ outline: 2px solid var(--dsw-alias-brand-primary);
85
+ outline-offset: -2px;
86
+ }
55
87
  [data-dsh-mcp-panel] .dmcp-cards {
56
88
  list-style: none;
57
89
  margin: 0;
package/src/command.ts CHANGED
@@ -34,6 +34,8 @@ export type CommandLanguage = 'en' | 'zh' | 'es' | 'pt' | 'hi'
34
34
  export interface CommandMessages {
35
35
  enabled: string
36
36
  disabled: string
37
+ /** State marker for leftover `mcp__` namespaces with no configured row. */
38
+ unconfigured: string
37
39
  status: string
38
40
  reconnects: string
39
41
  lastError: string
@@ -50,7 +52,11 @@ export interface CommandMessages {
50
52
  patchIntro: (action: string, server: string, entryId: string, patchFile: string | null) => string
51
53
  patchNoRuntimeToggle: string
52
54
  patchReloadPath: string
55
+ /** Rejection for disable/enable on an unconfigured leftover namespace. */
56
+ noPatchForLeftover: (server: string) => string
53
57
  usage: string
58
+ /** Command input hint shown in the command UI. */
59
+ hint: string
54
60
  probeStarted: (server: string, jobId: string) => string
55
61
  unknownServer: (server: string, known: string) => string
56
62
  }
@@ -59,6 +65,7 @@ export interface CommandMessages {
59
65
  export const EN_MESSAGES: CommandMessages = {
60
66
  enabled: 'enabled',
61
67
  disabled: 'disabled',
68
+ unconfigured: 'unconfigured',
62
69
  status: 'status',
63
70
  reconnects: 'reconnects',
64
71
  lastError: 'last error',
@@ -76,7 +83,10 @@ export const EN_MESSAGES: CommandMessages = {
76
83
  `To ${action} "${server}" (entry ${entryId}), add this line to the profile patch layer${patchFile === null ? '' : ` (${patchFile})`}:`,
77
84
  patchNoRuntimeToggle: '@deepseek-ai/dsh-mcp-client has no runtime toggle; the Loader applies the patch on reload.',
78
85
  patchReloadPath: 'The web surface hot-reloads cordis.patch.yml edits; other surfaces restart. This command never edits your config.',
86
+ noPatchForLeftover: server =>
87
+ `"${server}" is not a configured server — its tools come from another plugin's mcp__ namespace, so there is no row to disable or enable.`,
79
88
  usage: 'Usage: /mcp | /mcp <server> | /mcp <server> tools | /mcp <server> disable | /mcp <server> enable | /mcp <server> probe',
89
+ hint: '[server] [tools|disable|enable|probe]',
80
90
  probeStarted: (server, jobId) =>
81
91
  `Probe started for "${server}" (background job ${jobId}). Read the result in the MCP panel: Settings → Plugins → MCP.`,
82
92
  unknownServer: (server, known) =>
@@ -87,6 +97,7 @@ export const EN_MESSAGES: CommandMessages = {
87
97
  export const ZH_MESSAGES: CommandMessages = {
88
98
  enabled: '已启用',
89
99
  disabled: '已停用',
100
+ unconfigured: '未配置',
90
101
  status: '状态',
91
102
  reconnects: '重连',
92
103
  lastError: '最近错误',
@@ -106,7 +117,10 @@ export const ZH_MESSAGES: CommandMessages = {
106
117
  },
107
118
  patchNoRuntimeToggle: '@deepseek-ai/dsh-mcp-client 没有运行时开关;Loader 在重载时应用该 patch。',
108
119
  patchReloadPath: 'web 面板会热重载 cordis.patch.yml 的修改;其他面板重启生效。本命令绝不修改你的配置。',
120
+ noPatchForLeftover: server =>
121
+ `"${server}" 不是已配置的服务器——其工具来自其他插件的 mcp__ 命名空间,没有可停用/启用的条目。`,
109
122
  usage: '用法:/mcp | /mcp <server> | /mcp <server> tools | /mcp <server> disable | /mcp <server> enable | /mcp <server> probe',
123
+ hint: '[server] [tools|disable|enable|probe]',
110
124
  probeStarted: (server, jobId) =>
111
125
  `已对 "${server}" 启动探测(后台任务 ${jobId})。结果仅面板可见:设置 → 插件 → MCP。`,
112
126
  unknownServer: (server, known) =>
@@ -117,6 +131,7 @@ export const ZH_MESSAGES: CommandMessages = {
117
131
  export const ES_MESSAGES: CommandMessages = {
118
132
  enabled: 'habilitado',
119
133
  disabled: 'deshabilitado',
134
+ unconfigured: 'sin configurar',
120
135
  status: 'estado',
121
136
  reconnects: 'reconexiones',
122
137
  lastError: 'último error',
@@ -136,7 +151,10 @@ export const ES_MESSAGES: CommandMessages = {
136
151
  },
137
152
  patchNoRuntimeToggle: '@deepseek-ai/dsh-mcp-client no tiene conmutador en tiempo de ejecución; el Loader aplica el parche al recargar.',
138
153
  patchReloadPath: 'La superficie web recarga en caliente los cambios de cordis.patch.yml; otras superficies se reinician. Este comando nunca edita tu configuración.',
154
+ noPatchForLeftover: server =>
155
+ `"${server}" no es un servidor configurado — sus herramientas provienen del espacio mcp__ de otro plugin, así que no hay fila que deshabilitar o habilitar.`,
139
156
  usage: 'Uso: /mcp | /mcp <server> | /mcp <server> tools | /mcp <server> disable | /mcp <server> enable | /mcp <server> probe',
157
+ hint: '[server] [tools|disable|enable|probe]',
140
158
  probeStarted: (server, jobId) =>
141
159
  `Sonda iniciada para "${server}" (tarea en segundo plano ${jobId}). Lee el resultado en el panel MCP: Ajustes → Plugins → MCP.`,
142
160
  unknownServer: (server, known) =>
@@ -147,6 +165,7 @@ export const ES_MESSAGES: CommandMessages = {
147
165
  export const PT_MESSAGES: CommandMessages = {
148
166
  enabled: 'habilitado',
149
167
  disabled: 'desabilitado',
168
+ unconfigured: 'não configurado',
150
169
  status: 'status',
151
170
  reconnects: 'reconexões',
152
171
  lastError: 'último erro',
@@ -166,7 +185,10 @@ export const PT_MESSAGES: CommandMessages = {
166
185
  },
167
186
  patchNoRuntimeToggle: '@deepseek-ai/dsh-mcp-client não tem alternância em tempo de execução; o Loader aplica o patch ao recarregar.',
168
187
  patchReloadPath: 'A superfície web recarrega em quente as edições de cordis.patch.yml; outras superfícies reiniciam. Este comando nunca edita sua configuração.',
188
+ noPatchForLeftover: server =>
189
+ `"${server}" não é um servidor configurado — suas ferramentas vêm do namespace mcp__ de outro plugin, então não há linha para desabilitar ou habilitar.`,
169
190
  usage: 'Uso: /mcp | /mcp <server> | /mcp <server> tools | /mcp <server> disable | /mcp <server> enable | /mcp <server> probe',
191
+ hint: '[server] [tools|disable|enable|probe]',
170
192
  probeStarted: (server, jobId) =>
171
193
  `Sonda iniciada para "${server}" (tarefa em segundo plano ${jobId}). Leia o resultado no painel MCP: Configurações → Plugins → MCP.`,
172
194
  unknownServer: (server, known) =>
@@ -177,6 +199,7 @@ export const PT_MESSAGES: CommandMessages = {
177
199
  export const HI_MESSAGES: CommandMessages = {
178
200
  enabled: 'सक्षम',
179
201
  disabled: 'अक्षम',
202
+ unconfigured: 'अकॉन्फ़िगर',
180
203
  status: 'स्थिति',
181
204
  reconnects: 'रीकनेक्ट',
182
205
  lastError: 'अंतिम त्रुटि',
@@ -196,7 +219,10 @@ export const HI_MESSAGES: CommandMessages = {
196
219
  },
197
220
  patchNoRuntimeToggle: '@deepseek-ai/dsh-mcp-client के पास रनटाइम टॉगल नहीं है; Loader रीलोड पर पैच लागू करता है।',
198
221
  patchReloadPath: 'वेब सतह cordis.patch.yml के बदलाव हॉट-रीलोड करती है; अन्य सतहें रीस्टार्ट करें। यह कमांड आपका कॉन्फ़िगरेशन कभी नहीं बदलती।',
222
+ noPatchForLeftover: server =>
223
+ `"${server}" कॉन्फ़िगर किया गया सर्वर नहीं है — इसके टूल दूसरे प्लगइन के mcp__ नेमस्पेस से आते हैं, इसलिए अक्षम/सक्षम करने के लिए कोई पंक्ति नहीं है।`,
199
224
  usage: 'उपयोग: /mcp | /mcp <server> | /mcp <server> tools | /mcp <server> disable | /mcp <server> enable | /mcp <server> probe',
225
+ hint: '[server] [tools|disable|enable|probe]',
200
226
  probeStarted: (server, jobId) =>
201
227
  `"${server}" के लिए प्रोब शुरू (बैकग्राउंड जॉब ${jobId})। परिणाम MCP पैनल में पढ़ें: सेटिंग्स → प्लगइन्स → MCP।`,
202
228
  unknownServer: (server, known) =>
@@ -236,7 +262,10 @@ function statusText(view: McpServerView): string {
236
262
  * @returns the single display line.
237
263
  */
238
264
  export function renderServer(view: McpServerView, messages: CommandMessages = EN_MESSAGES): string {
239
- const state = view.enabled ? messages.enabled : messages.disabled
265
+ // A leftover namespace (foreign mcp__ tools, no loader row) is unconfigured,
266
+ // not disabled — its config facts do not exist. Keep this in sync with the
267
+ // tab's `connectionBadge`.
268
+ const state = view.entryId === '' ? messages.unconfigured : view.enabled ? messages.enabled : messages.disabled
240
269
  const details = [
241
270
  `${messages.status}: ${statusText(view)}`,
242
271
  `${messages.reconnects}: ${reconnectText(view)}`,
@@ -352,7 +381,7 @@ export function mcpCommand(service: McpPanelService, language: CommandLanguage =
352
381
  return {
353
382
  name: 'mcp',
354
383
  description: 'Show MCP server status, tools, and enable/disable patch suggestions (read-only)',
355
- input: { hint: '[server] [tools|disable|enable|probe]' },
384
+ input: { hint: messages.hint },
356
385
  handler: ({ rawInput }) => {
357
386
  const parsed = parseMcpArgs(rawInput)
358
387
  if (parsed.kind === 'usage') return { kind: 'error', text: messages.usage }
@@ -368,8 +397,14 @@ export function mcpCommand(service: McpPanelService, language: CommandLanguage =
368
397
  }
369
398
  switch (parsed.action) {
370
399
  case 'tools': return { kind: 'success', text: renderTools(view, messages) }
371
- case 'disable': return { kind: 'success', text: renderPatchSuggestion(view, 'disable', snapshot.patchFile, messages) }
372
- case 'enable': return { kind: 'success', text: renderPatchSuggestion(view, 'enable', snapshot.patchFile, messages) }
400
+ case 'disable':
401
+ case 'enable': {
402
+ // Leftover namespaces have no loader row: a patch suggestion with an
403
+ // empty entry id would be malformed, so refuse instead of emitting
404
+ // `- set: { id: , … }`.
405
+ if (view.entryId === '') return { kind: 'error', text: messages.noPatchForLeftover(parsed.server) }
406
+ return { kind: 'success', text: renderPatchSuggestion(view, parsed.action, snapshot.patchFile, messages) }
407
+ }
373
408
  case 'probe': {
374
409
  // The service throws for stdio rows and for a missing job registry;
375
410
  // the command reports those as errors without touching any config.
package/src/probe.ts CHANGED
@@ -31,7 +31,7 @@ const INITIALIZE_PROTOCOL_VERSION = '2024-11-05'
31
31
  /** MCP clientInfo facts; protocol constants, not configuration. Exported so the
32
32
  * version-consistency tripwire (`tests/version.spec.ts`) can assert the
33
33
  * advertised version tracks the package version. */
34
- export const PROBE_CLIENT_INFO = { name: 'dsh-mcp-panel', version: '0.2.0' }
34
+ export const PROBE_CLIENT_INFO = { name: 'dsh-mcp-panel', version: '0.3.0' }
35
35
 
36
36
  /** One settled probe: outcome status plus a sanitized one-line detail. */
37
37
  export interface ProbeOutcome {