dsh-project-mcp-manager 0.3.1 → 0.4.3

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,296 +1,153 @@
1
- # dsh-project-mcp-manager
2
-
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:
72
-
73
- ```yaml
74
- # >>> dsh-project-mcp-manager:mcp:begin
75
- - insert:
76
- - id: panel-mcp-gitlab
77
- name: '@deepseek-ai/dsh-mcp-client'
78
- config:
79
- serverName: gitlab
80
- transport: stdio
81
- command: npx
82
- args: ['-y', '@modelcontextprotocol/server-gitlab']
83
- cwd: . # resolved relative to the project root
84
- toolCallTimeoutMs: 60000
85
- failOnStartupError: false
86
- reconnect:
87
- enabled: true
88
- initialDelayMs: 500
89
- maxDelayMs: 30000
90
- maxAttempts: 10
91
- # <<< dsh-project-mcp-manager:mcp:end
92
- ```
93
-
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.
97
-
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`.
110
-
111
- ## Claude Code compatibility (read-only)
112
-
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:
116
-
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)
195
-
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).
199
-
200
- ## `${VAR}` expansion
201
-
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.
220
-
221
- ## CLI: `dsh-mcp`
222
-
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):
226
-
227
- ```powershell
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
234
- ```
235
-
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
286
-
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.
1
+ # dsh-project-mcp-manager
2
+
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
+ ## Documentation
12
+
13
+ Feature documentation lives in `docs/`, English and Chinese side by side:
14
+
15
+ - [Configuration format](docs/guide/format.md) — native YAML managed
16
+ block, JSON dialect, divergences from the cordis dialect.
17
+ - [Configuration sources and layers](docs/guide/layers.md) — the
18
+ six-layer source model, shadow priority, global vs project mounting, and the
19
+ read-only legacy Claude Code layer.
20
+ - [`${VAR}` expansion](docs/guide/env-expansion.md) — mount-time interpolation and
21
+ its diagnostics.
22
+ - [CLI `dsh-mcp`](docs/guide/cli.md) — scopes, write formats, ownership contract.
23
+
24
+ Design and release records (Chinese): [dsh 0.1.5-rc.1 adaptation](docs/design/adaptation-dsh-0.1.5-rc1.md) ·
25
+ [dsh 0.1.2-rc.1 adaptation](docs/design/adaptation-dsh-0.1.2-rc1.md) ·
26
+ [JSON config layer proposal](docs/design/proposal-json-mcp-config.md) ·
27
+ [v0.4.3 release notes](docs/releases/v0.4.3.md) ·
28
+ [v0.4.2 release notes](docs/releases/v0.4.2.md) ·
29
+ [v0.4.1 release notes](docs/releases/v0.4.1.md) ·
30
+ [v0.4.0 release notes](docs/releases/v0.4.0.md) ·
31
+ [v0.3.1 release notes](docs/releases/v0.3.1.md).
32
+
33
+ Code review records (Chinese): [TypeScript changes since v0.3.1](docs/code-review/ts-review-since-v0.3.1.zh.md).
34
+
35
+ ## Installation (mount into a profile)
36
+
37
+ The plugin is mounted through a **bundle patch**: once the package is added to
38
+ `dsh.profile.bundles`, dsh synthesizes each bundle's patch (the
39
+ `cordis.patch.yml` pointed to by `dsh.bundle.patch`) into plugin lines at
40
+ startup, in order.
41
+
42
+ **Prerequisite: install dsh itself** (for users who don't have dsh yet):
43
+
44
+ ```powershell
45
+ npm install -g @deepseek-ai/dsh # official npm package
46
+ npm install -g deepseek-ai/dsh # or install from the GitHub source
47
+ ```
48
+
49
+ **Option 1: the dsh plugin command (recommended)** — `dsh plugin` forwards
50
+ pnpm inside the profile directory and handles installing/upgrading
51
+ dependencies:
52
+
53
+ ```powershell
54
+ # Install the latest version (web profile shown as an example;
55
+ # substitute the name of any other profile, e.g. headless)
56
+ dsh plugin --profile web add dsh-project-mcp-manager@latest
57
+
58
+ # Install a specific version (check available versions with
59
+ # npm view dsh-project-mcp-manager versions)
60
+ dsh plugin --profile web add dsh-project-mcp-manager@0.4.2
61
+ ```
62
+
63
+ **Option 2: install directly with pnpm** (equivalent to option 1):
64
+
65
+ ```powershell
66
+ # dshHome defaults to %USERPROFILE%\.dsh (uses $DSH_HOME if set)
67
+ cd $env:USERPROFILE\.dsh\profiles\web
68
+ pnpm add dsh-project-mcp-manager@latest
69
+ ```
70
+
71
+ **Option 3: local development install** (a junction that live-syncs your
72
+ source, so code changes take effect immediately):
73
+
74
+ ```powershell
75
+ cd $env:USERPROFILE\.dsh\profiles\web
76
+ pnpm add link:<path-to-your-dsh-mcp-project-source> # e.g. D:\dev\dsh-mcp-project
77
+ ```
78
+
79
+ > **dsh ≥ 0.1.2 note**: whether the plugin loads depends on the profile's
80
+ > `dsh.profile.bundles` list, and a plain `pnpm add link:` does **not** add the
81
+ > package to it. Options 1 and 2 reconcile it automatically; if you ran pnpm by
82
+ > hand, run any `dsh plugin --profile web list` once (or check
83
+ > `dsh --profile web --dump-config` for a `dsh-project-mcp-manager` row) to
84
+ > trigger the bundle reconcile.
85
+
86
+ **Upgrading / pinning versions**: re-run the `add` command from option 1 with
87
+ the desired version suffix — `@latest` upgrades to the newest release, `@0.4.2`
88
+ pins to a specific version.
89
+
90
+ ## Build & test
91
+
92
+ ```powershell
93
+ pnpm install
94
+ pnpm run build # tsc → lib/
95
+ pnpm test # node test/test-model.mjs / test-mcp-file / test-json-file / test-json-write / test-registry / test-cli
96
+ ```
97
+
98
+ ## How it works
99
+
100
+ - **Project discovery**: the `session.header.cwd` of an active agent session,
101
+ plus the dsh process start directory → walk up to the nearest ancestor
102
+ containing `.git` as the project root (falls back to the directory itself
103
+ when there is no `.git`).
104
+ - **Mounting**: each `(project, serverName)` pair in the project layers mounts
105
+ one `@deepseek-ai/dsh-mcp-client` instance (`ctx.plugin`) on the host ctx and
106
+ registers it into the global tool layer; multiple sessions inside the same
107
+ project share a single connection. **Every user-layer row mounts exactly one
108
+ instance** (global, independent of the number of projects) — see
109
+ [configuration sources and layers](docs/guide/layers.md).
110
+ - **Hot reload**: chokidar watches each project root (depth 2, ignoring
111
+ node_modules/.git/.hg/.svn), but only edits to the **exact** config files of
112
+ known project roots — `<projectRoot>/.dsh/mcp.yml`,
113
+ `<projectRoot>/.dsh/mcp.json` and `<projectRoot>/.mcp.json` — trigger a full
114
+ reconciliation after a 150 ms debounce: added rows are mounted, removed rows
115
+ are unmounted, and config changes are remounted. A second watcher covers the
116
+ user layer as three **exact file paths** — `~/.dsh/mcp.yml`,
117
+ `~/.dsh/mcp.json` and `~/.dsh/profiles/<active profile>/mcp.json` (chokidar
118
+ v5 can deliver an event for a watched missing file when it is created, as
119
+ long as its parent directory exists) — never the home directory at large.
120
+ - **Profile name resolution**: derived from the loader root include's
121
+ `config.path` (`~/.dsh/profiles/<name>/cordis.yml`) or `ctx.baseUrl`, and
122
+ overridable with `DSH_MCP_PROFILE=<name>`; when it cannot be resolved the
123
+ profile layer is not read (the other layers still are).
124
+ - **Effective names**: when the original `serverName` is unique across the
125
+ whole catalog (host global rows + all project rows) it keeps its name; on a
126
+ conflict **project rows** are renamed to `p<first 6 hex chars of
127
+ sha256(project root)>_<original name>` (truncated to 32 characters,
128
+ deterministic and independent of mount order) to avoid the serverName
129
+ reservation conflicts that `dsh-mcp-client` makes per process root. Global
130
+ rows (profile patch lines and user-layer rows) participate in occupancy
131
+ determination but are never renamed. Model-visible tool names are built from
132
+ the **effective** server name and the MCP tool's own name
133
+ (`mcp__<effectiveServerName>__<toolName>`), which may differ from the
134
+ `serverName` written in the file.
135
+ - **Session visibility**: when an agent is created, its session cwd resolves to
136
+ a project, and `tools.restrict({ deny })` is applied to that agent to deny
137
+ every project server except those of the session's own project, plus the
138
+ global servers suppressed by the project's own rows; a session without a cwd
139
+ falls back to the owner project (subagents), then to the project containing
140
+ the dsh process cwd. Released when the session is destroyed.
141
+
142
+ ## Security boundary
143
+
144
+ `stdio` lines in `.dsh/mcp.yml`, `.dsh/mcp.json` and `.mcp.json` spawn their
145
+ `command` inside the dsh host process — config files are **executable code
146
+ carriers**, so only add them in projects you trust. The user layers
147
+ (`~/.dsh/mcp.yml`, `~/.dsh/mcp.json`, the profile json) are executable code
148
+ carriers too, they just belong to your own machine: user-layer rows mount
149
+ **globally** (one host-level connection, visible to every project) and are no
150
+ longer fanned out per project. Lines that fail to mount or are invalid are
151
+ skipped with a warning and do not affect other servers. Claude user-state
152
+ monoliths such as `~/.claude.json` (mixing credentials with project history)
153
+ are **no longer read at all** as of v0.4.0.