@auggieteo/dsh-mcp-adapter 0.3.0 → 0.4.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/CHANGELOG.md CHANGED
@@ -4,6 +4,30 @@ All notable changes to this project are documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [v0.4.1] - 2026-09-28
8
+
9
+ ### Fixed
10
+
11
+ - The Host entry no longer dies on start with `Error: cannot get property "config" without inject`. v0.4.0 read the Adapter's Loader-entry Config from `ctx.config`, but cordis 4 has no `config` service or accessor — the context proxy refuses any property the plugin did not declare in `inject`, so every DSH start that mounted the `mcp-adapter` entry threw inside `createMcpConfigScope` and the Adapter never came up. The validated Config is the second `apply` argument (`apply(ctx, config)`, the shape the shipped DSH plugins use), and its volatile fields are references the Loader commits through in place, so the scope stays live for the whole activation. `createMcpConfigScope(ctx, config)` now takes it explicitly; `test/settings.test.js` pins the contract against a real cordis fiber and its fakes refuse a `config` context property the way the proxy does. [ADR 0010](docs/adr/0010-loader-entry-config-model.md) records the correction.
12
+
13
+ ## [v0.4.0] - 2026-09-27
14
+
15
+ ### Added
16
+
17
+ - One-time legacy import: after the Loader settles on DSH 0.1.7, a still-untouched `mcp-adapter` entry adopts the legacy `mcp:` section of the profile's `settings.yaml` (validated, then written through the settings service once, with a `mcp-adapter.legacy-imported` marker in the profile home so clearing every Server never resurrects the old list). Failures warn with manual copy instructions and never throw. `test/legacy-import.test.js`.
18
+
19
+ ### Changed
20
+
21
+ - **Breaking: DSH 0.1.7 only.** The settings model the Adapter was built on is gone in 0.1.7 — `ctx.settings.register(namespace, schema)` was replaced by profile Loader-entry Config — and carrying the old generations alongside it would have meant two parallel settings subsystems, so support drops to DSH `0.1.7-rc.1` and newer (peer `@deepseek-ai/dsh-settings` `^0.1.7-rc.1`, `@deepseek-ai/schemastery` `^3.18.4`; stay on v0.3.x for older harnesses). The host half now exports `Config` with `mcpServers` and `skillInstall` marked volatile, reads live values off `ctx.config` (committed without a fiber restart via `loader/volatile-update`), and writes through `ctx.settings`; `createMcpConfigScope` keeps the `{ get, watch, update, mutate }` surface all consumers use. The settings namespace identity is the Loader entry id: `mcp-adapter` (was `mcp`). `src/host/index.js`, [ADR 0010](docs/adr/0010-loader-entry-config-model.md).
22
+ - The client reads the entry through the `configForms` service (`configForms.get('mcp-adapter')` plus the shared `describe()` mirror) instead of the removed `settingsScope`; the `settings.section` page, the `remote.settings` positional wire, and its `{ ok, value | error }` envelope are unchanged. An entry whose mirrored value fails the schema now shows an explicit invalid state instead of a permanent spinner.
23
+ - Cross-field validation moved from before-persist (the removed settings provider hook) to Host consumption: a Config section that passes schemastery but violates the transport rules serves no Servers with one deduplicated warning, so hand-edited profile Config cannot brick the manager.
24
+ - `test/client-apply.test.js` collapses to a single 0.1.7 generation fake pinning the `mcp-adapter` entry id on the wire.
25
+
26
+ ### Removed
27
+
28
+ - The DSH 0.1.1-rc.2 `connection.api.settings` client fallback, the 0.1.2/0.1.5 generation fakes, and the dead `@deepseek-ai/dsh-client-runtime` entry (last published for 0.1.1) from `dsh.client.inject`.
29
+ - The `installMcpSettings` export (replaced by `createMcpConfigScope`); the nav-icon patch script now targets 0.1.7's renamed `*Medium` icon members with `IconLinkOutlineMedium` as the default.
30
+
7
31
  ## [v0.3.0] - 2026-09-13
8
32
 
9
33
  ### Changed
package/CONTEXT.md CHANGED
@@ -29,9 +29,13 @@ A per-server toggle that lets that server's tool calls run without asking the us
29
29
  _Avoid_: trust, bypass, whitelist
30
30
 
31
31
  **Config**:
32
- The standard `mcpServers`-shaped JSON stored in the adapter's DSH settings namespace, editable in Settings > MCP. Global: one list for every DSH session.
32
+ The standard `mcpServers`-shaped JSON stored as the Adapter's profile Loader-entry Config (entry id `mcp-adapter`), editable in Settings > MCP. Global: one list for every DSH session.
33
33
  _Avoid_: manifest, registry file
34
34
 
35
+ **Entry Config**:
36
+ The DSH 0.1.7 configuration model: a plugin's `Config` schema validated by the cordis Loader and handed to the plugin's `apply` as its second argument — cordis has no `config` service, so `ctx.config` is not readable — persisted in the profile composition. Fields marked volatile are edited live through the settings forms and commit into the running plugin's references without a restart.
37
+ _Avoid_: settings namespace, settings document
38
+
35
39
  **Lazy Lifecycle**:
36
40
  The connection policy: a server connects on first use and disconnects after an idle timeout. Opposite of eager connect at startup.
37
41
  _Avoid_: on-demand boot, cold start
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  DSH plugin connecting the agent to MCP servers through one mcp proxy tool and a Settings > MCP page.
8
8
 
9
- A [Deepseek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH) plugin that mirrors the major features of [pi-mcp-adapter](https://github.com/nicobailon/pi-mcp-adapter): one token-efficient `mcp` proxy tool over every configured server, optional per-tool promotion to native DSH tools, and configurable Server lifecycles (`lazy` by default). Servers are added and configured in the DSH web UI under **Settings > MCP**; configuration persists in the DSH settings document (`$DSH_HOME/settings.yaml`, namespace `mcp`).
9
+ A [Deepseek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH) plugin that mirrors the major features of [pi-mcp-adapter](https://github.com/nicobailon/pi-mcp-adapter): one token-efficient `mcp` proxy tool over every configured server, optional per-tool promotion to native DSH tools, and configurable Server lifecycles (`lazy` by default). Servers are added and configured in the DSH web UI under **Settings > MCP**; configuration persists as the Adapter's DSH profile entry config (entry `mcp-adapter`).
10
10
 
11
11
  Vocabulary lives in [CONTEXT.md](./CONTEXT.md). Design decisions live in [docs/adr/](./docs/adr/). Work is tracked as [GitHub issues](https://github.com/auggie246/dsh-mcp-adapter/issues), and released changes are listed in [CHANGELOG.md](./CHANGELOG.md).
12
12
 
@@ -50,7 +50,7 @@ Vocabulary lives in [CONTEXT.md](./CONTEXT.md). Design decisions live in [docs/a
50
50
 
51
51
  [pi-mcp-adapter](https://github.com/nicobailon/pi-mcp-adapter) established a workflow for driving many MCP servers from one agent: a single token-efficient proxy tool, lazy connections, and per-tool promotion. This plugin brings that workflow to [Deepseek Harness](https://github.com/deepseek-ai/deepseek-harness) natively.
52
52
 
53
- Server Config is stored in the Adapter's `mcp` namespace of the DSH settings service (persisted to `$DSH_HOME/settings.yaml`, hot-reloaded, revision-fenced) rather than pi-mcp-adapter's layered `.mcp.json` files, while still adopting the standard `mcpServers`-shaped JSON so configs paste over from pi, Claude Desktop, Cursor, or VS Code. See [ADR 0002](./docs/adr/0002-config-in-dsh-settings-namespace.md) for the reasoning. The v1 end-to-end verification lives in [docs/verification/v1-e2e.md](./docs/verification/v1-e2e.md).
53
+ Server Config is stored as the Adapter's DSH Loader-entry Config (the `Config` schema the host module exports, persisted in the active profile composition, hot-reloaded through volatile updates, revision-fenced) rather than pi-mcp-adapter's layered `.mcp.json` files, while still adopting the standard `mcpServers`-shaped JSON so configs paste over from pi, Claude Desktop, Cursor, or VS Code. See [ADR 0010](./docs/adr/0010-loader-entry-config-model.md) for the reasoning. The v1 end-to-end verification lives in [docs/verification/v1-e2e.md](./docs/verification/v1-e2e.md).
54
54
 
55
55
  ## Install
56
56
 
@@ -66,11 +66,10 @@ Then restart DSH. The Settings panel (`⌘,` / sidebar foot) gains an **MCP** se
66
66
 
67
67
  | DSH release | Support |
68
68
  | --- | --- |
69
- | 0.1.2-rc.1 and newer 0.1.x | Supported. Settings writes go through the typed `remote.settings` face mounted by the `@deepseek-ai/dsh-api-remotes` client bundle. |
70
- | 0.1.1-rc.2 | Supported. The client falls back to the legacy `connection.api.settings` face when its module applies. |
71
- | Anything else | Unsupported. The client fails with an explicit "No DSH settings write API" error instead of a crash. |
69
+ | 0.1.7-rc.1 and newer 0.1.x | Supported. Configuration is the plugin's Loader-entry Config: the Host reads the validated Config cordis passes to `apply` (there is no `ctx.config`) plus its live-committed volatile updates, writes go through the settings service, and the page reads the `configForms` service and writes through the `remote.settings` face mounted by the `@deepseek-ai/dsh-api-remotes` client bundle. |
70
+ | Older 0.1.x | Unsupported. Those releases require the registered-namespace settings model this plugin used through v0.3.x; upgrade DSH or stay on that release line. |
72
71
 
73
- Both generations run the same host seam: the peer dependency accepts `@deepseek-ai/dsh-settings` `^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1`, and the Settings page, commands, and tools behave identically on either. The Adapter picks the right face when its client module applies. See [ADR 0009](./docs/adr/0009-dual-generation-settings-api.md).
72
+ Upgrading from v0.3.x on DSH 0.1.7: at the first start after the upgrade, the Adapter imports the legacy `mcp:` section of the profile's `settings.yaml` into the `mcp-adapter` entry once. A `mcp-adapter.legacy-imported` marker records the import; anything the import refuses to accept (for example an invalid section) stays in `settings.yaml.imported` and a Host warning names the manual copy path. See [ADR 0010](./docs/adr/0010-loader-entry-config-model.md).
74
73
 
75
74
  ### Local development
76
75
 
@@ -113,37 +112,40 @@ The Host registers three human commands. A bare `/mcp` or `/mcp status` prints o
113
112
 
114
113
  ### Config
115
114
 
116
- The Host registers one live DSH settings namespace, `mcp`. Its user layer lives under `mcp:` in `$DSH_HOME/settings.yaml`; the Settings > MCP page owns normal edits.
115
+ The Adapter's global Config is its DSH Loader-entry Config: the `Config` schema the host module exports, persisted in the active profile composition and validated when the entry loads. The Settings > MCP page owns normal edits; for manual edits the entry looks like this in the profile's patch layer:
117
116
 
118
117
  ```yaml
119
- mcp:
120
- mcpServers:
121
- filesystem:
122
- command: npx
123
- args: [-y, "@modelcontextprotocol/server-filesystem", /workspace]
124
- autoAllow: false
125
- hosted:
126
- url: https://mcp.example.com/api
127
- headers:
128
- Authorization: Bearer example-token
129
- github:
130
- url: https://mcp.example.com/other
131
- auth: oauth
132
- scopes: [repo, read:org]
133
- skillInstall: file
118
+ # cordis.patch.yml — the mcp-adapter entry's config
119
+ - id: mcp-adapter
120
+ name: "@auggieteo/dsh-mcp-adapter"
121
+ config:
122
+ mcpServers:
123
+ filesystem:
124
+ command: npx
125
+ args: [-y, "@modelcontextprotocol/server-filesystem", /workspace]
126
+ autoAllow: false
127
+ hosted:
128
+ url: https://mcp.example.com/api
129
+ headers:
130
+ Authorization: Bearer example-token
131
+ github:
132
+ url: https://mcp.example.com/other
133
+ auth: oauth
134
+ scopes: [repo, read:org]
135
+ skillInstall: file
134
136
  ```
135
137
 
136
138
  Each Server configures exactly one Transport: `command` for stdio, or `url` for streamable HTTP with SSE fallback. Adapter extension fields are `auth` (`headers` by default, or `oauth` for HTTP Servers), `scopes` (OAuth scopes, requires `auth: oauth`), `disabled`, `autoAllow`, `lifecycle` (`lazy`, `eager`, `keep-alive`, or `lazy-keep-alive`; default `lazy`), `idleTimeoutMinutes` (default `10`), and `promotedTools`.
137
139
 
138
140
  The one top-level Adapter field is `skillInstall` (`file` by default, `runtime`, or `off`); see [Agent skill](#agent-skill).
139
141
 
140
- The schema rejects unknown fields and invalid transport combinations before they reach `$DSH_HOME/settings.yaml`. Each `env` and `headers` value has the DSH `secret` schema role. Wire views retain each key and redact its value.
142
+ Schemastery validates the shape when the entry loads, and the Adapter enforces the full rule set — unknown fields, transport exclusivity, scope and lifecycle sanity — every time it serves the Config: a section that fails validation serves no Servers with one Host warning, never a partially applied or bricked configuration. Each `env` and `headers` value has the DSH `secret` schema role. Wire views retain each key and redact its value.
141
143
 
142
144
  #### Workspace Config layer
143
145
 
144
- The Host also reads `.dsh/mcp.json` under its workspace root and merges it over the global namespace at resolve time. A workspace Server entry with the same name replaces the global entry wholesale; workspace-only names are added; global-only names pass through. Disable or override a Server from the workspace by defining it there with `disabled: true`.
146
+ The Host also reads `.dsh/mcp.json` under its workspace root and merges it over the global entry Config at resolve time. A workspace Server entry with the same name replaces the global entry wholesale; workspace-only names are added; global-only names pass through. Disable or override a Server from the workspace by defining it there with `disabled: true`.
145
147
 
146
- The workspace file uses the same `mcpServers` shape and validation as the global Config. Unknown top-level keys (only `mcpServers` is allowed) or an invalid Server entry reject the whole layer, which fails closed to the global-only Config with a Host warning; a missing file is an empty layer. Values are never environment-variable-interpolated. The Adapter never writes the workspace file: page edits, imports, and API writes all stay on the global namespace, which the workspace file overrides. See [ADR 0005](./docs/adr/0005-per-workspace-config.md).
148
+ The workspace file uses the same `mcpServers` shape and validation as the global Config. Unknown top-level keys (only `mcpServers` is allowed) or an invalid Server entry reject the whole layer, which fails closed to the global-only Config with a Host warning; a missing file is an empty layer. Values are never environment-variable-interpolated. The Adapter never writes the workspace file: page edits, imports, and API writes all stay on the global entry Config, which the workspace file overrides. See [ADR 0005](./docs/adr/0005-per-workspace-config.md).
147
149
 
148
150
  ### Settings page
149
151
 
@@ -153,7 +155,7 @@ Servers whose Config comes from the workspace `.dsh/mcp.json` show a `workspace`
153
155
 
154
156
  JSON import accepts the standard `{ "mcpServers": { ... } }` shape. Import replaces matching Server entries and preserves Servers absent from the import.
155
157
 
156
- Every page write carries the latest namespace revision. Field edits use path mutations.
158
+ Every page write carries the latest entry revision. Field edits use path mutations.
157
159
 
158
160
  ### Server lifecycle
159
161
 
@@ -224,7 +226,7 @@ The installed file is never overwritten. If `$DSH_HOME/skills/mcp-adapter/SKILL.
224
226
 
225
227
  ## Architecture
226
228
 
227
- The package ships two faces. The host half (`src/host/`) is plain ESM with no build step; it registers the settings namespace, the manager, the proxy tool, promotions, commands, OAuth services, and the bundled skill. The client half (`src/client/`) is JSX bundled by esbuild (`build.mjs`) into `lib/client.js`, a loader-compatible bundle shaped for `window.__ModuleLoader__.load`. The client declares its service dependencies through `package.json`'s `dsh.client.inject`; `cordis.patch.yml` inserts the host composition row on install.
229
+ The package ships two faces. The host half (`src/host/`) is plain ESM with no build step; it exports the Loader-entry `Config` schema and installs the manager, the proxy tool, promotions, commands, OAuth services, and the bundled skill. The client half (`src/client/`) is JSX bundled by esbuild (`build.mjs`) into `lib/client.js`, a loader-compatible bundle shaped for `window.__ModuleLoader__.load`. The client declares its service dependencies through `package.json`'s `dsh.client.inject`; `cordis.patch.yml` inserts the host composition row on install.
228
230
 
229
231
  The host exposes one Connection RPC channel, `/mcp-adapter`, with endpoints `status`, `catalog`, `overview`, `layers`, `reconnect`, `oauth-status`, `oauth-login`, and `oauth-logout`. The Settings page polls `overview` and `layers` for detached snapshots (a `layers` refresh also re-reads the workspace layer); `reconnect` restarts one named Server; the OAuth endpoints drive and report the per-Server sign-in state.
230
232
 
@@ -269,7 +271,7 @@ CHANGELOG.md released changes
269
271
 
270
272
  Feel free to dive in! [Open an issue](https://github.com/auggie246/dsh-mcp-adapter/issues/new) or submit PRs; questions go through the issue tracker.
271
273
 
272
- - Run `pnpm test` before submitting. The suite covers the host, the client settings controller, and the dual-generation settings faces (`test/client-apply.test.js`).
274
+ - Run `pnpm test` before submitting. The suite covers the host, the client settings controller, the 0.1.7 entry-config wire (`test/client-apply.test.js`), and the legacy `settings.yaml` import (`test/legacy-import.test.js`).
273
275
  - New project vocabulary belongs in [CONTEXT.md](./CONTEXT.md); design decisions get an [ADR](./docs/adr/) before or with the change.
274
276
 
275
277
  ## License
@@ -5,3 +5,5 @@ Server Config is stored in the Adapter's `mcp` namespace of the DSH settings ser
5
5
  Considered options: pi's multi-file merge chain (`~/.config/mcp/mcp.json` → agent dirs → workspace `.mcp.json`, plus cross-tool `imports`) was rejected because DSH users interact through the web UI, not dotfiles; a single global namespace with a Settings > MCP form matches how every other DSH settings surface works, and gives us validation, conflict detection, and live update events for free.
6
6
 
7
7
  Consequences: per-workspace servers and config-file imports become a later feature layered on top of the namespace (a workspace source merged at resolve time), not a change to where the global Config lives. The settings page needs an explicit JSON import action, since there is no file to copy-paste into.
8
+
9
+ *Amended (0.1.7):* DSH 0.1.7 replaced the settings service's registered namespaces with profile Loader-entry Config, so global Server Config now persists in the active profile composition under the `mcp-adapter` entry rather than an `mcp:` section of `settings.yaml`. The decisions that outlive the seam — standard `mcpServers`-shaped JSON, one global list edited through Settings > MCP, workspace sources layered at resolve time — all hold; see [ADR 0010](./0010-loader-entry-config-model.md).
@@ -11,3 +11,5 @@ Considered options: shipping a 0.1.2-only release and telling 0.1.1-rc.2 users t
11
11
  Consequences: the `apply`-level test suite (`test/client-apply.test.js`) compiles the real client entry and drives both fake generations, locking the fallback contract. When DSH drops 0.1.1-rc.2 support, the fallback branch, the legacy fake, and the README note are the removal surface — the controller and host need no changes.
12
12
 
13
13
  *Widening (0.1.5 check):* the DSH 0.1.2-rc.1 → 0.1.5-rc.2 diff leaves every face this ADR pins on untouched — `packages/settings/settings` and `packages/api/settings-controller` change version strings only, the remotes mount list still includes the settings controller (two remotes added, none removed), and the `connection` service keeps its `rpc` face while refactoring reconnect internals. The one rename in range (`CommandInputDescriptor.images` → `attachments`) does not touch the Adapter's commands, which declare only `input.hint`. The peer dependency therefore widens to `^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1` with no code change; `test/client-apply.test.js` locks the 0.1.5-rc.1 shape with its own generation fake.
14
+
15
+ *Superseded (0.1.7):* DSH 0.1.7 is the third shape this ADR anticipated and hard-failed on: `ctx.settings.register` is gone, configuration is the plugin's Loader-entry Config, and the client reads through the `configForms` service while the `remote.settings` wire keeps the positional shape and flat envelope pinned here. The 0.1.1 fallback and its fake are deleted; the surviving `remote.settings` wrapper now addresses the `mcp-adapter` entry id. See [ADR 0010](./0010-loader-entry-config-model.md).
@@ -0,0 +1,15 @@
1
+ # Configuration is the plugin's Loader-entry Config (DSH 0.1.7)
2
+
3
+ DSH 0.1.7 removed the settings model the Adapter was built on. `ctx.settings.register(namespace, schema)` — the `SettingsProvider` seam where plugins registered their own namespace — is gone: `@deepseek-ai/dsh-settings` now exports a single `SettingsForms` service that projects the cordis **Loader entries' Config schemas** into editable profile forms (verified against the 0.1.7-rc.2 packages). Host plugins `export const Config`; the Loader validates the entry's profile config and cordis hands the validated object to the plugin as the **second `apply` argument** (`new runtime.callback(ctx, fiber.config)`) — there is no `config` service or accessor, so reading `ctx.config` throws `cannot get property "config" without inject`; fields marked `.volatile()` (schemastery `~3.18.4`) resolve to cosmokit volatile references, and a config edit touching only volatile fields is committed into the running fiber's references by cordis-plugin-loader without a restart, emitting `loader/volatile-update` — everything else restarts the entry. `SettingsForms` reads and writes by **profile entry id** (ours is `mcp-adapter`, the id `cordis.patch.yml` registers), refuses writes to non-volatile paths, and `configure({ auto })` decides whether the Settings shell auto-generates a form page for the entry. On the client `settingsScope` is replaced by the `configForms` service — `get(entryId)` answers a per-entry form controller (`{ status, value, base, user, revision, writable, mode }` snapshots, `subscribe`, `mutate`) and `describe()` a shared document mirror — while the `remote.settings` wire face (positional args, flat `{ ok, value | error }` envelope, `settings/document-updated` invalidation) and the `settings.section` slot (now owned by `dsh-client-ui-settings-general`) survive. This supersedes [ADR 0002](./0002-config-in-dsh-settings-namespace.md) (where global Config lives) and completes [ADR 0009](./0009-dual-generation-settings-api.md) (whose predicted third shape is exactly this one).
4
+
5
+ The Adapter's Config becomes its entry Config: `src/host/index.js` exports `Config = z.object({ mcpServers: …volatile(), skillInstall: …volatile() })`. Both marks sit on the top-level fields because schemastery rejects volatile fields below a dict or beneath another volatile field, and SettingsForms only accepts writes on schema-declared volatile paths. `MCP_SETTINGS_NAMESPACE` changes from `'mcp'` to `'mcp-adapter'`: under 0.1.7 the namespace identity is the entry id, so the client's `configForms.get()`, the `remote.settings` writes, and the host's `settings.update()` all address the entry. `createMcpConfigScope(ctx, config)` wraps the entry Config in the same `{ get, watch, update, mutate }` surface the old scope provided — `get()` unwraps the volatile refs off the Config object cordis passed to `apply`, `watch` notifies `(next, previous)` on `loader/volatile-update` when the value actually changed, and the write methods forward to `ctx.get('settings')` — so the workspace layer, manager, promotions, OAuth, and commands consume the port unchanged. The scope holds the object for the whole activation, which is sound because a volatile commit writes through the references inside it (cosmokit freezes them; the shared `Symbol.for('cosmokit.volatile.write')` member is the only writer) and any ordinary edit replaces `fiber.config` and restarts the entry, so a fresh `apply` receives the new object.
6
+
7
+ Considered options: keeping any legacy generation alongside 0.1.7 was rejected because the model replaced both faces at once — supporting 0.1.2/0.1.5 too means two parallel settings subsystems in Host and client (register-scope plus volatile-Config, `settingsScope` plus `configForms`) and doubled generation fakes; the release is 0.1.7-only, deleting the `connection.api.settings` fallback, the legacy fakes, and the dead `@deepseek-ai/dsh-client-runtime` entry (last published for 0.1.1) from `dsh.client.inject`. Restarting the whole entry on every form edit (the platform default for non-volatile fields) was rejected for `mcpServers` because a single keystroke in a Server dialog would drop every live connection; live volatile commits plus `loader/volatile-update` keep the manager reconciling exactly as it watched the old scope. The alternative to the scope facade — rewriting every consumer against the entry Config refs — was rejected as a wide diff with no behavior benefit.
8
+
9
+ Cross-field validation had to move. The old `SettingsProvider` ran `validateMcpSettings` before every persist; the Config write path validates only the schema, so structure errors now surface as a failed entry start, and `validateMcpSettings` runs inside the scope's `get()` instead: a section that passes the schema but fails the cross-field rules serves an empty server list with one deduplicated Host warning, so a hand-edited profile can never brick the connection manager. The client writes keep the direct `remote.settings` wrapper rather than the form controller's queue so conflict messages still reach the page as text, and the controller reports a mirrored-but-schema-invalid entry (whose form stays `loading` by contract) as an `invalid` state instead of spinning.
10
+
11
+ Legacy data needs one explicit bridge. 0.1.7 renames the profile's `settings.yaml` to `settings.yaml.imported` and imports only sections whose name matches an entry id — the Adapter's legacy `mcp` section under its `mcp-adapter` entry would be silently stranded. After the Loader settles (the platform import runs first and skips the mismatched name), if the entry is untouched — no user section, no non-default servers — the section is validated against the Config schema and written once through `settings.update`, and an `mcp-adapter.legacy-imported` marker file in the profile home records the import so a later deliberate "remove every Server" never resurrects the old list. Every failure warns with manual copy instructions and never throws.
12
+
13
+ Consequences: peers widen to `@deepseek-ai/dsh-settings` `^0.1.7-rc.1` and `@deepseek-ai/schemastery` `^3.18.4`; `yaml` becomes a runtime dependency of the migration module. Global Servers persist in the active profile composition (through Settings > MCP or the profile document) instead of a settings document, and README/CONTEXT vocabulary follows. `test/client-apply.test.js` collapses to a single 0.1.7 generation fake pinning ns `mcp-adapter` on the wire; `test/legacy-import.test.js` locks the migration. The nav-icon patch script targets 0.1.7's renamed `*Medium` icon members: its `MODELS_BRANCH` pattern now pins only the `Icon` prefix and the default icon is `IconLinkOutlineMedium`.
14
+
15
+ Correction (v0.4.1): this ADR originally said the Loader validates the entry Config *into `ctx.config`*, and the shipped v0.4.0 entry acted on that — it died on start with `cannot get property "config" without inject`. cordis 4 resolves every context property through a proxy that refuses anything the plugin did not declare in `inject`, and there is no `config` service or accessor to declare, so `ctx.config` is unreachable by construction. The validated Config is the second `apply` argument (`apply(ctx, config)`), which is how the shipped DSH plugins take it. `createMcpConfigScope` now receives that argument and `src/host/index.js` threads it through; the test fakes refuse a `config` context property the way the cordis proxy does, and a probe mounted through a real cordis fiber pins the contract, so the assumption cannot come back unnoticed.
package/lib/client.js CHANGED
@@ -43,8 +43,9 @@ module.exports = __toCommonJS(index_exports);
43
43
  var import_react = __toESM(require("react"), 1);
44
44
 
45
45
  // src/client/settings-controller.js
46
- var MCP_SETTINGS_NAMESPACE = "mcp";
46
+ var MCP_SETTINGS_NAMESPACE = "mcp-adapter";
47
47
  var MCP_RPC_CHANNEL = "/mcp-adapter";
48
+ var INVALID_SETTINGS_MESSAGE = "The saved MCP configuration failed validation. Fix or remove the invalid fields in the profile, then reopen Settings.";
48
49
  var SERVER_FIELDS = /* @__PURE__ */ new Set([
49
50
  "command",
50
51
  "args",
@@ -236,6 +237,12 @@ function secretRowOps(serverName, field, rows, existingKeys) {
236
237
  function overviewEmpty() {
237
238
  return { status: { servers: [] }, catalog: { servers: [] } };
238
239
  }
240
+ function projectSettingsStatus(settings, document2) {
241
+ if (settings.status !== "loading") return settings;
242
+ const row = document2.view?.namespaces?.find((entry) => entry.ns === MCP_SETTINGS_NAMESPACE);
243
+ if (row === void 0 || row.value === void 0) return settings;
244
+ return { ...settings, status: "invalid", error: { message: INVALID_SETTINGS_MESSAGE } };
245
+ }
239
246
  function serverSource(layers, name) {
240
247
  return layers?.source?.[name] === "workspace" ? "workspace" : "global";
241
248
  }
@@ -275,9 +282,10 @@ var McpSettingsController = class {
275
282
  this.getSnapshot = () => this.snapshot;
276
283
  }
277
284
  projection() {
285
+ const settingsDocument = this.describe.getSnapshot();
278
286
  return {
279
- settings: this.scope.getSnapshot(),
280
- settingsDocument: this.describe.getSnapshot(),
287
+ settings: projectSettingsStatus(this.scope.getSnapshot(), settingsDocument),
288
+ settingsDocument,
281
289
  overview: this.overview,
282
290
  layers: this.layers,
283
291
  oauthStatuses: this.oauthStatuses,
@@ -1392,10 +1400,8 @@ function McpSettingsPage({ controller }) {
1392
1400
  function createSettingsApi(ctx) {
1393
1401
  const remoteSettings = readRemoteSettings(ctx);
1394
1402
  if (remoteSettings !== void 0) return remoteSettingsApi(remoteSettings);
1395
- const connectionSettings = ctx.connection?.api?.settings;
1396
- if (connectionSettings !== void 0) return connectionSettings;
1397
1403
  throw new Error(
1398
- "No DSH settings write API: expected ctx.remote.settings (DSH 0.1.2+) or ctx.connection.api.settings (DSH 0.1.1-rc.2). Upgrade @auggieteo/dsh-mcp-adapter to a release supporting this harness."
1404
+ "No DSH settings write API: expected the ctx.remote.settings namespace (DSH 0.1.7 with @deepseek-ai/dsh-api-remotes). Upgrade @auggieteo/dsh-mcp-adapter to a release supporting this harness."
1399
1405
  );
1400
1406
  }
1401
1407
  function readRemoteSettings(ctx) {
@@ -1523,10 +1529,10 @@ function installMcpSettingsStyles() {
1523
1529
  }
1524
1530
 
1525
1531
  // src/client/index.jsx
1526
- var inject = ["slots", "settingsScope", "connection", "remote"];
1532
+ var inject = ["slots", "configForms", "connection", "remote"];
1527
1533
  function apply(ctx) {
1528
- const scope = ctx.settingsScope.bind({ namespace: MCP_SETTINGS_NAMESPACE });
1529
- const describe = ctx.settingsScope.describe();
1534
+ const scope = ctx.configForms.get(MCP_SETTINGS_NAMESPACE);
1535
+ const describe = ctx.configForms.describe();
1530
1536
  const controller = new McpSettingsController({
1531
1537
  scope,
1532
1538
  describe,