@sunshaobei/dsh-mcp-manager 0.0.0-stage → 0.4.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.
Files changed (144) hide show
  1. package/LICENSE +1 -0
  2. package/README.en.md +476 -0
  3. package/README.md +379 -2
  4. package/cordis.patch.yml +9 -0
  5. package/lib/THIRD-PARTY-LICENSES +43 -0
  6. package/lib/client/core/api.d.ts +42 -0
  7. package/lib/client/core/constants.d.ts +58 -0
  8. package/lib/client/core/current-session.d.ts +39 -0
  9. package/lib/client/core/dom.d.ts +19 -0
  10. package/lib/client/core/i18n.d.ts +12 -0
  11. package/lib/client/core/session.d.ts +27 -0
  12. package/lib/client/core/state.d.ts +120 -0
  13. package/lib/client/float/float.d.ts +126 -0
  14. package/lib/client/float/panel.d.ts +29 -0
  15. package/lib/client/float/quick-add.d.ts +38 -0
  16. package/lib/client/float/servers.d.ts +54 -0
  17. package/lib/client/index.d.ts +26 -0
  18. package/lib/client/locales.d.ts +123 -0
  19. package/lib/client/settings/settings-card.d.ts +47 -0
  20. package/lib/client.js +2568 -0
  21. package/lib/index.d.ts +86 -0
  22. package/lib/index.js +10035 -0
  23. package/lib/server/api/deps.d.ts +36 -0
  24. package/lib/server/api/impl/health/type.d.ts +12 -0
  25. package/lib/server/api/impl/service/index.d.ts +28 -0
  26. package/lib/server/api/interface.d.ts +27 -0
  27. package/lib/server/api/routes-controllers.d.ts +61 -0
  28. package/lib/server/api/routes-helpers.d.ts +8 -0
  29. package/lib/server/api/routes.d.ts +58 -0
  30. package/lib/server/catalog/deps.d.ts +51 -0
  31. package/lib/server/catalog/impl/cache-view/index.d.ts +48 -0
  32. package/lib/server/catalog/impl/digest/index.d.ts +19 -0
  33. package/lib/server/catalog/impl/directory/index.d.ts +168 -0
  34. package/lib/server/catalog/impl/entries/index.d.ts +129 -0
  35. package/lib/server/catalog/impl/entries/type.d.ts +19 -0
  36. package/lib/server/catalog/impl/history/index.d.ts +36 -0
  37. package/lib/server/catalog/impl/injection/index.d.ts +45 -0
  38. package/lib/server/catalog/impl/search/type.d.ts +59 -0
  39. package/lib/server/catalog/impl/service/index.d.ts +25 -0
  40. package/lib/server/catalog/interface.d.ts +45 -0
  41. package/lib/server/catalog/search.d.ts +108 -0
  42. package/lib/server/config/config-schema.d.ts +85 -0
  43. package/lib/server/config/impl/env/index.d.ts +24 -0
  44. package/lib/server/config/impl/env/policy.d.ts +54 -0
  45. package/lib/server/config/impl/model/type.d.ts +39 -0
  46. package/lib/server/config/impl/ui/type.d.ts +20 -0
  47. package/lib/server/config/import.d.ts +20 -0
  48. package/lib/server/config/interface.d.ts +15 -0
  49. package/lib/server/config/normalize.d.ts +24 -0
  50. package/lib/server/config/sources-sync.d.ts +32 -0
  51. package/lib/server/config/sources.d.ts +50 -0
  52. package/lib/server/connection/interface.d.ts +86 -0
  53. package/lib/server/connection/orchestrator/deps.d.ts +93 -0
  54. package/lib/server/connection/orchestrator/impl/service/index.d.ts +27 -0
  55. package/lib/server/connection/orchestrator/interface.d.ts +25 -0
  56. package/lib/server/connection/orchestrator/manager.d.ts +484 -0
  57. package/lib/server/connection/orchestrator/tool-names.d.ts +14 -0
  58. package/lib/server/connection/runtime/deps.d.ts +141 -0
  59. package/lib/server/connection/runtime/impl/middleware/type.d.ts +45 -0
  60. package/lib/server/connection/runtime/impl/service/index.d.ts +28 -0
  61. package/lib/server/connection/runtime/interface.d.ts +29 -0
  62. package/lib/server/connection/runtime/limits.d.ts +19 -0
  63. package/lib/server/connection/runtime/middleware.d.ts +194 -0
  64. package/lib/server/erasure/impl/guard/index.d.ts +35 -0
  65. package/lib/server/erasure/interface.d.ts +31 -0
  66. package/lib/server/inject/deps.d.ts +54 -0
  67. package/lib/server/inject/impl/call-timeout/index.d.ts +1 -0
  68. package/lib/server/inject/impl/image-admission/index.d.ts +81 -0
  69. package/lib/server/inject/impl/service/index.d.ts +25 -0
  70. package/lib/server/inject/interface.d.ts +27 -0
  71. package/lib/server/inject/middleware-register.d.ts +114 -0
  72. package/lib/server/pipeline/deps.d.ts +27 -0
  73. package/lib/server/pipeline/impl/args/index.d.ts +26 -0
  74. package/lib/server/pipeline/impl/authorize/index.d.ts +43 -0
  75. package/lib/server/pipeline/impl/msg/index.d.ts +8 -0
  76. package/lib/server/pipeline/impl/project/index.d.ts +43 -0
  77. package/lib/server/pipeline/impl/redact/index.d.ts +13 -0
  78. package/lib/server/pipeline/impl/service/index.d.ts +25 -0
  79. package/lib/server/pipeline/impl/timeout/index.d.ts +8 -0
  80. package/lib/server/pipeline/interface.d.ts +32 -0
  81. package/lib/server/servers/dispatch/deps.d.ts +100 -0
  82. package/lib/server/servers/dispatch/impl/call/index.d.ts +49 -0
  83. package/lib/server/servers/dispatch/impl/redact/index.d.ts +13 -0
  84. package/lib/server/servers/dispatch/interface.d.ts +18 -0
  85. package/lib/server/servers/lifecycle/deps.d.ts +62 -0
  86. package/lib/server/servers/lifecycle/impl/ledger/index.d.ts +52 -0
  87. package/lib/server/servers/lifecycle/impl/logs/index.d.ts +48 -0
  88. package/lib/server/servers/lifecycle/impl/mount/index.d.ts +83 -0
  89. package/lib/server/servers/lifecycle/impl/service/index.d.ts +26 -0
  90. package/lib/server/servers/lifecycle/impl/state/index.d.ts +35 -0
  91. package/lib/server/servers/lifecycle/impl/timeout/index.d.ts +39 -0
  92. package/lib/server/servers/lifecycle/interface.d.ts +56 -0
  93. package/lib/server/shared/compose.d.ts +38 -0
  94. package/lib/server/shared/constants.d.ts +62 -0
  95. package/lib/server/shared/file-io.d.ts +8 -0
  96. package/lib/server/shared/host-faces.d.ts +206 -0
  97. package/lib/server/shared/interface.d.ts +14 -0
  98. package/lib/server/shared/paths.d.ts +66 -0
  99. package/lib/server/shared/tool-names.d.ts +11 -0
  100. package/lib/server/stats/impl/collector.d.ts +68 -0
  101. package/lib/server/stats/impl/type.d.ts +41 -0
  102. package/lib/server/stats/interface.d.ts +13 -0
  103. package/lib/server/store/impl/middleware-state.d.ts +45 -0
  104. package/lib/server/store/impl/store.d.ts +43 -0
  105. package/lib/server/store/interface.d.ts +9 -0
  106. package/lib/server/upgrade/deps.d.ts +25 -0
  107. package/lib/server/upgrade/impl/chain/index.d.ts +2 -0
  108. package/lib/server/upgrade/impl/service/index.d.ts +9 -0
  109. package/lib/server/upgrade/impl/steps/index.d.ts +19 -0
  110. package/lib/server/upgrade/impl/steps/project-layout.d.ts +24 -0
  111. package/lib/server/upgrade/impl/steps/storage-layout.d.ts +17 -0
  112. package/lib/server/upgrade/impl/version/index.d.ts +7 -0
  113. package/lib/server/upgrade/interface.d.ts +21 -0
  114. package/lib/server/visibility/impl/mask/index.d.ts +7 -0
  115. package/lib/server/visibility/interface.d.ts +46 -0
  116. package/lib/server/workspace/impl/full-name/index.d.ts +24 -0
  117. package/lib/server/workspace/impl/root-resolution/index.d.ts +18 -0
  118. package/lib/server/workspace/impl/scope/index.d.ts +14 -0
  119. package/lib/server/workspace/impl/server-id/index.d.ts +17 -0
  120. package/lib/server/workspace/interface.d.ts +22 -0
  121. package/lib/shared/constants.d.ts +22 -0
  122. package/lib/shared/dto.d.ts +62 -0
  123. package/lib/shared/frames.d.ts +19 -0
  124. package/lib/shared/interface.d.ts +22 -0
  125. package/lib/shared/placement-math.d.ts +81 -0
  126. package/lib/shared/routes.d.ts +43 -0
  127. package/lib/shared/service.d.ts +85 -0
  128. package/lib/shared/status.d.ts +23 -0
  129. package/lib/vendor/shared/README.md +147 -0
  130. package/lib/vendor/shared/client/ensure-style.d.ts +23 -0
  131. package/lib/vendor/shared/client/i18n.d.ts +30 -0
  132. package/lib/vendor/shared/client/ui/index.d.ts +214 -0
  133. package/lib/vendor/shared/dsh-home.d.ts +26 -0
  134. package/lib/vendor/shared/host-utils.d.ts +82 -0
  135. package/lib/vendor/shared/loopback.d.ts +12 -0
  136. package/lib/vendor/shared/package.json +12 -0
  137. package/lib/vendor/shared/paths.d.ts +35 -0
  138. package/lib/vendor/shared/settings-namespace.d.ts +82 -0
  139. package/lib/vendor/shared/sse-hub.d.ts +75 -0
  140. package/lib/vendor/shared/upgrade-chain.d.ts +140 -0
  141. package/lib/vendor/shared/upgrade-tick.d.ts +17 -0
  142. package/locale/en.json +6 -0
  143. package/locale/zh.json +6 -0
  144. package/package.json +171 -4
package/LICENSE ADDED
@@ -0,0 +1 @@
1
+ MIT License (inherited from wingsky-1/dsh-plugin-hub)
package/README.en.md ADDED
@@ -0,0 +1,476 @@
1
+ # dsh-mcp-manager
2
+ [![npm](https://img.shields.io/npm/v/dsh-mcp-manager)](https://www.npmjs.com/package/dsh-mcp-manager)
3
+ [![GitHub Releases](https://img.shields.io/github/v/release/wingsky-1/dsh-plugin-hub)](https://github.com/wingsky-1/dsh-plugin-hub/releases)
4
+
5
+ A **MCP server management plugin** for DSH (DeepSeek Harness): a floating window in the
6
+ top-right of the session UI + a tiered panel + quick onboarding (manual form + paste
7
+ `mcpServers` JSON import, **no servers preconfigured**). Connection and protocol come from
8
+ the official `@deepseek-ai/dsh-mcp-client` (mounted by name through the host cordis loader,
9
+ resolved to the copy inside the DSH installation); this plugin keeps only the configuration
10
+ surface and the model-visible surface. The official client is not redistributed with this
11
+ package, and nothing extra needs to be installed.
12
+
13
+ The model surface carries exactly four atomic tools (`ws_mcp_list` / `ws_mcp_detail` /
14
+ `ws_mcp_search` / `ws_mcp_call`): project-level, global and runtime-injected
15
+ wrapped-definition servers **all go through the middleware** (two-level discovery:
16
+ `ws_mcp_list` for a full inventory → `ws_mcp_detail` to pull the complete schema on
17
+ demand), falling back to the virtual global root `@global` when cwd has no project. The
18
+ host still registers tools as `mcp__<id>__<tool>` (`id` is an opaque short string
19
+ allocated per (workspace, server) for one plugin assembly and cannot be derived from the
20
+ server name), but those names are **removed from the model's tool list** — never call them
21
+ directly.
22
+ The server lists across both config tiers hot-reload without a restart
23
+ (add/remove/toggle/edit).
24
+ (The `middleware` / `middlewarePolicy` config keys were removed in #767 batch 2: writing
25
+ them has no effect, and this plugin never rewrites your config file.)
26
+
27
+ > **A historical session will not open after upgrading DSH to 0.1.5+**
28
+ > (`unclassified message source`)? See
29
+ > [Troubleshooting: a historical session will not open after upgrading (#723)](#723-repair),
30
+ > one command recovers it.
31
+
32
+ ## Quick install
33
+
34
+ ```sh
35
+ dsh plugin --profile web add dsh-mcp-manager
36
+ ```
37
+
38
+ > After install / uninstall / update, **restart `dsh web` once** (bundle layers are only composed at startup) for changes to take effect.
39
+
40
+ ## Core advantages
41
+
42
+ - **Context cost under control**: all MCP goes through the middleware — the model surface
43
+ carries only `ws_mcp_list` / `ws_mcp_detail` / `ws_mcp_search` / `ws_mcp_call`, so no
44
+ matter how many servers or tools you connect the system prompt never balloons
45
+ (two-level discovery: `ws_mcp_list` for a full inventory → `ws_mcp_detail` to pull the
46
+ complete schema on demand)
47
+ - **Per-working-directory maintenance**: project-level config `<project root>/.dsh/dsh-mcp-manager/mcp.json`
48
+ travels with the repo and can be committed to git for team sharing; global config
49
+ `<DSH_HOME>/dsh-mcp-manager/mcp.json` stays always connected; switching sessions auto-loads the
50
+ MCP set of the current directory
51
+ - **Workspace isolation**: the middleware routes by the session's cwd to the matching
52
+ connection pool, with server-full-name consistency checks against cross-workspace
53
+ crosstalk; same-named servers in different directories never clash
54
+ - **Secure by default**: configs store only `${ENV}` references, never key material
55
+ (0600 permissions + atomic writes); stdio subprocess environments are sanitized so
56
+ host credential-shaped variables never leak through; directory summaries and error
57
+ paths go through a redactor
58
+ - **Low-maintenance operations**: tiered status display (running / connecting / failed…);
59
+ bounded exponential-backoff auto-reconnect on disconnects; a per-server timeout override
60
+ (`toolCallTimeoutMs`), while a middleware call settles at the largest in-use server timeout
61
+ plus a 25s internal tail (15s + 25s = 40s by default)
62
+
63
+ ## Installation
64
+
65
+ Prerequisite: DeepSeek Harness installed and `dsh web` running normally (for running dsh
66
+ without a global install, see "Without a global dsh install" below).
67
+
68
+ ### Install plugins (add)
69
+
70
+ ```sh
71
+ dsh plugin --profile web add dsh-mcp-manager
72
+ ```
73
+
74
+ ### Uninstall plugins (remove)
75
+
76
+ ```sh
77
+ dsh plugin --profile web remove dsh-mcp-manager
78
+ ```
79
+
80
+ ### Update plugins (update)
81
+
82
+ ```sh
83
+ dsh plugin --profile web update dsh-mcp-manager
84
+ ```
85
+
86
+ > After install / uninstall / update, **restart `dsh web` once** (bundle layers are only
87
+ > composed at startup) for changes to take effect.
88
+
89
+ ### Pin a version (@version)
90
+
91
+ Omitting `@version` installs the default latest (recommended). Only when the registry has not synced the latest yet, or the latest has issues in your environment, append `@version` to the package name:
92
+
93
+ ```sh
94
+ dsh plugin --profile web add dsh-mcp-manager@<version>
95
+ ```
96
+
97
+ ### Without a global dsh install
98
+
99
+ If there is no global `dsh` command on the machine, use `npx` to run it on the fly (`dsh plugin`
100
+ calls `pnpm` under the hood, so `pnpm` and `Node.js` must still be installed locally):
101
+
102
+ ```sh
103
+ npx @deepseek-ai/dsh plugin --profile web add dsh-mcp-manager
104
+ npx @deepseek-ai/dsh plugin --profile web remove dsh-mcp-manager
105
+ npx @deepseek-ai/dsh plugin --profile web update dsh-mcp-manager
106
+ ```
107
+
108
+ ## Capabilities
109
+
110
+ | Capability | Description |
111
+ | --- | --- |
112
+ | Top-right floating window | Status dot + count summary (`MCP 2/3`); click to expand the dropdown panel; auto-refreshes on session switch |
113
+ | Project-level MCP | Servers are split into "project-level / global" tiers: project-level stored in `<project root>/.dsh/dsh-mcp-manager/mcp.json` (travels with the project, can be committed to git); global stored in `<DSH_HOME>/dsh-mcp-manager/mcp.json` for persistent connection |
114
+ | Tiered display | Running / Connecting / Reconnecting / Disconnected / Disabled / Failed; each server shows transport, endpoint, and tool count |
115
+ | Server management | CRUD (project-level/global optional), connect / disconnect / reconnect; versioned JSON config, atomic write |
116
+ | Two transports | stdio (local subprocess, env supports `${ENV}` references) and streamable-http (remote, header supports `${ENV}` references, auto-echoes `Mcp-Session-Id`) |
117
+ | JSON import | Paste `mcpServers` JSON text to import (JSON format only; does not scan any application config files) |
118
+ | Model tools | Every server (project-level / global / runtime-injected) goes through the same four atomic middleware tools: `ws_mcp_list` / `ws_mcp_detail` / `ws_mcp_search` / `ws_mcp_call`, so workspaces never clash; the host registration name `mcp__<id>__<tool>` (`id` allocated at random per (workspace, server), stable within one assembly, opaque; names still obey the 64-char / `[A-Za-z0-9_-]` / hashed-suffix-on-conflict rules) is internal only — it is removed from the model's tool list and must never be called directly |
119
+ | Workspace isolation | The middleware routes by the calling session's cwd to the matching workspace connection pool; server full-name consistency checks (`@<root>/<server>`) prevent cross-workspace crosstalk |
120
+ | Reconnection | Exponential backoff (starts at 500ms, caps at 30s, gives up after 10 attempts and deregisters the tools) |
121
+ | Result size | This package performs **no result truncation**; tool results are returned as-is. The catalog boxing limits (4KB per tool / 256KB total) bound the **catalog**, not execution results — enforce a limit on the MCP server side or via the host compaction policy |
122
+ | Timeout fallback | Direct-connect tool call timeout defaults to 60s → 15s (overridable per server via `toolCallTimeoutMs`); a middleware `ws_mcp_call` settles at the largest in-use server timeout plus a 25s internal tail, **not a fixed value** |
123
+ | Push self-healing | Triple safeguard on the SSE channel: server sends a data ping heartbeat every 30s; client reconnects (closing the stale EventSource first) after 60s of frame silence (watchdog); when the page becomes visible again the client rebuilds SSE and POSTs resume for a non-forced availability recovery (carrying the current session cwd, the server pins the root for that call; old body-less requests fall back to the existing binding) — half-open connections silently severed by mobile OS backgrounding heal on their own instead of piling up as zombies. Auto recovery reuses the official management mechanism's existing state projection and in-flight dedup and never force-rebuilds healthy/connecting/reconnecting connections; it does **not** claim to detect real health (a healthy connection with tool registrations is indistinguishable from a half-open one in real time) — half-open connections still recover through explicit user reconnect |
124
+
125
+ ## Configuration (floating window position)
126
+
127
+ Configuration page: **Plugin Manager → dsh-mcp-manager → Configure**. The floating button
128
+ (MCP pill) position and offsets (`position` / `offset`) are saved there through the plugin's
129
+ own `Config`; no config-file editing is needed.
130
+
131
+ > Non-loopback LAN origins (for example `192.168.*:3081`) intentionally do not expose a persistent settings surface, so the Configure control is hidden by DSH's security policy. Manage settings from `127.0.0.1:3080` / `127.0.0.1:3081` or an SSH loopback tunnel. Enable dsh-lan-proxy `ownsHostCompat` only on a trusted LAN and only when you accept the shared control-plane risk.
132
+
133
+ | Key | Allowed values | Default |
134
+ | --- | --- | --- |
135
+ | `position` | `top-right` (top-right, default) / `top-left` (top-left) / `bottom-right` (bottom-right) / `bottom-left` (bottom-left) | `top-right` |
136
+ | `offset.x` | Non-negative integer (horizontal offset, px) | `8` |
137
+ | `offset.y` | Non-negative integer (vertical offset, px) | `8` |
138
+ | `offset.blankY` | Non-negative integer (blank-session vertical offset, px) | `40` |
139
+ | `zIndexBase` | Integer, clamped to 1-9000 (floating window z-index base; **the pill and the main panel opened on click both use this same config value**; the modal manager panel is unaffected) | `10` |
140
+
141
+ #### Settings namespace
142
+
143
+ The DSH importer consumes the same-id `dsh-mcp-manager` section directly; this plugin does not run a second namespace migration. `settings.yaml.imported` is an audit copy of the consumed document and must not be copied wholesale.
144
+
145
+ When `position = bottom-right` or `bottom-left`, the dropdown panel expands **above the
146
+ pill** (bottom anchor, popping upward), does not overflow the viewport, and content stays
147
+ fully visible and clickable; at top anchors it expands downward (historical behavior,
148
+ unchanged default).
149
+
150
+ **Mobile / tablet adaptation** (issue #128): the breakpoint is decided from the
151
+ conversation container's viewport width rather than a window media query — on narrow
152
+ screens (<=480px, portrait phones / very narrow splits) the panel goes near full-width,
153
+ server cards reflow, and action buttons get touch targets of about 44px; the tablet tier
154
+ (<=834px) transitions; desktop is unchanged. Final floating coordinates are clamped to
155
+ the viewport in JS (safe-area semantics: the host has no `viewport-fit=cover`, so
156
+ `env(safe-area-inset-*)` is always 0 and this degrades naturally to a plain clamp);
157
+ the on-screen keyboard is followed via `visualViewport` resize, and orientation changes
158
+ recompute on the next frame.
159
+
160
+ **Cross-package avoidance contract (from issue #116, must not be reverted)**: this
161
+ plugin's pill defaults to `top-right` at 8px from the top and ~26px tall;
162
+ dsh-provider-usage's usage capsule relies on that default position with `offsetY: 48`
163
+ to sit right below it (no overlap by default). Changing this plugin's default anchor /
164
+ vertical offsets breaks that avoidance — treat it as a cross-package behavioral contract
165
+ and adjust provider-usage defaults in lockstep before reverting.
166
+
167
+ Saving in the settings page takes effect immediately, **without restarting dsh web** and
168
+ without manually refreshing the page: the host pushes a frame over the existing SSE events
169
+ channel, and the client automatically re-fetches `/api/dsh-mcp/config` and updates the
170
+ floating position in place.
171
+
172
+ ## Configuration (middleware)
173
+
174
+ All MCP goes through the middleware (single pool, **no mode switch**): no server exposes a
175
+ model-visible `mcp__` direct-call tool; the model surface carries four atomic tools
176
+ (two-level discovery, the standard MCP ecosystem shape) —
177
+ `ws_mcp_list` (full inventory of the current workspace's servers and each server's
178
+ complete tool list, not truncated by `ws_mcp_search`'s `limit`; supports `server`
179
+ full-name/bare-name filtering; `perServerLimit` caps tools per server at 50 by default
180
+ / 500 max, setting `toolsTruncated` when exceeded; empty results carry an explicit
181
+ `message`, and when a `server` filter matches nothing the message attributes the miss to
182
+ the filter and lists the visible project-level servers) / `ws_mcp_detail` (exact single-tool lookup by `@<root>/<server>` + bare tool
183
+ name, returning the complete `inputSchema`; three-way errors: discovery failed with
184
+ reason / server not connected or not found / tool does not exist) / `ws_mcp_search` (keyword search, search first then call; returns a `truncated` flag
185
+ when results hit the `limit`) / `ws_mcp_call` (invoke by `@<root>/<server>`, verify
186
+ argument schema with `ws_mcp_detail`), routed by the calling session's current cwd to
187
+ the matching workspace connection pool, so different workspaces inject different MCPs
188
+ without name clashes, and falling back to the virtual global root `@global` when cwd has no
189
+ project — list/search/detail always merge queries across the "project root unit +
190
+ `@global` unit", and call allows the `@global` root (global config is shared across
191
+ workspaces, so the semantics hold). The host registration name looks like
192
+ `mcp__<id>__<tool>` with an opaque `id` (read it from the tool list); it is internal only
193
+ (the model's tool list never shows it), and a wrapped definition entry
194
+ (`toolDefinitions`) is reached only through `ws_mcp_call` at `@<root>/<server>`.
195
+ Note: global server add/remove/edit refreshes the `@global` unit only after a restart or a
196
+ session touch (existing behavior).
197
+
198
+ ### Top-level config keys (Config schema)
199
+
200
+ Top-level keys of `Config` (`packages/dsh-mcp-manager/src/server/config/config-schema.ts`) with defaults and semantics:
201
+
202
+ | Key | Default | Semantics |
203
+ | --- | --- | --- |
204
+ | `enabled` | `true` | Whether the plugin is enabled (plugin-level master switch). |
205
+ | `announceToAgent` | `true` | Whether to announce the plugin to the Agent (capability list carried by `<available_mcp_servers>`). |
206
+ | `storePath` | empty (defaults to `<DSH_HOME>/dsh-mcp-manager/mcp.json`) | Global server config path; empty uses the default path. |
207
+ | `announceCatalog` | `true` | Whether to inject the MCP capability catalog (`<available_mcp_servers>`). |
208
+ | `catalogMaxEntries` | `6` | Cap on catalog injection entries. |
209
+ | `debug.callStats` | `false` | Whether to enable call-stats debugging and persistence (off by default, config-file only; see "Call stats and debug mode" below). |
210
+ | `debug.statsFile` | empty (defaults to `<DSH_HOME>/dsh-mcp-manager/stats.json`) | Stats persistence path; empty uses the default path. |
211
+ | `ui` | see "Configuration (floating window position)" (`position` defaults to `top-right`; `offset.x` / `offset.y` default to `8`, `blankY` to `40`; `zIndexBase` defaults to `10`, clamped to 1-9000) | Floating window position and z-index; see "Configuration (floating window position)". |
212
+
213
+ **Removed config keys** (#767 batch 2): `middleware` (`off` / `project` / `all`) and
214
+ `middlewarePolicy` (`allowTools` / `denyTools`). The settings-page mode dropdown is gone;
215
+ writing either key **neither errors nor takes effect** (the keys are passed through with no
216
+ consumer), and this plugin never rewrites your config file. Per-tool disable is the only
217
+ admission gate.
218
+
219
+ **Retired implementations**: S1-5c retired the self-built connection stack (four files including `runtime/supervisor.ts` and `runtime/reconnect.ts`, removed as a whole; connections now go through the official engine); W11b2a removed `src/types` and `src/integration` (no migration).
220
+
221
+ ### Per-tool disable (floating window)
222
+
223
+ - Expanding the "Tools (N)" details of a server card shows a **checkbox list**; toggling
224
+ each tool persists via `PATCH /api/dsh-mcp/tool-disable` (stored under
225
+ `<DSH_HOME>/dsh-mcp-manager/user-state.json` → `disabledTools`, merged write, survives restarts);
226
+ - Semantics: **both project-level and global servers' tools can be disabled** (after the
227
+ single-pool change both go through the middleware; global records are keyed by `@global`
228
+ and shared across workspaces); everything is enabled by default;
229
+ - Per-tool disable is **independent of the server-level `enabled` switch** (re-enabling a
230
+ server does not clear its tool-level state);
231
+ - It **applies to all mcp-manager-managed MCP tools** (`mcp__`-prefixed direct calls and
232
+ middleware `ws_mcp_*` calls consistently; runtime wrapped tools are covered too);
233
+ plugins' own declared discipline bare-name tools are not affected;
234
+ - Overlong tool names (>64 chars, hashed suffix) are irreversible → treated as unknown
235
+ server, neither disabled nor mistakenly denied;
236
+ - **Both** the "project-level" and "global" groups in the floating window render tool
237
+ switches (after the single-pool change both groups go through the middleware, so a global
238
+ switch takes effect just the same);
239
+ - Both the floating window and the management panel group servers by
240
+ "project-level / global" (each group further ordered by connection status).
241
+
242
+ ## Routes (all loopback-fenced)
243
+
244
+ | Route | Description |
245
+ | --- | --- |
246
+ | `/api/dsh-mcp/health` | Health check (note: differs from the directory name) |
247
+ | `/api/dsh-mcp/tool-disable` | Per-tool disable toggle (PATCH, loopback-only) |
248
+ | `/api/dsh-mcp/*` | Server management / connection control / tool listing / SSE events, etc. |
249
+
250
+ ## Runtime registration (registerServer.toolDefinitions)
251
+
252
+ Other plugins can register MCP servers at runtime via `ctx.mcpManager.registerServer`
253
+ (in-memory only, not persisted; idempotent for same names). The registration input
254
+ supports an optional `toolDefinitions` field (caller-provided wrapped tool definitions,
255
+ `ToolDefinition[]`; tool names are **bare names**):
256
+
257
+ - **With `toolDefinitions`**: all tools of that server are registered **from the wrapped
258
+ definitions** — `execute` comes from the caller (which may preprocess first and then
259
+ forward to the underlying command internally), skipping the
260
+ remote schema projection and the generic `callTool`; the underlying real implementation
261
+ is never exposed;
262
+ - **Without**: current behavior is preserved (remote schema + generic `callTool`), zero
263
+ impact on other servers;
264
+ - **Direct calls are denied by this package's own pre-execute guard, not by convention (#1014)**: `mcp__<id>__<tool>` is
265
+ an internal registration name; the pre-execute guard **denies by default** every `mcp__`
266
+ direct call that is not this package's own forwarding — it no longer relies on the model
267
+ obeying a prompt, nor on declaration-surface masking (which necessarily leaks during connection
268
+ flips). The single allow-path is the host-issued **in-flight** token: the sub-call of
269
+ `ws_mcp_call` → dispatch → `ctx.tools.execute` is admitted by its `parent`, deregistered when
270
+ it settles, and replaying the same token is denied. The denial text points at `ws_mcp_call`
271
+ and says not to retry the same call.
272
+ - **No model-visible `mcp__` direct-call tool: calls only go through `ws_mcp_call` with bare
273
+ names** (`@<root>/<server>` + bare name; a wrapped `execute` is caller JS, so there is
274
+ no remote implementation to call directly). The call name is still derived via
275
+ `publicToolName` (64-char / hash-suffix rules unchanged), but only as the name the
276
+ middleware forwards when calling;
277
+ - **Per-tool disable / visibility / capability catalog still apply to wrapped tools**
278
+ (judged by server + tool name, same as `mcp__`-prefixed tools);
279
+ - Consumed only by the runtime registration surface (runtimeRegistry) — never persisted
280
+ to the store, never passed through mcpServers imports.
281
+
282
+ ```ts
283
+ await ctx.mcpManager.registerServer({
284
+ name: "my-mcp",
285
+ transport: "stdio",
286
+ command: "my-mcp-server",
287
+ args: ["serve", "--mcp"],
288
+ toolDefinitions: [
289
+ {
290
+ name: "my_tool", // bare name
291
+ description: "Caller-provided wrapped tool",
292
+ parameters: { type: "object", properties: { query: { type: "string" } }, required: ["query"] },
293
+ output: { schema: { ... }, render(args, value) { ... } },
294
+ execute: async (args) => { await prepare(); return forwarded; }, // internal forwarding, never exposed
295
+ },
296
+ ],
297
+ });
298
+ ```
299
+
300
+ ## Data and Security
301
+
302
+ - Server config: `<DSH_HOME>/dsh-mcp-manager/mcp.json` (stores only `${ENV}` references, **never the
303
+ secrets themselves**); on-disk 0600 permissions + same-directory temporary-file atomic replacement.
304
+ On Windows, `EPERM` / `EBUSY` replacement errors receive bounded asynchronous retries; an external
305
+ change detected during the wait aborts the save and preserves the external contents.
306
+ - All `/api/dsh-mcp/*` routes are restricted to loopback access (non-loopback → 403 / wrong
307
+ method → 405)
308
+ - **stdio subprocess environment sanitization (official dsh-mcp-client semantics)**: sanitization
309
+ applies to the **inherited parent environment** only — credential-shaped names (`KEY` /
310
+ `PASSWORD` / `SECRET` / `TOKEN`) and every `DSH_*` variable are stripped from the parent
311
+ environment so host secrets never leak into the MCP subprocess implicitly; **an `env` entry you
312
+ declare explicitly is passed to the child verbatim** (explicit layers merge after the scrub), so
313
+ `env` is not a redaction boundary — do not put credentials there if you want them isolated
314
+ - **stdio subprocess inherits host privileges**: MCP server commands run under the host
315
+ process's permissions; only configure trusted servers. Trust boundary: in-process plugin
316
+ injection (`ctx.mcpManager.registerServer`) shares this plugin's privileges and is not a
317
+ privilege escalation; **the project-level `mcp.json` that ships inside a repository is the
318
+ side you should scrutinize**
319
+ - **Stats carry metadata only**: debug stats (`debug.callStats`) record call counts / durations /
320
+ outcomes plus the progressive-disclosure funnel; **search terms are persisted only as SHA-256
321
+ hash buckets** (`h:<12 hex>`, empty queries as `<empty>`), and plaintext keys written by older
322
+ versions are dropped on read (the drop happens **at read time**; the next flush stops writing them
323
+ back — until then an inactive stats file still holds them). Hashing is **pseudonymization, not
324
+ anonymization**: search terms have low entropy and remain dictionary-attackable. The funnel's
325
+ other two dimensions are still plaintext: the `ws_mcp_list` server filter and the
326
+ `ws_mcp_detail` server/tool — those are server and tool names, already part of the config and
327
+ catalog surfaces, not free text
328
+ - **MCP tools execute on the real server — confirm before acting**; tool results are returned
329
+ as-is and may contain sensitive information; treat tool descriptions/results as untrusted input
330
+ - **Injection trust tiers**: remote tool descriptions/results are untrusted input — render and pass as parameters only, never execute as instructions; local configuration and explicit user actions are trusted
331
+ - **Middleware tool read-only boundary**: `ws_mcp_list` / `ws_mcp_detail` / `ws_mcp_search`
332
+ only read the local catalog cache (never touch remote servers or execute tools);
333
+ `ws_mcp_call` is the only entry point that executes remote tools, and it is governed by the
334
+ per-tool disable table; `ws_mcp_call` error messages follow an
335
+ "explicit + next step" style (confirm the server connection / verify the argument schema
336
+ with `ws_mcp_detail` / check the per-tool disable state)
337
+ - **Per-tool disable (three entry points consistent)**: `ws_mcp_call` (callTool checks the
338
+ disable table), the pre-execute guard (`mcp__`-prefixed direct calls),
339
+ and plugins' own declared discipline bare-name tools all go through the single
340
+ `isToolDenied` decision; disabling only affects `mcp__`-prefixed tools, and the denial
341
+ reason carries that semantic note; records live under `<DSH_HOME>/dsh-mcp-manager/user-state.json`
342
+ → `disabledTools` (the `@global` key is shared across workspaces; merged writes never
343
+ overwrite the whole table)
344
+ - **Call stats and debug mode (Metadata-Only)**: Disabled by default; when configured with `dsh-mcp-manager.debug.callStats: true` in `~/.dsh/settings.yaml`, tool call metrics (call count, success/error, average/max duration) and progressive disclosure funnel stats (`ws_mcp_search` query frequencies, `ws_mcp_list` / `ws_mcp_detail` query distributions) are debounced and atomically written to `<DSH_HOME>/dsh-mcp-manager/stats.json`, with single-line console debug logs; strictly does not persist user arguments or returned content, avoiding code or privacy leaks
345
+ - The capability catalog injection includes source annotations and a "does not represent current
346
+ connection status" note
347
+ - **Message-source shape of the catalog injection**: `source` uses the host-registered generic
348
+ shape `{ kind: "plugin", plugin: "dsh-mcp-manager", form: "snapshot",
349
+ sections: [{ name: "mcp-catalog", text }] }`. A self-invented `source.kind` is refused by
350
+ DSH's session-format v2-to-v3 migration gate (closed whitelist), which makes every session
351
+ persisted before the upgrade unloadable — see the next section
352
+
353
+ <a id="723-repair"></a>
354
+ ## Troubleshooting: a historical session will not open after upgrading (#723)
355
+
356
+ **Symptom**: after upgrading DSH to 0.1.5 or later, a historical session reports in the GUI:
357
+
358
+ ```
359
+ 历史加载失败:failed to observe session "session-…":
360
+ cannot safely transform unclassified message source;
361
+ source v0 artifact remains unchanged (raw log: …/session.jsonl.zstd)(gateway/internal)
362
+ ```
363
+
364
+ and `session.v3.jsonl.zstd` never appears next to the original log. Cause: 0.2.x and earlier
365
+ wrote the catalog injection message as `source.kind = "mcp-catalog"`, which DSH's v2-to-v3
366
+ migration refuses against its closed `source.kind` whitelist for surface messages (the original
367
+ artifact is preserved by design). This version changes the write side to the generic
368
+ host-registered shape, **but artifacts already on disk need a one-time repair**.
369
+
370
+ **Repair (one-off, repeatable)**: the repository script rewrites the old source in place; it
371
+ touches only source metadata, never the body or the event sequence, so DSH still performs the
372
+ migration itself.
373
+
374
+ ```sh
375
+ # 1) Dry run: list affected sessions and counts, change nothing
376
+ node scripts/maintenance/repair-mcp-catalog-sessions.mjs
377
+
378
+ # 2) Apply (a session.jsonl.zstd.bak-<timestamp> backup is created for every rewritten log)
379
+ node scripts/maintenance/repair-mcp-catalog-sessions.mjs --apply
380
+
381
+ # 3) Restart dsh web and open the session
382
+ ```
383
+
384
+ - Stop `dsh web` first: a session log being written is not guaranteed safe to rewrite
385
+ - Reads `<DSH_HOME>` by default (`--home <dir>` or `DSH_HOME` overrides it); `--session <id>`
386
+ limits the run to one session
387
+ - Idempotent: repaired artifacts no longer match; every write is self-checked (frame structure,
388
+ per-line JSON, zero leftover legacy kinds)
389
+ - Rollback: overwrite `session.jsonl.zstd` with the matching `.bak-<timestamp>`
390
+ - **v3 artifacts are repaired by default too**: the v3 read path does not validate
391
+ `message.source`, so v3 sessions with the legacy kind load fine today — but a v3 session created
392
+ before the upgrade and written incrementally afterwards carries **both kinds**, and a future
393
+ v3-to-v4 migration with a similar gate would repeat this outage. The repair only swaps source
394
+ metadata, so v3 semantics are unchanged (verified with strict restore + `Session.fromRestore`)
395
+ - `--legacy-only` limits the run to v0/v1/v2; it never writes a v3 file (DSH performs the migration itself)
396
+ - A trailing torn frame (crash during write) is prefix-decoded with the host's recovery semantics; a truncated last line is dropped
397
+
398
+ ## Verification
399
+
400
+ ```sh
401
+ # Health check (loopback)
402
+ curl -s http://127.0.0.1:3080/api/dsh-mcp/health
403
+
404
+ # Source is in src/; must build after changes
405
+ pnpm --filter dsh-mcp-manager build
406
+ pnpm --filter dsh-mcp-manager test
407
+ ```
408
+
409
+ ## Known Limitations
410
+
411
+ - **The `mcp__` registration name is internal only**: servers are mounted by the official
412
+ dsh-mcp-client, so the host registers tools as `mcp__<id>__<tool>`, where `id` is a
413
+ **random short string** allocated per (workspace, server) — stable within one plugin
414
+ assembly, opaque, and not derivable from the server name; it changes on plugin reload or
415
+ host restart. Those names are removed from the model's tool list, and the model reaches
416
+ them through `ws_mcp_call` — **never hardcode tool names** in scripts or prompts; always
417
+ resolve them through `ws_mcp_list` / `ws_mcp_detail`
418
+ - Does not subscribe to the MCP `tools/list_changed` notification (no long-lived SSE
419
+ connection); tool list changes are re-synced on reconnect / manual refresh
420
+ - The middleware catalog is a "last-good snapshot within collection bounds" (≤512 tools /
421
+ ≤256KB total per server); on discovery failure `ws_mcp_list` surfaces the `unavailable`
422
+ reason
423
+ - Only bridges tool capabilities; MCP resources and prompts have no harness consumption
424
+ interface yet
425
+ - **With several tabs bound to different sessions, project-scoped MCP can follow only one**
426
+ (open item from issue #1028): the host keeps a single `projectRoot`, so tabs overwrite
427
+ each other, and the `rebindSession` on every foreground switch amplifies the overwrite.
428
+ This package does not arbitrate which tab wins.
429
+ - **An unresolvable current session keeps the existing binding instead of clearing it**
430
+ (#1028): before the official session snapshot is ready, or while the tab is not held by
431
+ the official `mainView`, the package treats the read as *unknown* and does **not** send
432
+ `cwd:""` — unknown is not "no project", and a wrong clear leaves the panel with global
433
+ entries only and makes project-scoped imports fail with
434
+ `no active project session`. Warning criterion: a first-frame unknown is the race
435
+ where the official snapshot is not ready yet (it happens on every cold start) and stays
436
+ quiet; one console warning is emitted only when a previously resolved session becomes
437
+ unreadable, or when the session stays unreadable across consecutive frames — so a normal
438
+ cold start and a real failure do not show the same yellow warning.
439
+ - **Foreground auto recovery is non-forced (#1120), not an explicit user reconnect**:
440
+ it reuses the official management mechanism's existing state projection and per-root/server
441
+ in-flight dedup instead of disposing/restarting on every visibilitychange;
442
+ connected/connecting/reconnecting connections keep their generation and official backoff,
443
+ missing entries and real failures recover through the normal path, and userDisabled entries
444
+ are never revived. The official mount layer only exposes ready/disposed/tool-registration
445
+ faces, so **a healthy connection with tool registrations is indistinguishable from a
446
+ half-open one in real time** — auto recovery makes no real-health claim and sends no
447
+ home-grown ping; half-open connections still recover through explicit user reconnect.
448
+ Resume carries the current session cwd, and the server resolves and pins the root within
449
+ that call (old body-less requests fall back to the existing binding; a new request that
450
+ explicitly has no workspace, i.e. `cwd:""`, pins `@global` instead of falling back to
451
+ someone else's projectRoot); the client recovery throttle covers resume keyed by cwd, so
452
+ recoveries for different cwds never swallow each other.
453
+ - Requires Node ≥ 20
454
+
455
+ ## Type dependencies
456
+
457
+ Host-side and client-side types both come from the official `@deepseek-ai/*` packages
458
+ (`cordis`, `dsh-host-webserver`, `dsh-agent`, `dsh-tools`, `dsh-system-prompt`,
459
+ `dsh-llm`, `dsh-client-ui-slots`, `dsh-api-session-controller`,
460
+ `dsh-client-ui-session`; versions are pinned in the repository's `pnpm-workspace.yaml`
461
+ catalog and upgraded with DSH releases): **`import type` only, compile-time usage** —
462
+ build artifacts contain zero official runtime imports. The package declares this host
463
+ coupling as optional peerDependencies; consumers running type checks against the plugin
464
+ must be able to resolve these official packages (skipping type checking is unaffected).
465
+
466
+ The client's "current session" read goes through the single seam
467
+ `src/client/core/current-session.ts`, using the official criterion
468
+ `retainedBy.mainView > 0` and anchored to the official `SessionListState` /
469
+ `SessionSummary` types (**no self-declared mirror types**): since DSH 0.1.7-rc.2 the
470
+ session list snapshot no longer has a `current` field and the official `ISessions`
471
+ exposes no current-session accessor, so the package stopped hand-rolling a
472
+ `{current, byId}` shape.
473
+
474
+ ## License
475
+
476
+ MIT