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.
@@ -0,0 +1,150 @@
1
+ # Upstream proposal: minimal connection-status observability for `@deepseek-ai/dsh-mcp-client`
2
+
3
+ > **副本说明**:本文档随本插件仓库分发(权威副本)。提案的最终落点目标是
4
+ > `deepseek-ai/deepseek-harness` 仓库的 `docs/upstream-proposal.md`;若二者不一致,
5
+ > 以本插件仓库版本为插件实现所依据的契约,PR 内容以提交到上游仓库的版本为准。
6
+
7
+ **Status:** proposal (not yet merged upstream). Implementation exists in the fork branch
8
+ `PerryLink/deepseek-harness:feat/mcp-client-status-observability-seam` (commit `e1611e9`,
9
+ rebase of `deepseek-ai/deepseek-harness:master`) with tests, bilingual docs, and an Agent
10
+ Note; a ready-to-open PR is described in the handoff at
11
+ [deepseek-harness Discussions #1300](https://github.com/deepseek-ai/deepseek-harness/discussions/1300)
12
+ (compare link: `deepseek-ai/deepseek-harness/compare/master...PerryLink:feat/mcp-client-status-observability-seam`).
13
+ The upstream repository currently does not accept external pull requests; whoever holds merge
14
+ access can open the PR from that branch. Target repository: `deepseek-ai/deepseek-harness`,
15
+ package `packages/mcp/mcp-client`.
16
+ **Author:** `dsh-mcp-panel` (runtime management panel for the official MCP client).
17
+ **Scope:** status events + a status query service only. No transport, OAuth, protocol, or reconnect-policy changes.
18
+
19
+ ## Motivation
20
+
21
+ The official MCP client keeps every connection fact in supervisor closure state
22
+ (`packages/mcp/mcp-client/src/connection.ts`): the live client generation,
23
+ `failedAttempts`, `connectedAt`, `firstAttemptError`, and the registered tool
24
+ disposers are private locals of `startConnection()`. The only observability is
25
+ the logger: "reconnecting (warn, with attempt count and delay)", "recovered
26
+ (info)", "final failure and disabled-loss (error)" (README "Behavior"). The
27
+ package's own invariant companion states the gap explicitly:
28
+
29
+ > "the bridge exposes no independent server-to-tool snapshot after an
30
+ > asynchronous resync" — `src/invariant.ts`
31
+
32
+ A read-only runtime management surface (a `/mcp` command, a web settings card)
33
+ therefore cannot show connection status, recent errors, or reconnect counts
34
+ without either reimplementing the client or guessing from the tool registry — and guessing from `ctx.tools` misreports a failed-but-tools-still-registered
35
+ server as healthy. This proposal adds the minimal seam that lets any consumer
36
+ observe the supervisor without touching its transport or reconnect logic.
37
+
38
+ ## Proposed surface
39
+
40
+ ### 1. Typed Cordis event `mcp/status` (emit)
41
+
42
+ One emission per supervisor state transition, payload:
43
+
44
+ ```ts
45
+ /** App-level connection-status payload emitted on every mcp-client state transition. */
46
+ export interface McpStatusPayload {
47
+ /** Stable local namespace from plugin config. */
48
+ serverName: string
49
+ /** Supervisor phase after the transition. */
50
+ phase: 'connecting' | 'connected' | 'waiting' | 'exhausted' | 'disposed'
51
+ /** Consecutive failed attempts in the current outage (0 while connected). */
52
+ attempt: number
53
+ /** Resolved reconnect budget (`reconnect.maxAttempts`). */
54
+ maxAttempts: number
55
+ /** Scheduled backoff delay while `waiting`. */
56
+ delayMs?: number
57
+ /** Raw error text of the failed attempt or re-sync; consumers sanitize before display. */
58
+ error?: string
59
+ /** Tools registered after the last successful sync (`disposers.size`). */
60
+ toolCount: number
61
+ /** Epoch ms of the last successful connect; absent while down. */
62
+ connectedAt?: number
63
+ }
64
+ ```
65
+
66
+ Emission sites (all inside `startConnection()`, current line numbers):
67
+
68
+ | Phase | Where | Notes |
69
+ |---|---|---|
70
+ | `connecting` | top of `connectGeneration()` (line ~237) | `attempt` = `failedAttempts` at entry; first attempt reports 0 |
71
+ | `connected` | after initial sync, `connectedAt` set (line ~303) | `attempt: 0`, `toolCount` = `disposers.size` |
72
+ | `connected` (re-sync failure) | notification re-sync catch (line ~267) | phase stays `connected` (last good list keeps serving), `error` set |
73
+ | `waiting` | `scheduleReconnect()` after the timer is armed (line ~218) | `attempt` after increment, `delayMs` included |
74
+ | `exhausted` | give-up branch (line ~213) | after `maxAttempts` consecutive failures |
75
+ | `disposed` | `dispose()` entry (line ~327) | terminal per plugin instance |
76
+
77
+ The event is process-app-level (no agent/session): connection state belongs to
78
+ the app, not to a conversation, so a session event would be the wrong carrier
79
+ and would pollute every session log with duplicated app state.
80
+
81
+ ### 2. Query service `mcpStatus`
82
+
83
+ ```ts
84
+ /** Current per-server status snapshot; the query face of `mcp/status`. */
85
+ export interface McpServerStatus extends McpStatusPayload {}
86
+
87
+ /**
88
+ * Per-app status registry. `report()` is the single writer: it stores the
89
+ * payload and emits the typed `mcp/status` event, so push consumers and
90
+ * late-joining query consumers observe the same truth.
91
+ */
92
+ export class McpStatusService extends Service {
93
+ constructor(ctx: Context) // super(ctx, 'mcpStatus')
94
+ report(payload: McpStatusPayload): void
95
+ list(): McpServerStatus[]
96
+ get(serverName: string): McpServerStatus | undefined
97
+ }
98
+ ```
99
+
100
+ One service per app root, created by the first live mcp-client instance and
101
+ shared by the rest — the same `WeakMap<Context, …>` singleton pattern the file
102
+ already uses for `activeServerNames` (`src/index.ts` lines ~45, 148). Each
103
+ instance's supervisor calls `report()` at the six sites above. `dispose()`
104
+ reports `disposed` before unregistering tools so `toolCount` is still accurate
105
+ in the terminal payload.
106
+
107
+ ### 3. Typing
108
+
109
+ - Event: `declare module '@deepseek-ai/cordis' { interface Events { 'mcp/status'(payload: McpStatusPayload): void } }` with `@mode emit` JSDoc.
110
+ - Service: `declare module '@deepseek-ai/cordis' { interface Context { mcpStatus: McpStatusService } }`.
111
+ - Both live in a new `src/status.ts`, re-exported from the package root; a new
112
+ `mcp-client-invariant` companion can assert `report` — tool-registry
113
+ generation if desired (optional, not required for this proposal).
114
+
115
+ ## Deliberate non-goals
116
+
117
+ - **No transport / OAuth / protocol changes** — the supervisor's reconnect
118
+ loop, transport factory, and tool bridge stay byte-for-byte; this only adds
119
+ notifications around them.
120
+ - **No sanitization in the event** — the payload is trusted same-process data;
121
+ `error` carries the real text. Display consumers redact before rendering
122
+ (reference implementation: `dsh-mcp-panel` `src/sanitize.ts`).
123
+ - **No Typert remote export from mcp-client itself** — which host services
124
+ reach the browser is an app-composition choice (gateway selection), not a
125
+ client-package concern. Panels compose their own remote service over this
126
+ seam, as `dsh-mcp-panel` does.
127
+ - **No per-session projection** — app-level runtime-varying state does not
128
+ belong in session logs.
129
+
130
+ ## PR contents (when implemented)
131
+
132
+ 1. `src/status.ts` — payload type, service, event declaration.
133
+ 2. `src/connection.ts` — six `report()` call sites (no behavior change).
134
+ 3. `src/index.ts` — mount the shared `McpStatusService` singleton; export the types.
135
+ 4. `README.md` — "Observability" section documenting the event and the service.
136
+ 5. Tests — `status.spec.ts` (report/list/get, singleton across two instances),
137
+ `reconnect.spec.ts` additions asserting the emitted phase sequence for a
138
+ crash loop (connecting → connected → waiting → … → exhausted) and for
139
+ `reconnect.enabled: false`.
140
+ 6. Agent Note per repository convention (non-trivial change).
141
+
142
+ ## Consumer behavior (dsh-mcp-panel, implemented against this proposal)
143
+
144
+ The panel subscribes `ctx.on('mcp/status', …)` and optionally queries
145
+ `ctx.get('mcpStatus')` on start (feature detection — the service is absent
146
+ until this PR lands). When neither produces data, the panel reports status as
147
+ `unknown` with `statusSource: 'derived'` (from loader entries + tool registry
148
+ only) instead of fabricating a connection state. That keeps the panel honest
149
+ both before and after this proposal lands.
150
+