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 CHANGED
@@ -1,34 +1,74 @@
1
1
  # dsh-project-mcp-manager
2
2
 
3
- 独立项目级 MCP 自动加载插件:在项目根 `<projectRoot>/.dsh/mcp.yml` 写入 MCP
4
- 服务器配置,在该项目开启 dsh 会话时自动装载(经官方
5
- `@deepseek-ai/dsh-mcp-client`),文件改动热重载到运行中的 dsh 进程,并按
6
- 会话 cwd 控制工具可见性。无 UI,核心功能精简型插件。
7
-
8
- ## 工作原理
9
-
10
- - **项目发现**:在线 agent 会话的 `session.header.cwd` + dsh 进程启动目录 →
11
- 向上找最近的含 `.git` 的祖先目录作为项目根(无 `.git` 时退回目录本身)。
12
- - **装载**:每个 `(项目, serverName)` 在宿主 ctx 上装载一个
13
- `@deepseek-ai/dsh-mcp-client` 实例(`ctx.plugin`),注册进全局工具层。
14
- 同一项目内多会话共享同一连接。
15
- - **热重载**:chokidar 监听各项目根(depth 2,忽略 node_modules/.git/.hg/
16
- .svn),`.dsh/mcp.yml` 的增删改经 150ms 防抖触发全量对账:新增行装载、
17
- 删除行卸载、配置变化重装。
18
- - **生效名**:原始 `serverName` 在整个目录(全局行 + 全部项目行)中唯一时
19
- 保持原名;冲突时双方都改为 `p<sha256(项目根)前6位>_<原名>`(截断 32 字符,
20
- 确定性、与装载顺序无关),避免 `dsh-mcp-client` 按进程根的 serverName
21
- 预留冲突。全局行(profile `cordis.patch.yml` / bundle 层已装载的
22
- mcp-client 行)参与占用判定但不改名。
23
- - **会话可见性**:agent 创建时按其会话 cwd 解析项目,对该 agent 应用
24
- `tools.restrict({ deny })`,deny 掉除本会话项目外的全部项目服务器;会话
25
- 无 cwd 时回退 owner 项目(子代理),再回退 dsh 进程 cwd 所在项目。会话
26
- 销毁时释放。
27
-
28
- ## 配置格式
29
-
30
- `<projectRoot>/.dsh/mcp.yml`,格式与 profile `cordis.patch.yml` 的受管块
31
- 一致(begin/end 标记之间的 YAML insert 列表),每行一个 MCP 服务器:
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` 支持 `stdio`(command/args/env/cwd)与 `streamable-http`
55
- (url/headers)。行加 `disabled: true` 即停用。标记之外的内容逐字节保留。
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
- ## 安装(挂载到 profile)
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
- 插件通过 **bundle patch** 挂载:把包加入 `dsh.profile.bundles` 后,dsh 启动时
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
- **本地开发安装**(未发布 npm 前的推荐方式,junction 实时同步源码):
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
- ```powershell
68
- cd C:\Users\haima\.dsh\profiles\web
69
- pnpm add link:D:\path\to\dsh-mcp-project # 源码包目录
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
- 然后在 `package.json` 的 `dsh.profile.bundles` 数组末尾追加
73
- `"dsh-project-mcp-manager"`,重启 dsh 生效(HMR 只热装首次 insert 的行,
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
- **发布后安装**:`pnpm add dsh-project-mcp-manager`(或 dsh 的插件子命令,
77
- `dsh plugin --profile web ...`),同样确认包已加入 `dsh.profile.bundles`。
200
+ ## `${VAR}` expansion
78
201
 
79
- ## 与 dsh-skill-mcp-panel 的关系
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
- - 已装的 v2.0.1 面板:无项目级装载能力,与本插件不冲突;其全局受管块行
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
- npm install
92
- npm run build # tsc → lib/
93
- node test/test-model.mjs
94
- node test/test-registry.mjs
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` 中的 `stdio` 行会在 dsh 宿主进程内 spawn 其 `command`——
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 或诊断输出里出现。