@auggieteo/dsh-mcp-adapter 0.4.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,12 @@ 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
+
7
13
  ## [v0.4.0] - 2026-09-27
8
14
 
9
15
  ### Added
package/CONTEXT.md CHANGED
@@ -33,7 +33,7 @@ The standard `mcpServers`-shaped JSON stored as the Adapter's profile Loader-ent
33
33
  _Avoid_: manifest, registry file
34
34
 
35
35
  **Entry Config**:
36
- The DSH 0.1.7 configuration model: a plugin's `Config` schema validated by the cordis Loader into `ctx.config`, persisted in the profile composition. Fields marked volatile are edited live through the settings forms and commit into the running plugin without a restart.
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
37
  _Avoid_: settings namespace, settings document
38
38
 
39
39
  **Lazy Lifecycle**:
package/README.md CHANGED
@@ -66,7 +66,7 @@ Then restart DSH. The Settings panel (`⌘,` / sidebar foot) gains an **MCP** se
66
66
 
67
67
  | DSH release | Support |
68
68
  | --- | --- |
69
- | 0.1.7-rc.1 and newer 0.1.x | Supported. Configuration is the plugin's Loader-entry Config: the Host reads `ctx.config` and 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. |
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
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. |
71
71
 
72
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).
@@ -1,13 +1,15 @@
1
1
  # Configuration is the plugin's Loader-entry Config (DSH 0.1.7)
2
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 into `ctx.config`; 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).
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
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)` wraps the fiber in the same `{ get, watch, update, mutate }` surface the old scope provided — `get()` unwraps the volatile refs off `ctx.config`, `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.
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
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 `ctx.config` refs — was rejected as a wide diff with no behavior benefit.
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
8
 
9
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
10
 
11
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
12
 
13
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@auggieteo/dsh-mcp-adapter",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "DSH plugin connecting the agent to MCP servers through one mcp proxy tool and a Settings > MCP page.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/host/index.js CHANGED
@@ -15,17 +15,19 @@ import { installWorkspaceLayer } from './workspace-config.js'
15
15
  export const name = 'dsh-mcp-adapter'
16
16
 
17
17
  // 0.1.7 model: the Adapter's configuration is its Loader-entry Config (the
18
- // `Config` schema below), always readable from `ctx.config`; Timer is only
19
- // needed by the connection manager, and Settings by writes and the page.
18
+ // `Config` schema below). cordis validates it and passes it to `apply` as the
19
+ // second argument — there is no `config` service, so `ctx.config` is not a
20
+ // thing. Timer is only needed by the connection manager, and Settings by
21
+ // writes and the page.
20
22
  export const inject = []
21
23
 
22
24
  export const Config = McpConfigSchema
23
25
 
24
- export function apply(ctx) {
26
+ export function apply(ctx, config) {
25
27
  // The bundled agent skill is independent of every other Adapter feature: a
26
28
  // deployment without the skills service simply gets no skill entry.
27
29
  // `skillInstall` is read once here, from the entry Config.
28
- const scope = createMcpConfigScope(ctx)
30
+ const scope = createMcpConfigScope(ctx, config)
29
31
  ctx.effect(() => () => scope.dispose(), 'dsh-mcp-adapter: config scope')
30
32
  const skillInstall = scope.get().skillInstall
31
33
  const layeredScope = installWorkspaceLayer(ctx, scope)
@@ -241,8 +241,16 @@ export function readMcpConfig(config = {}, onInvalid) {
241
241
  * updates (`loader/volatile-update`) whose value actually changed;
242
242
  * - `update(patch)` / `mutate(ops)` write through the settings service onto
243
243
  * this entry and throw when no settings service is mounted.
244
+ *
245
+ * `config` is the validated Config cordis hands the plugin as the second
246
+ * `apply` argument. Schema-declared volatile fields are stable references
247
+ * inside that object and the Loader commits edits into them in place, so the
248
+ * object stays live for the whole activation (an ordinary edit restarts the
249
+ * entry instead). It is the only way to read the entry Config: cordis has no
250
+ * `config` service, so touching `ctx.config` throws
251
+ * `cannot get property "config" without inject`.
244
252
  */
245
- export function createMcpConfigScope(ctx, options = {}) {
253
+ export function createMcpConfigScope(ctx, config, options = {}) {
246
254
  const warn = options.warn ?? ((message) => ctx.logger?.warn?.(message))
247
255
  const settingsService = options.settings ?? (() => ctx.get('settings'))
248
256
  const listeners = new Set()
@@ -250,7 +258,7 @@ export function createMcpConfigScope(ctx, options = {}) {
250
258
  let last = readCurrent()
251
259
 
252
260
  function readCurrent() {
253
- return readMcpConfig(ctx.config, (message) => {
261
+ return readMcpConfig(config, (message) => {
254
262
  if (message === warnedInvalid) return
255
263
  warnedInvalid = message
256
264
  warn(`dsh-mcp-adapter: invalid MCP Config, serving no Servers: ${message}`)