dsh-project-mcp-manager 0.1.0 → 0.3.1
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/README.md +259 -64
- package/docs/README.zh.md +244 -0
- package/lib/cc-file.js +192 -0
- package/lib/cli.js +457 -0
- package/lib/mcp-file.js +9 -0
- package/lib/model.js +121 -4
- package/lib/registry.js +578 -89
- package/package.json +53 -48
package/README.md
CHANGED
|
@@ -1,34 +1,74 @@
|
|
|
1
1
|
# dsh-project-mcp-manager
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
3
|
+
English | [中文](docs/README.zh.md)
|
|
4
|
+
|
|
5
|
+
A project-level MCP auto-loading plugin for DSH: write MCP server configs in
|
|
6
|
+
`<projectRoot>/.dsh/mcp.yml` and they are mounted automatically (via the
|
|
7
|
+
official `@deepseek-ai/dsh-mcp-client`) whenever a dsh session opens in that
|
|
8
|
+
project. Changes to the file hot-reload into the running dsh process, and tool
|
|
9
|
+
visibility is scoped per session cwd. No UI — core functionality only.
|
|
10
|
+
|
|
11
|
+
## Installation (mount into a profile)
|
|
12
|
+
|
|
13
|
+
The plugin is mounted through a **bundle patch**: once the package is added to
|
|
14
|
+
`dsh.profile.bundles`, dsh synthesizes each bundle's patch (the
|
|
15
|
+
`cordis.patch.yml` pointed to by `dsh.bundle.patch`) into plugin lines at
|
|
16
|
+
startup, in order.
|
|
17
|
+
|
|
18
|
+
**Prerequisite: install dsh itself** (for users who don't have dsh yet):
|
|
19
|
+
|
|
20
|
+
```powershell
|
|
21
|
+
npm install -g @deepseek-ai/dsh # official npm package
|
|
22
|
+
npm install -g deepseek-ai/dsh # or install from the GitHub source
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Option 1: the dsh plugin command (recommended)** — `dsh plugin` forwards
|
|
26
|
+
pnpm inside the profile directory and handles installing/upgrading
|
|
27
|
+
dependencies:
|
|
28
|
+
|
|
29
|
+
```powershell
|
|
30
|
+
# Install the latest version (web profile shown as an example;
|
|
31
|
+
# substitute the name of any other profile, e.g. headless)
|
|
32
|
+
dsh plugin --profile web add dsh-project-mcp-manager@latest
|
|
33
|
+
|
|
34
|
+
# Install a specific version (check available versions with
|
|
35
|
+
# npm view dsh-project-mcp-manager versions)
|
|
36
|
+
dsh plugin --profile web add dsh-project-mcp-manager@0.2.0
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Option 2: install directly with pnpm** (equivalent to option 1):
|
|
40
|
+
|
|
41
|
+
```powershell
|
|
42
|
+
# dshHome defaults to %USERPROFILE%\.dsh (uses $DSH_HOME if set)
|
|
43
|
+
cd $env:USERPROFILE\.dsh\profiles\web
|
|
44
|
+
pnpm add dsh-project-mcp-manager@latest
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Option 3: local development install** (a junction that live-syncs your
|
|
48
|
+
source, so code changes take effect immediately):
|
|
49
|
+
|
|
50
|
+
```powershell
|
|
51
|
+
cd $env:USERPROFILE\.dsh\profiles\web
|
|
52
|
+
pnpm add link:<path-to-your-dsh-mcp-project-source> # e.g. D:\dev\dsh-mcp-project
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Upgrading / pinning versions**: re-run the `add` command from option 1 with
|
|
56
|
+
the desired version suffix — `@latest` upgrades to the newest release, `@0.2.0`
|
|
57
|
+
pins to a specific version.
|
|
58
|
+
|
|
59
|
+
## Build & test
|
|
60
|
+
|
|
61
|
+
```powershell
|
|
62
|
+
npm install
|
|
63
|
+
npm run build # tsc → lib/
|
|
64
|
+
npm test # node test/test-model.mjs / test-mcp-file / test-cc-file / test-registry / test-cli
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Configuration format
|
|
68
|
+
|
|
69
|
+
`<projectRoot>/.dsh/mcp.yml` uses the same managed-block format as the profile
|
|
70
|
+
`cordis.patch.yml` (a YAML `insert` list between begin/end markers), with one
|
|
71
|
+
MCP server per line:
|
|
32
72
|
|
|
33
73
|
```yaml
|
|
34
74
|
# >>> dsh-project-mcp-manager:mcp:begin
|
|
@@ -40,7 +80,7 @@
|
|
|
40
80
|
transport: stdio
|
|
41
81
|
command: npx
|
|
42
82
|
args: ['-y', '@modelcontextprotocol/server-gitlab']
|
|
43
|
-
cwd: . #
|
|
83
|
+
cwd: . # resolved relative to the project root
|
|
44
84
|
toolCallTimeoutMs: 60000
|
|
45
85
|
failOnStartupError: false
|
|
46
86
|
reconnect:
|
|
@@ -51,51 +91,206 @@
|
|
|
51
91
|
# <<< dsh-project-mcp-manager:mcp:end
|
|
52
92
|
```
|
|
53
93
|
|
|
54
|
-
`transport`
|
|
55
|
-
|
|
94
|
+
`transport` supports `stdio` (command/args/env/cwd) and `streamable-http`
|
|
95
|
+
(url/headers). Add `disabled: true` to a line to deactivate it. Content outside
|
|
96
|
+
the markers is preserved byte-for-byte.
|
|
56
97
|
|
|
57
|
-
|
|
98
|
+
**Divergences from the native cordis dialect**: the `!!js` tag (a js-yaml
|
|
99
|
+
expression evaluated by the profile loader, e.g. the official README's
|
|
100
|
+
`env: { TOKEN: !!js process.env.GITHUB_TOKEN }`) is **not supported** in
|
|
101
|
+
project files — an unresolved tag inside the managed block makes the whole
|
|
102
|
+
file fail with an explicit error (logged and written to `.dsh/.mcp-diag.json`)
|
|
103
|
+
instead of silently mounting the expression text as a literal string. Values
|
|
104
|
+
in `env`/`headers` are otherwise literal, except for `${VAR}` references
|
|
105
|
+
which are interpolated at mount time (see `${VAR}` expansion below);
|
|
106
|
+
`disabled` must be `true`/`false`. The project file is otherwise a superset
|
|
107
|
+
grammar: `env`/`headers` accept `KEY: null` to delete a key (stripped at
|
|
108
|
+
mount), which the official mcp-client schema rejects — such lines would fail
|
|
109
|
+
if moved back to `cordis.patch.yml`.
|
|
58
110
|
|
|
59
|
-
|
|
60
|
-
按顺序合成每个 bundle 的 patch(`dsh.bundle.patch` 指向的 `cordis.patch.yml`)
|
|
61
|
-
作为插件行。**不要再往 profile 的 `cordis.patch.yml` 手动插入同名行**——两层
|
|
62
|
-
同时存在会启动失败:`duplicate loader entry id: mcp-project`(EntryGroup
|
|
63
|
-
拒绝重复 id)。二选一:留在 bundles,或移出 bundles 改手动插行,不能同时。
|
|
111
|
+
## Claude Code compatibility (read-only)
|
|
64
112
|
|
|
65
|
-
|
|
113
|
+
To fit the habits of Claude Code users, two CC locations are **loaded** but
|
|
114
|
+
never written by this plugin — with different defaults, because they are
|
|
115
|
+
different risk classes:
|
|
66
116
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
117
|
+
- `<projectRoot>/.mcp.json` — the CC project file (`{ "mcpServers": { … } }`).
|
|
118
|
+
An in-repo, version-controlled project declaration: **on by default**,
|
|
119
|
+
read-only. Turn the whole layer off with `DSH_MCP_IGNORE_MCP_JSON=1`.
|
|
120
|
+
- `~/.claude.json` — only the **top-level `mcpServers`** subtree is read, and
|
|
121
|
+
only **on explicit request**: set `DSH_MCP_READ_CLAUDE_USER=1`. This is CC's
|
|
122
|
+
machine-wide user state, not something any one project asked for: mounting
|
|
123
|
+
it unconditionally fans every such server into *every* known dsh project as
|
|
124
|
+
its own process — the exact incident that made it opt-in. Nothing else in
|
|
125
|
+
the file is ever touched, logged or displayed (strict allowlist: oauth
|
|
126
|
+
credentials, per-project history and UI state stay invisible to this
|
|
127
|
+
plugin).
|
|
128
|
+
|
|
129
|
+
Notes and limits:
|
|
130
|
+
|
|
131
|
+
- **No `local` scope.** CC's `claude mcp add` defaults to a per-project
|
|
132
|
+
section inside `~/.claude.json` (`projects.<cwd>.mcpServers`); this plugin
|
|
133
|
+
does not read it. Use `dsh-mcp add --scope user` or the project files.
|
|
134
|
+
- `type: "sse"` entries are rejected with a per-entry diagnostic — the mount
|
|
135
|
+
backend (`dsh-mcp-client`) only speaks `stdio` and `streamable-http`.
|
|
136
|
+
CC's `type: "http"` and an explicit `type: "streamable-http"` both map to
|
|
137
|
+
`streamable-http`; with `type` omitted, an entry that has only `url` (no
|
|
138
|
+
`command`) is treated as http, everything else as stdio.
|
|
139
|
+
- stdio `cwd`: for project-layer rows an empty `cwd` resolves to the project
|
|
140
|
+
root; for user-layer rows (both `~/.dsh/mcp.yml` and `~/.claude.json`) it
|
|
141
|
+
resolves to the dsh host's working directory. Note what "user layer" does
|
|
142
|
+
*not* mean: user rows still mount **one process per known project** (a
|
|
143
|
+
session in two projects gets two fibers, renamed `p<hash>_…` per the
|
|
144
|
+
effective-name rules) — only their `cwd` is shared, not the connection.
|
|
145
|
+
- Unknown CC keys are ignored per entry. `enabled: false` — and, as an alias,
|
|
146
|
+
`disabled: true` — skips the row silently (no diagnostic, and no name
|
|
147
|
+
occupancy — see below).
|
|
148
|
+
- A `disabled: true` row in a **native yml** still holds its shadow keys in
|
|
149
|
+
the shadow chain (see dedup below): same-name rows in lower layers are
|
|
150
|
+
shadowed too and stay unmounted — disabled means "this name must not run",
|
|
151
|
+
not "let the CC copy through". CC-side `enabled: false` has no placeholder
|
|
152
|
+
effect. Consequence: switching off a `.mcp.json` entry without deleting it
|
|
153
|
+
lets a **same-named user-layer row surface** in that project; to suppress a
|
|
154
|
+
server across all layers, keep a `disabled: true` placeholder row (matching
|
|
155
|
+
name or normalized name) in `.dsh/mcp.yml`.
|
|
156
|
+
- Broken files/entries never take down the valid ones; each source fails
|
|
157
|
+
independently, so an unreadable `.mcp.json` cannot unmount the project's
|
|
158
|
+
yml rows (and vice versa). Entry errors land in `.dsh/.mcp-diag.json` and
|
|
159
|
+
the host log (file content is never echoed). A project whose only MCP
|
|
160
|
+
config is `.mcp.json` still gets a `.dsh/` directory as soon as there is
|
|
161
|
+
anything worth reporting there.
|
|
162
|
+
- `~/.claude.json` is rewritten by CC on every session; the watcher re-reads
|
|
163
|
+
it but only triggers reconciliation when the `mcpServers` subtree actually
|
|
164
|
+
changed (canonical-JSON hash gate).
|
|
165
|
+
- Boundary switches (dsh host environment): `DSH_MCP_READ_CLAUDE_USER=1`
|
|
166
|
+
opts the `~/.claude.json` user layer in; `DSH_MCP_IGNORE_MCP_JSON=1` turns
|
|
167
|
+
the project `.mcp.json` layer off; the legacy `DSH_MCP_IGNORE_CLAUDE_JSON=1`
|
|
168
|
+
force-disables the user layer and **wins over the opt-in** (a one-shot
|
|
169
|
+
warning names the winner; the legacy switch will be removed in a later
|
|
170
|
+
release). While the user layer is on, the host logs its fan-out size once
|
|
171
|
+
("N servers will join M known projects") so extra spawns are explainable.
|
|
172
|
+
|
|
173
|
+
**Shadow priority** — layers merge first-come-first-served (1 → 4 below), and
|
|
174
|
+
a row is shadowed when it collides with an earlier row on **any** of three
|
|
175
|
+
keys: the exact `serverName`; the *normalized name* (lowercased with
|
|
176
|
+
non-alphanumerics stripped — `unityMCP` and `unity-mcp` are one service
|
|
177
|
+
written two ways); or the *service identity* (`stdio`: command + args,
|
|
178
|
+
path-case-insensitive on Windows; `streamable-http`: the url). Rows without a
|
|
179
|
+
command/url register no identity key — `node a.js` and `node b.js` stay
|
|
180
|
+
different services — while `disabled` placeholder rows hold all three keys
|
|
181
|
+
without mounting anything. Identity is compared on the **raw strings as
|
|
182
|
+
written in the file, before `${VAR}` expansion**, and `env`, `headers`, `cwd`
|
|
183
|
+
are *not* part of the key: two genuinely different servers sharing one command
|
|
184
|
+
line (but e.g. different env) still collapse to the winner, while the same
|
|
185
|
+
server written once with a `${VAR}` and once as a literal does not match. If a
|
|
186
|
+
drop was unintended, rename the loser (past normalization) or adjust its
|
|
187
|
+
command line. Losers are reported in `.dsh/.mcp-diag.json`
|
|
188
|
+
(`shadowedByYml` / `shadowedByProject` / `shadowedIdentity`) and every
|
|
189
|
+
identity/normalized-name drop warns in the host log:
|
|
190
|
+
|
|
191
|
+
1. `<projectRoot>/.dsh/mcp.yml` (native, panel/CLI-managed)
|
|
192
|
+
2. `<projectRoot>/.mcp.json` (CC project)
|
|
193
|
+
3. `~/.dsh/mcp.yml` (native user layer — see CLI)
|
|
194
|
+
4. `~/.claude.json` top-level `mcpServers` (CC user, opt-in)
|
|
71
195
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
196
|
+
User-layer rows apply to every known project, so a name defined both in one
|
|
197
|
+
project and in a user layer (or in two projects) participates in the regular
|
|
198
|
+
effective-name conflict renaming (see How it works).
|
|
75
199
|
|
|
76
|
-
|
|
77
|
-
`dsh plugin --profile web ...`),同样确认包已加入 `dsh.profile.bundles`。
|
|
200
|
+
## `${VAR}` expansion
|
|
78
201
|
|
|
79
|
-
|
|
202
|
+
`${VAR}` references (matching `\$\{[A-Za-z_][A-Za-z0-9_]*\}` anywhere in the
|
|
203
|
+
string) in `command`, `args[*]`, `env[*]`, `cwd`, `url` and `headers[*]` — from
|
|
204
|
+
**any** of the four sources above — are **interpolated** from the dsh host
|
|
205
|
+
process environment at mount time (same semantics as Claude Code, so
|
|
206
|
+
`"Authorization": "Bearer ${TOKEN}"` works). An unset or empty variable makes
|
|
207
|
+
the row skip with an `env-missing` diagnostic naming the variable (never its
|
|
208
|
+
value); a literal `${NAME}` that must survive unexpanded is not expressible.
|
|
209
|
+
A `url` containing a reference is accepted by the pre-mount schema in any
|
|
210
|
+
position — including the host part, e.g. `https://${HOST}/mcp` — because
|
|
211
|
+
validity is judged only after expansion: expanded inputs are re-validated
|
|
212
|
+
against the mount schema before spawn, and a malformed result (e.g. a non-URL
|
|
213
|
+
`${GATEWAY}/mcp`) skips the row with an `env-invalid` diagnostic instead of
|
|
214
|
+
reaching the mount backend. In the snapshot/row views, `fiberPhase` stays on
|
|
215
|
+
the mount-lifecycle vocabulary (`pending` for a row that never mounted) and
|
|
216
|
+
the reason rides on a separate `skipReason` field (`env-missing` /
|
|
217
|
+
`env-invalid` / `config-invalid` / `plugin-throw`). Values are never
|
|
218
|
+
persisted anywhere by the plugin; the CLI writes `${VAR}` through literally,
|
|
219
|
+
so secrets can live in the environment while configs live in git.
|
|
80
220
|
|
|
81
|
-
|
|
82
|
-
会被本插件读取(loader entries),项目同名行自动改名避开。
|
|
83
|
-
- v2.1 面板源码(含 ProjectMcpRegistry):与本插件功能重复,**同一进程内
|
|
84
|
-
两者只能启用一个**(双装载同一 serverName 会触发 mcp-client 的进程根
|
|
85
|
-
预留冲突)。v2.1 写出的项目文件行(`panel-mcp-` 前缀 id)本插件可直接
|
|
86
|
-
读取,过渡平滑。
|
|
221
|
+
## CLI: `dsh-mcp`
|
|
87
222
|
|
|
88
|
-
|
|
223
|
+
CC-style command-line management for the **native** files (writes only
|
|
224
|
+
`.dsh/mcp.yml` — never `.mcp.json` / `~/.claude.json`; running hosts
|
|
225
|
+
converge via the file watchers, no dsh connection needed):
|
|
89
226
|
|
|
90
227
|
```powershell
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
node
|
|
94
|
-
|
|
228
|
+
dsh-mcp add gitlab npx -y @modelcontextprotocol/server-gitlab -e GITLAB_TOKEN=${GITLAB_TOKEN}
|
|
229
|
+
dsh-mcp add --transport http sentry https://mcp.sentry.dev/mcp -H "Authorization: Bearer ${SENTRY_TOKEN}"
|
|
230
|
+
dsh-mcp add --scope user shared node ./tools/shared.js # writes ~/.dsh/mcp.yml
|
|
231
|
+
dsh-mcp list # all four layers, with shadow annotations
|
|
232
|
+
dsh-mcp get gitlab # winning entry, secret values shown as key names only
|
|
233
|
+
dsh-mcp remove gitlab # native yml only; read-only layers get guidance
|
|
95
234
|
```
|
|
96
235
|
|
|
97
|
-
|
|
236
|
+
Scopes: `--scope project` (default; writes `<projectRoot>/.dsh/mcp.yml` under
|
|
237
|
+
the nearest `.git` ancestor) and `--scope user` (writes `~/.dsh/mcp.yml`,
|
|
238
|
+
mounted into every project). `add` defaults `cwd` to `"."` (project root) for
|
|
239
|
+
project scope and `""` (host directory) for user scope; `-c` overrides.
|
|
240
|
+
There is no `local` scope — `--scope local`
|
|
241
|
+
fails with an explanation. `--transport` accepts `stdio` (default) and `http`;
|
|
242
|
+
`sse` is refused (unsupported by the backend).
|
|
243
|
+
|
|
244
|
+
## How it works
|
|
245
|
+
|
|
246
|
+
- **Project discovery**: the `session.header.cwd` of an active agent session,
|
|
247
|
+
plus the dsh process start directory → walk up to the nearest ancestor
|
|
248
|
+
containing `.git` as the project root (falls back to the directory itself
|
|
249
|
+
when there is no `.git`).
|
|
250
|
+
- **Mounting**: each `(project, serverName)` pair mounts one
|
|
251
|
+
`@deepseek-ai/dsh-mcp-client` instance (`ctx.plugin`) on the host ctx and
|
|
252
|
+
registers it into the global tool layer. Multiple sessions inside the same
|
|
253
|
+
project share a single connection.
|
|
254
|
+
- **Hot reload**: chokidar watches each project root (depth 2, ignoring
|
|
255
|
+
node_modules/.git/.hg/.svn), but only edits to the **exact** config files of
|
|
256
|
+
known project roots — `<projectRoot>/.dsh/mcp.yml` and
|
|
257
|
+
`<projectRoot>/.mcp.json` — trigger a full reconciliation after a 150 ms
|
|
258
|
+
debounce: added lines are mounted, removed lines are unmounted, and config
|
|
259
|
+
changes are remounted. A second watcher covers the user layer as two
|
|
260
|
+
**exact file paths** — `~/.dsh/mcp.yml` and `~/.claude.json` (chokidar v5
|
|
261
|
+
notices a watched file being created as long as its parent directory
|
|
262
|
+
exists) — never the home directory at large. `~/.claude.json` events are
|
|
263
|
+
arbitrated by the canonical-JSON content hash alone (no size/mtime fast
|
|
264
|
+
path: same-instant, same-length rewrites with different content must not be
|
|
265
|
+
swallowed).
|
|
266
|
+
- **Effective names**: when the original `serverName` is unique across the
|
|
267
|
+
whole catalog (global lines + all project lines, where each project's
|
|
268
|
+
merged rows include the user-layer rows that survived shadowing) it keeps
|
|
269
|
+
its name; on a conflict both sides are renamed to
|
|
270
|
+
`p<first 6 chars of sha256(projectRoot)>_<original name>` (truncated to 32
|
|
271
|
+
characters, deterministic and independent of mount order) to avoid the
|
|
272
|
+
serverName reservation conflicts that `dsh-mcp-client` makes per process
|
|
273
|
+
root. Global lines (profile `cordis.patch.yml` / mcp-client lines already
|
|
274
|
+
mounted at the bundle level) participate in occupancy determination but are
|
|
275
|
+
never renamed. Model-visible tool names are built from the **effective**
|
|
276
|
+
server name and the MCP tool's own name (`mcp__<effectiveServerName>__<toolName>`),
|
|
277
|
+
which may differ from the `serverName` written in the file.
|
|
278
|
+
- **Session visibility**: when an agent is created, its session cwd resolves
|
|
279
|
+
to a project, and `tools.restrict({ deny })` is applied to that agent to deny
|
|
280
|
+
every project server except those of the session's own project; a session
|
|
281
|
+
without a cwd falls back to the owner project (subagents), then to the
|
|
282
|
+
project containing the dsh process cwd. Released when the session is
|
|
283
|
+
destroyed.
|
|
284
|
+
|
|
285
|
+
## Security boundary
|
|
98
286
|
|
|
99
|
-
`.dsh/mcp.yml`
|
|
100
|
-
|
|
101
|
-
|
|
287
|
+
`stdio` lines in `.dsh/mcp.yml` (and `.mcp.json` rows mounted from it) spawn
|
|
288
|
+
their `command` inside the dsh host process — project files are **executable
|
|
289
|
+
code carriers**, so only add them in projects you trust. Lines that fail to
|
|
290
|
+
mount or are invalid are skipped with a warning and do not affect other
|
|
291
|
+
servers. CC's machine-wide `~/.claude.json` user layer is therefore **off by
|
|
292
|
+
default**: reading it opts foreign, environment-level servers into every
|
|
293
|
+
project's spawn set, which must be a deliberate `DSH_MCP_READ_CLAUDE_USER=1`.
|
|
294
|
+
The read-only allowlist exists precisely because that file also holds
|
|
295
|
+
credentials: nothing from it is ever written out, echoed into diagnostics, or
|
|
296
|
+
printed by the CLI.
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# dsh-project-mcp-manager
|
|
2
|
+
|
|
3
|
+
[English](../README.md) | 中文
|
|
4
|
+
|
|
5
|
+
项目级 MCP 自动加载插件:在项目根 `<projectRoot>/.dsh/mcp.yml` 写入 MCP
|
|
6
|
+
服务器配置,在该项目开启 dsh 会话时自动装载(经官方
|
|
7
|
+
`@deepseek-ai/dsh-mcp-client`),文件改动热重载到运行中的 dsh 进程,并按
|
|
8
|
+
会话 cwd 控制工具可见性。无 UI,仅具备核心功能。
|
|
9
|
+
|
|
10
|
+
## 安装(挂载到 profile)
|
|
11
|
+
|
|
12
|
+
插件通过 **bundle patch** 挂载:把包加入 `dsh.profile.bundles` 后,dsh 启动时
|
|
13
|
+
按顺序合成每个 bundle 的 patch(`dsh.bundle.patch` 指向的 `cordis.patch.yml`)
|
|
14
|
+
作为插件行。
|
|
15
|
+
|
|
16
|
+
**前置:安装 dsh 本体**(尚未安装 dsh 的用户):
|
|
17
|
+
|
|
18
|
+
```powershell
|
|
19
|
+
npm install -g @deepseek-ai/dsh # npm 官方包
|
|
20
|
+
npm install -g deepseek-ai/dsh # 或从 GitHub 源码安装
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**方式一:dsh 插件命令(推荐)**——`dsh plugin` 在 profile 目录内转发 pnpm,
|
|
24
|
+
负责安装/升级依赖:
|
|
25
|
+
|
|
26
|
+
```powershell
|
|
27
|
+
# 安装最新版(web profile 示例;headless 等其他 profile 替换名字即可)
|
|
28
|
+
dsh plugin --profile web add dsh-project-mcp-manager@latest
|
|
29
|
+
|
|
30
|
+
# 安装指定版本(版本号可先 npm view dsh-project-mcp-manager versions 查看)
|
|
31
|
+
dsh plugin --profile web add dsh-project-mcp-manager@0.2.0
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**方式二:直接 pnpm 安装**(与方式一等价):
|
|
35
|
+
|
|
36
|
+
```powershell
|
|
37
|
+
# dshHome 默认为 %USERPROFILE%\.dsh(设置了 DSH_HOME 则用其值)
|
|
38
|
+
cd $env:USERPROFILE\.dsh\profiles\web
|
|
39
|
+
pnpm add dsh-project-mcp-manager@latest
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**方式三:本地开发安装**(junction 实时同步源码,改代码即生效):
|
|
43
|
+
|
|
44
|
+
```powershell
|
|
45
|
+
cd $env:USERPROFILE\.dsh\profiles\web
|
|
46
|
+
pnpm add link:<你的 dsh-mcp-project 源码目录> # 例如 D:\dev\dsh-mcp-project
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**升级/锁定版本**:重跑方式一的 `add` 命令并带上目标版本后缀——`@latest`
|
|
50
|
+
升级到最新,`@0.2.0` 锁定到指定版本。
|
|
51
|
+
|
|
52
|
+
## 构建与测试
|
|
53
|
+
|
|
54
|
+
```powershell
|
|
55
|
+
npm install
|
|
56
|
+
npm run build # tsc → lib/
|
|
57
|
+
npm test # node 直跑 test/ 下五个 .mjs(model / mcp-file / cc-file / registry / cli)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## 配置格式
|
|
61
|
+
|
|
62
|
+
`<projectRoot>/.dsh/mcp.yml`,格式与 profile `cordis.patch.yml` 的受管块
|
|
63
|
+
一致(begin/end 标记之间的 YAML insert 列表),每行一个 MCP 服务器:
|
|
64
|
+
|
|
65
|
+
```yaml
|
|
66
|
+
# >>> dsh-project-mcp-manager:mcp:begin
|
|
67
|
+
- insert:
|
|
68
|
+
- id: panel-mcp-gitlab
|
|
69
|
+
name: '@deepseek-ai/dsh-mcp-client'
|
|
70
|
+
config:
|
|
71
|
+
serverName: gitlab
|
|
72
|
+
transport: stdio
|
|
73
|
+
command: npx
|
|
74
|
+
args: ['-y', '@modelcontextprotocol/server-gitlab']
|
|
75
|
+
cwd: . # 相对项目根解析
|
|
76
|
+
toolCallTimeoutMs: 60000
|
|
77
|
+
failOnStartupError: false
|
|
78
|
+
reconnect:
|
|
79
|
+
enabled: true
|
|
80
|
+
initialDelayMs: 500
|
|
81
|
+
maxDelayMs: 30000
|
|
82
|
+
maxAttempts: 10
|
|
83
|
+
# <<< dsh-project-mcp-manager:mcp:end
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`transport` 支持 `stdio`(command/args/env/cwd)与 `streamable-http`
|
|
87
|
+
(url/headers)。行加 `disabled: true` 即停用。标记之外的内容逐字节保留。
|
|
88
|
+
|
|
89
|
+
**与原生 cordis 方言的差异**:`!!js` 标签(profile 的 `cordis.patch.yml` 由
|
|
90
|
+
Loader 求值的 js-yaml 表达式,如官方 README 示例 `env: { TOKEN: !!js
|
|
91
|
+
process.env.GITHUB_TOKEN }`)在项目文件里**不支持**——受管块内出现未解析
|
|
92
|
+
标签会使该文件整体报错跳过(写入 `.dsh/.mcp-diag.json` 并打日志),不会把
|
|
93
|
+
表达式当字面量字符串静默装载。`env`/`headers` 的值 otherwise 是字面量,仅
|
|
94
|
+
`${VAR}` 引用会在装载时做串内插值(见下文「${VAR} 展开」);`disabled` 只能是
|
|
95
|
+
`true`/`false`。反之项目文件是超集语法:`env`/`headers` 允许 `KEY: null`
|
|
96
|
+
表示删除该键(装载时被剔除),这在官方 mcp-client 校验里会被拒绝——把这类
|
|
97
|
+
行原样挪回 `cordis.patch.yml` 会装载失败。
|
|
98
|
+
|
|
99
|
+
## Claude Code 兼容层(只读)
|
|
100
|
+
|
|
101
|
+
为顺应 CC 用户习惯,插件会**装载**以下两个 CC 配置位置,但从不写入它们。
|
|
102
|
+
两者的默认姿态不同,因为风险等级不同:
|
|
103
|
+
|
|
104
|
+
- `<projectRoot>/.mcp.json` —— CC 的 project 层文件(`{ "mcpServers": { … } }`)。
|
|
105
|
+
它是随仓库版本管理的项目自身声明:**默认开启**、只读。设
|
|
106
|
+
`DSH_MCP_IGNORE_MCP_JSON=1` 可整层停用。
|
|
107
|
+
- `~/.claude.json` —— 只读取**顶层 `mcpServers`** 子树,且**只在显式请求时**:
|
|
108
|
+
设 `DSH_MCP_READ_CLAUDE_USER=1` 才读。这是 CC 的机器级用户状态,不属于任何
|
|
109
|
+
项目:无条件读取会把每台这样的服务器扇出到**每个已知 dsh 项目**各起一个
|
|
110
|
+
进程——这正是把它改为 opt-in 的事故根因。该文件其余内容一律不读入配置、
|
|
111
|
+
不写、不打日志、不在任何输出里回显(严格 allowlist:oauth 凭据、项目历史、
|
|
112
|
+
UI 状态对本插件不可见)。
|
|
113
|
+
|
|
114
|
+
边界与限制:
|
|
115
|
+
|
|
116
|
+
- **没有 local 作用域**。CC 的 `claude mcp add` 默认写进 `~/.claude.json` 的
|
|
117
|
+
`projects.<cwd>.mcpServers`(local 层),本插件不读取该层;请用
|
|
118
|
+
`dsh-mcp add --scope user` 或项目文件。
|
|
119
|
+
- `type: "sse"` 条目按条目报错跳过——装载后端(`dsh-mcp-client`)只支持
|
|
120
|
+
`stdio` 与 `streamable-http`。CC 的 `type: "http"` 与显式
|
|
121
|
+
`type: "streamable-http"` 都映射为 `streamable-http`;缺省 `type` 时,
|
|
122
|
+
只带 `url` 不带 `command` 的条目按 http 处理,其余视为 stdio。
|
|
123
|
+
- stdio 的 `cwd`:项目层行的空 `cwd` 解析为项目根;用户层行
|
|
124
|
+
(`~/.dsh/mcp.yml` 与 `~/.claude.json`)解析为 dsh 宿主的工作目录。
|
|
125
|
+
注意"用户层"不等于共享进程:用户层行仍**按每个已知项目各装一个进程**
|
|
126
|
+
(两个项目各开会话就是两个 fiber,按生效名规则改名为 `p<hash>_…`)——
|
|
127
|
+
共享的只是 `cwd`,不是连接。
|
|
128
|
+
- 未知 CC 键容忍忽略;`enabled: false`(以及作为别名的 `disabled: true`)
|
|
129
|
+
静默跳过该条目(不记诊断,也不占名)。
|
|
130
|
+
- 原生 yml 里 `disabled: true` 的行仍**占住影子键**(见下方去重规则):下层
|
|
131
|
+
同名行一并被遮蔽不装载——禁用意味着"这个名字不许跑",而不是"让位给 CC
|
|
132
|
+
副本"。CC 侧的 `enabled: false` 没有占位效果。推论:不删除、只在
|
|
133
|
+
`.mcp.json` 里关掉一条,同名的**用户层行会在该项目浮上来**装载;要把某台
|
|
134
|
+
服务器在所有层压住,请在 `.dsh/mcp.yml` 留一条 `disabled: true` 占位行
|
|
135
|
+
(名字或归一化名字对上即可)。
|
|
136
|
+
- 坏文件/坏条目不影响其他服务器,且**按源隔离**:`.mcp.json` 坏了不会卸掉
|
|
137
|
+
同项目的 yml 行(反之亦然);条目错误写入 `.dsh/.mcp-diag.json` 与宿主
|
|
138
|
+
日志(诊断从不带文件内容)。只有 `.mcp.json`、没用过原生 yml 的项目,
|
|
139
|
+
一旦有可报内容也会创建 `.dsh/` 目录。
|
|
140
|
+
- CC 每次会话都会重写 `~/.claude.json`;watcher 会对该文件做
|
|
141
|
+
`mcpServers` 子树的规范化哈希门控——子树没变就不触发对账。
|
|
142
|
+
- 共存边界开关(dsh 宿主环境变量):`DSH_MCP_READ_CLAUDE_USER=1` 显式启用
|
|
143
|
+
`~/.claude.json` 用户层;`DSH_MCP_IGNORE_MCP_JSON=1` 停用项目 `.mcp.json`
|
|
144
|
+
层;旧开关 `DSH_MCP_IGNORE_CLAUDE_JSON=1` 为强制关闭用户层,**优先级高于
|
|
145
|
+
opt-in**(冲突时打一次「胜出」告警;旧开关将在后续版本移除)。用户层开启
|
|
146
|
+
时,宿主一次性日志提示扇出规模(「N 条服务器将并入 M 个已知项目」),
|
|
147
|
+
让多出来的 spawn 可解释、可回退。
|
|
148
|
+
|
|
149
|
+
**影子优先序**——按 1→4 先到先得合并,后到行与已收录行命中**三把键中的任何
|
|
150
|
+
一把**即被遮蔽:精确 `serverName`;*归一化名称*(转小写去掉非字母数字后相同
|
|
151
|
+
——`unityMCP` 与 `unity-mcp` 就是一台服务器的两种写法);*服务身份*
|
|
152
|
+
(`stdio` 取 command + args,Windows 下路径大小写不敏感;`streamable-http`
|
|
153
|
+
取 url)。command/url 为空的行不注册身份键——`node a.js` 与 `node b.js` 是
|
|
154
|
+
不同服务、绝不互杀——而 `disabled` 占位行三键全占、自身不装载。身份比对用的
|
|
155
|
+
是**文件里的原始字符串,发生在 `${VAR}` 展开之前**,且 `env`、`headers`、
|
|
156
|
+
`cwd` **不参与**身份键:同一命令行、仅 env 不同的两台真不同服务器仍会被去重
|
|
157
|
+
(只留高优先级一条),同一台服务器一条写 `${VAR}`、一条写字面量则**不**互认。
|
|
158
|
+
误剔时的处置:给被剔行改名(归一化后不同)或调整命令与参数。被遮蔽方写入
|
|
159
|
+
`.dsh/.mcp-diag.json`(`shadowedByYml` / `shadowedByProject` /
|
|
160
|
+
`shadowedIdentity`),身份/归一名去重剔除的每行还会在宿主日志告警「跳过重复
|
|
161
|
+
服务定义」:
|
|
162
|
+
|
|
163
|
+
1. `<projectRoot>/.dsh/mcp.yml`(原生格式,面板/CLI 管理)
|
|
164
|
+
2. `<projectRoot>/.mcp.json`(CC project 层)
|
|
165
|
+
3. `~/.dsh/mcp.yml`(原生用户层,见 CLI)
|
|
166
|
+
4. `~/.claude.json` 顶层 `mcpServers`(CC user 层,opt-in)
|
|
167
|
+
|
|
168
|
+
用户层行适用于所有已知项目,因此「某项目与用户层同名」(或两个项目同名)
|
|
169
|
+
会走常规的生效名冲突改名规则(见工作原理)。
|
|
170
|
+
|
|
171
|
+
## `${VAR}` 展开
|
|
172
|
+
|
|
173
|
+
以上任一来源中,`command`、`args[*]`、`env[*]`、`cwd`、`url`、`headers[*]` 里的
|
|
174
|
+
`${VAR}` 引用(正则 `\$\{[A-Za-z_][A-Za-z0-9_]*\}`,允许出现在字符串任意
|
|
175
|
+
位置)在装载时刻从 dsh 宿主进程环境做**串内插值**——与 Claude Code 同语义,
|
|
176
|
+
`"Authorization": "Bearer ${TOKEN}"` 这类写法可用。变量未设置**或为空串**时
|
|
177
|
+
该行跳过装载,诊断记 `env-missing` 并只带变量名(绝不带值);需要保留字面
|
|
178
|
+
`${NAME}` 的写法目前不可表达。含引用的 `url` 在装载前的 schema 校验里任意
|
|
179
|
+
位置都放行(包括 host 段,如 `https://${HOST}/mcp`)——合法性只在展开后判定:
|
|
180
|
+
展开结果会再过一遍装载 schema 复验,产出非法配置(如 `${GATEWAY}/mcp` 拼出
|
|
181
|
+
非 URL)时以 `env-invalid` 跳过,不把坏值递给装载后端。快照/行视图里
|
|
182
|
+
`fiberPhase` 保持装载生命周期枚举(未挂上的行是 `pending`),跳过原因走独立
|
|
183
|
+
的 `skipReason` 字段(`env-missing` / `env-invalid` / `config-invalid` /
|
|
184
|
+
`plugin-throw`)。插件任何写路径都不落盘展开后的值;
|
|
185
|
+
CLI 写入时 `${VAR}` 原样保留——配置可以进 git,凭据留在环境里。
|
|
186
|
+
|
|
187
|
+
## CLI:`dsh-mcp`
|
|
188
|
+
|
|
189
|
+
CC 风格的原生文件命令行管理(**只写** `.dsh/mcp.yml`——从不写
|
|
190
|
+
`.mcp.json` / `~/.claude.json`;不连接运行中的 dsh 宿主,宿主经文件监听自动
|
|
191
|
+
收敛):
|
|
192
|
+
|
|
193
|
+
```powershell
|
|
194
|
+
dsh-mcp add gitlab npx -y @modelcontextprotocol/server-gitlab -e GITLAB_TOKEN=${GITLAB_TOKEN}
|
|
195
|
+
dsh-mcp add --transport http sentry https://mcp.sentry.dev/mcp -H "Authorization: Bearer ${SENTRY_TOKEN}"
|
|
196
|
+
dsh-mcp add --scope user shared node ./tools/shared.js # 写 ~/.dsh/mcp.yml
|
|
197
|
+
dsh-mcp list # 四个来源全展示,带遮蔽标注
|
|
198
|
+
dsh-mcp get gitlab # 优先层条目;密钥值只显示键名
|
|
199
|
+
dsh-mcp remove gitlab # 只动原生 yml;命中只读层时给出编辑指引
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
作用域:`--scope project`(缺省,写最近 `.git` 祖先下的
|
|
203
|
+
`.dsh/mcp.yml`)与 `--scope user`(写 `~/.dsh/mcp.yml`,装载进每个项目)。
|
|
204
|
+
`add` 的 `cwd` 缺省随作用域而变:project 为 `"."`(项目根),user 为 `""`
|
|
205
|
+
(宿主目录);`-c` 显式覆盖。
|
|
206
|
+
没有 `local` 作用域——`--scope local` 会报错并解释。`--transport` 接受
|
|
207
|
+
`stdio`(缺省)与 `http`;`sse` 拒绝(后端不支持)。
|
|
208
|
+
|
|
209
|
+
## 工作原理
|
|
210
|
+
|
|
211
|
+
- **项目发现**:在线 agent 会话的 `session.header.cwd` + dsh 进程启动目录 →
|
|
212
|
+
向上找最近的含 `.git` 的祖先目录作为项目根(无 `.git` 时退回目录本身)。
|
|
213
|
+
- **装载**:每个 `(项目, serverName)` 在宿主 ctx 上装载一个
|
|
214
|
+
`@deepseek-ai/dsh-mcp-client` 实例(`ctx.plugin`),注册进全局工具层。
|
|
215
|
+
同一项目内多会话共享同一连接。
|
|
216
|
+
- **热重载**:chokidar 监听各项目根(depth 2,忽略 node_modules/.git/.hg/
|
|
217
|
+
.svn),但只有**已知项目根的精确配置文件**(`<projectRoot>/.dsh/mcp.yml`
|
|
218
|
+
与 `<projectRoot>/.mcp.json`)的改动经 150ms 防抖触发全量对账:新增行
|
|
219
|
+
装载、删除行卸载、配置变化重装。另有独立 watcher 以**两个精确文件路径**
|
|
220
|
+
监听用户层:`~/.dsh/mcp.yml` 与 `~/.claude.json`(chokidar v5 对被监听的
|
|
221
|
+
缺失文件能在其创建时补发事件,前提是父目录已存在)——不监听家目录整体。
|
|
222
|
+
`~/.claude.json` 事件只由规范化内容哈希门裁决(没有 size/mtime 快路径:
|
|
223
|
+
同刻、同体积而内容不同的重写不能被吞掉)。
|
|
224
|
+
- **生效名**:原始 `serverName` 在整个目录(全局行 + 全部项目行;每个项目的
|
|
225
|
+
行集合含遮蔽后幸存的用户层行)中唯一时
|
|
226
|
+
保持原名;冲突时双方都改为 `p<sha256(项目根)前6位>_<原名>`(截断 32 字符,
|
|
227
|
+
确定性、与装载顺序无关),避免 `dsh-mcp-client` 按进程根的 serverName
|
|
228
|
+
预留冲突。全局行(profile `cordis.patch.yml` / bundle 层已装载的
|
|
229
|
+
mcp-client 行)参与占用判定但不改名。模型可见的工具名由生效服务器名与
|
|
230
|
+
MCP 工具自身的名字拼成 `mcp__<生效名>__<工具名>`,与文件里写的 `serverName` 可能不同。
|
|
231
|
+
- **会话可见性**:agent 创建时按其会话 cwd 解析项目,对该 agent 应用
|
|
232
|
+
`tools.restrict({ deny })`,deny 掉除本会话项目外的全部项目服务器;会话
|
|
233
|
+
无 cwd 时回退 owner 项目(子代理),再回退 dsh 进程 cwd 所在项目。会话
|
|
234
|
+
销毁时释放。
|
|
235
|
+
|
|
236
|
+
## 安全边界
|
|
237
|
+
|
|
238
|
+
`.dsh/mcp.yml` 中的 `stdio` 行(以及经兼容层装载的 `.mcp.json` 行)会在 dsh
|
|
239
|
+
宿主进程内 spawn 其 `command`——项目文件是**可执行代码载体**,只应在可信项目
|
|
240
|
+
中添加。装载失败/配置无效行仅告警跳过,不影响其他服务器。CC 的机器级
|
|
241
|
+
`~/.claude.json` 用户层因此**默认关闭**:读取它等于把外来的环境级服务器塞进
|
|
242
|
+
每个项目的 spawn 集合,必须是显式决定(`DSH_MCP_READ_CLAUDE_USER=1`)。
|
|
243
|
+
`~/.claude.json` 之所以同时按严格 allowlist 只读,正是因为该文件还存放凭据:
|
|
244
|
+
其中未使用的部分不会被读出、写入任何文件,也不会在 CLI 或诊断输出里出现。
|