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.
- package/CHANGELOG.md +95 -0
- package/README.es.md +17 -5
- package/README.hi.md +17 -5
- package/README.md +17 -5
- package/README.pt.md +17 -5
- package/README.zh.md +17 -5
- package/THIRD_PARTY_NOTICES.md +11 -0
- package/docs/optimization-plan-v2.zh.md +325 -0
- package/docs/optimization-plan.zh.md +157 -0
- package/docs/research-notes.zh.md +114 -0
- package/docs/upstream-proposal.md +150 -0
- package/lib/client.js +283 -130
- package/lib/client.js.map +1 -1
- package/lib/index.js +87 -19
- package/lib/typert.host.js +2 -1
- package/lib/types/client/McpPanelTab.d.ts.map +1 -1
- package/lib/types/client/locales.d.ts +12 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/present.d.ts +29 -1
- package/lib/types/client/present.d.ts.map +1 -1
- package/lib/types/client/remote.d.ts +9 -9
- package/lib/types/command.d.ts +6 -0
- package/lib/types/command.d.ts.map +1 -1
- package/lib/types/probe.d.ts.map +1 -1
- package/lib/types/service.d.ts +11 -2
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/typert.host.d.ts +9 -9
- package/lib/types/wire.d.ts +21 -21
- package/lib/types/wire.d.ts.map +1 -1
- package/package.json +18 -8
- package/src/client/McpPanelTab.tsx +68 -14
- package/src/client/locales.ts +12 -0
- package/src/client/present.ts +49 -1
- package/src/client/styles.ts +32 -0
- package/src/command.ts +39 -4
- package/src/probe.ts +1 -1
- package/src/service.ts +62 -10
- package/src/wire.ts +3 -3
package/src/service.ts
CHANGED
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
import { groupMcpTools } from './grouping.ts'
|
|
34
34
|
import { probeEndpoint, probeJob, PROBE_KIND } from './probe.ts'
|
|
35
35
|
import { sanitizeText } from './sanitize.ts'
|
|
36
|
-
import type { McpServerStatus } from './upstream.ts'
|
|
36
|
+
import type { McpServerStatus, McpStatusPhase } from './upstream.ts'
|
|
37
37
|
import type { McpFiberPhase, McpPanelSnapshot, McpProbeView, ProbeStarted } from './wire.ts'
|
|
38
38
|
|
|
39
39
|
declare module '@deepseek-ai/cordis' {
|
|
@@ -62,6 +62,18 @@ const PROFILE_PATCH_FILENAME = 'cordis.patch.yml'
|
|
|
62
62
|
/** Label prefix written by the `mcp_probe` tool and the panel probe action. */
|
|
63
63
|
const PROBE_LABEL_PREFIX = 'mcp_probe '
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Supervisor phases this panel understands. Upstream payloads are
|
|
67
|
+
* unvalidated runtime data (any plugin can emit `mcp/status`): a payload
|
|
68
|
+
* whose required fields do not match the wire codec would otherwise be
|
|
69
|
+
* stored verbatim and later REJECT the whole `mcpPanel/status` snapshot on
|
|
70
|
+
* the strict Typert codec, taking down the panel over one bad event.
|
|
71
|
+
*/
|
|
72
|
+
const KNOWN_PHASES: ReadonlySet<string> = new Set(['connecting', 'connected', 'waiting', 'exhausted', 'disposed'])
|
|
73
|
+
|
|
74
|
+
/** Background-job statuses the panel displays; anything else reads `unknown`. */
|
|
75
|
+
const KNOWN_JOB_STATUSES: ReadonlySet<string> = new Set(['running', 'stopping', 'completed', 'killed', 'failed'])
|
|
76
|
+
|
|
65
77
|
/** Service-level runtime settings; the plugin passes its resolved config in. */
|
|
66
78
|
export interface McpPanelServiceConfig {
|
|
67
79
|
/** Per-probe timeout in milliseconds. */
|
|
@@ -93,6 +105,12 @@ export class McpPanelService extends TypertRemoteService {
|
|
|
93
105
|
private readonly statuses = new Map<string, McpServerStatus>()
|
|
94
106
|
/** Cumulative reconnect attempts observed per server namespace. */
|
|
95
107
|
private readonly reconnects = new Map<string, number>()
|
|
108
|
+
/**
|
|
109
|
+
* Highest `attempt` already counted per server, so re-observing the same
|
|
110
|
+
* payload (HMR remount, event + query seed of one transition) never double
|
|
111
|
+
* counts a reconnect.
|
|
112
|
+
*/
|
|
113
|
+
private readonly countedAttempts = new Map<string, number>()
|
|
96
114
|
/** Epoch ms of the latest upstream event receipt per server namespace. */
|
|
97
115
|
private readonly observedAt = new Map<string, number>()
|
|
98
116
|
/** Latest passive-probe reachability per server namespace. */
|
|
@@ -115,17 +133,45 @@ export class McpPanelService extends TypertRemoteService {
|
|
|
115
133
|
}
|
|
116
134
|
|
|
117
135
|
/**
|
|
118
|
-
* Record one upstream `mcp/status` payload (event or query-seed).
|
|
119
|
-
*
|
|
136
|
+
* Record one upstream `mcp/status` payload (event or query-seed). Payloads
|
|
137
|
+
* are validated before storage — they are unvalidated runtime data and a
|
|
138
|
+
* malformed one must never poison the strict wire codec downstream. A
|
|
139
|
+
* `connecting` payload with a strictly increasing attempt counts one
|
|
140
|
+
* reconnect; re-observing the same payload never double counts.
|
|
120
141
|
*
|
|
121
142
|
* @param payload - post-transition status facts.
|
|
122
143
|
*/
|
|
123
144
|
observe(payload: McpServerStatus): void {
|
|
124
|
-
if (typeof payload
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (
|
|
128
|
-
|
|
145
|
+
if (typeof payload !== 'object' || payload === null) return
|
|
146
|
+
const { serverName, phase, attempt, maxAttempts, toolCount } = payload
|
|
147
|
+
if (typeof serverName !== 'string' || serverName === '') return
|
|
148
|
+
if (typeof phase !== 'string' || !KNOWN_PHASES.has(phase)) return
|
|
149
|
+
if (!Number.isFinite(attempt) || !Number.isFinite(maxAttempts) || !Number.isFinite(toolCount)) return
|
|
150
|
+
// Normalize into the exact shape the wire codec accepts; optional fields
|
|
151
|
+
// drop when they are not codec-shaped instead of rejecting the snapshot.
|
|
152
|
+
const clean: McpServerStatus = {
|
|
153
|
+
serverName,
|
|
154
|
+
phase: phase as McpStatusPhase,
|
|
155
|
+
attempt: Math.floor(attempt),
|
|
156
|
+
maxAttempts: Math.floor(maxAttempts),
|
|
157
|
+
toolCount: Math.floor(toolCount),
|
|
158
|
+
...(Number.isFinite(payload.delayMs) ? { delayMs: Math.floor(payload.delayMs as number) } : {}),
|
|
159
|
+
...(typeof payload.error === 'string' ? { error: payload.error } : {}),
|
|
160
|
+
...(Number.isFinite(payload.connectedAt) ? { connectedAt: Math.floor(payload.connectedAt as number) } : {}),
|
|
161
|
+
}
|
|
162
|
+
this.statuses.set(serverName, clean)
|
|
163
|
+
this.observedAt.set(serverName, Date.now())
|
|
164
|
+
if (clean.phase === 'connecting' && clean.attempt > 0) {
|
|
165
|
+
const counted = this.countedAttempts.get(serverName) ?? 0
|
|
166
|
+
if (clean.attempt > counted) {
|
|
167
|
+
this.countedAttempts.set(serverName, clean.attempt)
|
|
168
|
+
this.reconnects.set(serverName, (this.reconnects.get(serverName) ?? 0) + 1)
|
|
169
|
+
}
|
|
170
|
+
} else if (clean.phase === 'connected' || clean.phase === 'disposed') {
|
|
171
|
+
// A recovery or teardown resets the counter so the next outage counts
|
|
172
|
+
// from attempt 1 again. `waiting`/`exhausted` keep it: those phases
|
|
173
|
+
// carry the ongoing outage's attempt and must not re-arm counting.
|
|
174
|
+
this.countedAttempts.set(serverName, 0)
|
|
129
175
|
}
|
|
130
176
|
}
|
|
131
177
|
|
|
@@ -230,7 +276,11 @@ export class McpPanelService extends TypertRemoteService {
|
|
|
230
276
|
for (const entry of this.ctx.loader.entries()) {
|
|
231
277
|
if (entry.options.name !== MCP_CLIENT_MODULE) continue
|
|
232
278
|
const config = entry.options.config
|
|
233
|
-
|
|
279
|
+
// `entry.options.id` — not `entry.id` — keeps the namespace fallback
|
|
280
|
+
// identical to the snapshot's (`entry:<options.id>`); `entry.id` carries
|
|
281
|
+
// enclosing group prefixes (e.g. `include:`), which would silently break
|
|
282
|
+
// probe targeting for rows nested in groups.
|
|
283
|
+
if (serverNameOf(config, `entry:${entry.options.id}`) !== serverName) continue
|
|
234
284
|
if (typeof config !== 'object' || config === null || Array.isArray(config)) return undefined
|
|
235
285
|
const row = config as Record<string, unknown>
|
|
236
286
|
if (row['transport'] !== 'streamable-http') return undefined
|
|
@@ -258,7 +308,9 @@ export class McpPanelService extends TypertRemoteService {
|
|
|
258
308
|
.map(job => ({
|
|
259
309
|
id: job.id,
|
|
260
310
|
serverName: job.label.startsWith(PROBE_LABEL_PREFIX) ? job.label.slice(PROBE_LABEL_PREFIX.length) : job.label,
|
|
261
|
-
|
|
311
|
+
// Job registries may grow lifecycle states; anything outside the
|
|
312
|
+
// panel's vocabulary reads `unknown` instead of failing the codec.
|
|
313
|
+
status: KNOWN_JOB_STATUSES.has(job.status) ? job.status as McpProbeView['status'] : 'unknown',
|
|
262
314
|
startedAt: job.startedAt,
|
|
263
315
|
finishedAt: job.finishedAt ?? null,
|
|
264
316
|
detail: job.detail === undefined ? null : sanitizeText(job.detail),
|
package/src/wire.ts
CHANGED
|
@@ -82,8 +82,8 @@ export interface McpProbeView {
|
|
|
82
82
|
id: string
|
|
83
83
|
/** Server the probe targeted. */
|
|
84
84
|
serverName: string
|
|
85
|
-
/** Job lifecycle state. */
|
|
86
|
-
status: 'running' | 'stopping' | 'completed' | 'killed' | 'failed'
|
|
85
|
+
/** Job lifecycle state (`unknown` for registry states outside this panel's vocabulary). */
|
|
86
|
+
status: 'running' | 'stopping' | 'completed' | 'killed' | 'failed' | 'unknown'
|
|
87
87
|
/** Epoch ms when the probe started. */
|
|
88
88
|
startedAt: number
|
|
89
89
|
/** Epoch ms when the probe settled; `null` while running. */
|
|
@@ -139,7 +139,7 @@ export const MCP_PANEL_SNAPSHOT_SCHEMA = z.object({
|
|
|
139
139
|
probes: z.array(z.object({
|
|
140
140
|
id: z.string(),
|
|
141
141
|
serverName: z.string(),
|
|
142
|
-
status: z.union([z.literal('running'), z.literal('stopping'), z.literal('completed'), z.literal('killed'), z.literal('failed')]),
|
|
142
|
+
status: z.union([z.literal('running'), z.literal('stopping'), z.literal('completed'), z.literal('killed'), z.literal('failed'), z.literal('unknown')]),
|
|
143
143
|
startedAt: z.number().int(),
|
|
144
144
|
finishedAt: z.number().int().nullable(),
|
|
145
145
|
detail: z.string().nullable(),
|