@auggieteo/dsh-mcp-adapter 0.1.2 → 0.2.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 ADDED
@@ -0,0 +1,60 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
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
+
7
+ ## [v0.2.1] - 2026-09-06
8
+
9
+ ### Added
10
+
11
+ - `scripts/patch-dsh-settings-nav-icon.mjs`: patches the installed DSH settings shell so the `mcp` nav row in Settings shows a connection icon (`IconLinkOutline16`) instead of the generic settings-gear fallback. The icon mapping is DSH shell chrome that this plugin cannot reach through the `settings.section` slot (it accepts only `id`, `order`, `label`). The insert is idempotent and marked `// dsh-mcp-adapter`; `--revert` restores the original bytes. A DSH upgrade replaces the patched file, so re-run the script afterwards, then restart `dsh web` and refresh the page.
12
+
13
+ ### Changed
14
+
15
+ - Settings > MCP has a single Add Server entry point: the dialog now offers an Input fields mode for one Server and a Paste JSON mode for a standard `mcpServers` object (existing names are replaced), replacing the separate Import JSON button and dialog in both the toolbar and the empty state.
16
+
17
+ ### Fixed
18
+
19
+ - The plugin failed to load on DSH 0.1.2-rc.1 with `failed to apply loader entry … cannot get property "remote.settings" without inject`: the runner mounts the settings namespace as the traced dotted service `remote.settings`, so the plain `ctx.remote?.settings` read is governed and throws for plugins that do not declare it — while declaring it would park the plugin forever on 0.1.1-rc.2, where the namespace is never mounted. The write face is now resolved through the inject-free `ctx.get('remote.settings')`, which works on both generations ([ADR 0009](docs/adr/0009-dual-generation-settings-api.md)).
20
+ - The Add Server and Import JSON dialogs follow the host dialog theme again: the modal card now surfaces on `bg-layer-2` with the shared `--dsw-elevation-prominent` treatment and the overlay mask blurs like the host Modal primitive, instead of the off-palette `bg-overlay` fill and a hardcoded drop shadow.
21
+
22
+ ## [v0.2.0] - 2026-09-05
23
+
24
+ Everything from v0.1.2 through this release: the six deferred v1 features (#8–#12, #14), the dual-generation DSH compatibility fix, and the standard-readme documentation pass.
25
+
26
+ ### Added
27
+
28
+ - OAuth 2.0 authorization code flow with PKCE for remote HTTP Servers (#8): `auth: oauth`, optional `scopes`, sign-in from Settings > MCP or `/mcp-auth`, a local loopback callback, and token records in `~/.dsh/mcp-auth/tokens.json`.
29
+ - OAuth sign-in state surfaced in the MCP settings UI, with Sign in / Sign out per Server and `oauth-status`/`oauth-login`/`oauth-logout` RPC endpoints.
30
+ - Per-workspace Config layering (#10, [ADR 0005](docs/adr/0005-per-workspace-config.md)): `.dsh/mcp.json` in the workspace root merges over the global namespace at resolve time and overrides same-named Servers.
31
+ - MCP resources and prompts through the Proxy Tool (`resources`, `read`, `prompts`, `prompt` actions) plus the `/mcp-prompt` human command (#9).
32
+ - The `/mcp` human command family (#11): `status`, `reconnect`, `enable`, and `disable`.
33
+ - `eager`, `keep-alive`, and `lazy-keep-alive` lifecycle modes alongside `lazy` (#12, [ADR 0004](docs/adr/0004-lifecycle-modes.md)), including keep-alive auto-reconnect after unexpected closes.
34
+ - A bundled `mcp-adapter` agent skill (`skills/mcp-adapter/SKILL.md`) registered as a runtime skill when the DSH `skills` service is present.
35
+ - `layers` Connection RPC endpoint reporting the workspace-vs-global source of each Server's Config.
36
+
37
+ ### Changed
38
+
39
+ - Support DSH 0.1.2-rc.1 and 0.1.1-rc.2 simultaneously: the client resolves the settings write face at apply time (`remote.settings` on 0.1.2-rc.1+, `connection.api.settings` on 0.1.1-rc.2) ([ADR 0009](docs/adr/0009-dual-generation-settings-api.md)); the peer dependency widened to `^0.1.1-rc.2 || ^0.1.2-rc.1`.
40
+ - MCP RPC failure envelopes use the platform's error codes, so error messages reach the UI instead of being rejected by the client-side schema.
41
+ - README restructured to the [standard-readme](https://github.com/RichardLitt/standard-readme) layout, with a Compatibility section, a Security section, and the package description aligned to the spec's short-description limit.
42
+ - Promotion discovery documented as the single Lazy Lifecycle startup exception ([ADR 0003](docs/adr/0003-promotion-discovery-lazy-exception.md)).
43
+
44
+ ### Fixed
45
+
46
+ - Plugin load on DSH 0.1.2-rc.1 crashed the web client with `can't access property "settings", ctx.connection.api is undefined` because the connection service lost its `api` face; the settings write face is now resolved across both harness generations (see Changed).
47
+ - The host imported `settingsNamespace`, an export removed from `@deepseek-ai/dsh-settings` 0.1.2-rc.1; the `mcp` namespace now registers as a plain string, which both releases accept.
48
+
49
+ ## [v0.1.2] - 2026-08-31
50
+
51
+ ### Changed
52
+
53
+ - Restyle the MCP Settings page to the host theme and fix layout overflow.
54
+
55
+ ## [v0.1.1] and earlier
56
+
57
+ Initial releases of the Adapter through v1 issues #1–#7: proxy-first tool surface, Config in the DSH settings namespace, promotion registry, lazy lifecycle, output guard, settings page, and the `/mcp-adapter` RPC channel. See [docs/verification/v1-e2e.md](docs/verification/v1-e2e.md).
58
+
59
+ [v0.2.0]: https://github.com/auggie246/dsh-mcp-adapter/compare/v0.1.2...v0.2.0
60
+ [v0.1.2]: https://github.com/auggie246/dsh-mcp-adapter/releases/tag/v0.1.2
package/README.md CHANGED
@@ -1,14 +1,117 @@
1
1
  # dsh-mcp-adapter
2
2
 
3
- A [Deepseek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH) plugin that connects the DSH agent to MCP servers. It 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 lazy server lifecycles. 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`).
3
+ [![npm version](https://img.shields.io/npm/v/@auggieteo/dsh-mcp-adapter)](https://www.npmjs.com/package/@auggieteo/dsh-mcp-adapter)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
5
+ [![standard-readme compliant](https://img.shields.io/badge/readme%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme)
6
+
7
+ DSH plugin connecting the agent to MCP servers through one mcp proxy tool and a Settings > MCP page.
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`).
10
+
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
+
13
+ ## Table of Contents
14
+
15
+ - [Security](#security)
16
+ - [Background](#background)
17
+ - [Install](#install)
18
+ - [Compatibility](#compatibility)
19
+ - [Local development](#local-development)
20
+ - [Uninstall](#uninstall)
21
+ - [Usage](#usage)
22
+ - [CLI](#cli)
23
+ - [Config](#config)
24
+ - [Workspace Config layer](#workspace-config-layer)
25
+ - [Settings page](#settings-page)
26
+ - [Server lifecycle](#server-lifecycle)
27
+ - [Proxy Tool](#proxy-tool)
28
+ - [Resources and prompts](#resources-and-prompts)
29
+ - [Promotion](#promotion)
30
+ - [OAuth](#oauth)
31
+ - [Agent skill](#agent-skill)
32
+ - [Architecture](#architecture)
33
+ - [Release](#release)
34
+ - [Layout](#layout)
35
+ - [Maintainers](#maintainers)
36
+ - [Thanks](#thanks)
37
+ - [Contributing](#contributing)
38
+ - [License](#license)
39
+
40
+ ## Security
41
+
42
+ - Secret values — every `env` and `headers` entry carries the DSH `secret` schema role — are redacted in every wire view: the Settings page and the `/mcp-adapter` RPC channel see keys and `set` state, never values. Existing secret values stay saved when their value inputs are left blank.
43
+ - The `/mcp-adapter` RPC endpoints never expose Config secrets or SDK objects.
44
+ - OAuth tokens live in `${DSH_HOME:-~/.dsh}/mcp-auth/tokens.json` (directory `0700`, file `0600`, atomic writes), one record per Server, bound to the Server URL they were issued for. The callback listener runs on a local loopback address and validates the OAuth `state` parameter; token material never travels through the RPC channel.
45
+ - Proxy tool output is limited to 50 KiB and 2,000 lines; larger text uses `spillStore` for a full-result reference, and raw structured details over 16 KiB become a compact summary with their own spill reference.
46
+ - Binary resource content is never inlined as base64. It is base64-decoded and written to a sanitized `mcp-resource-` path under the system temp folder.
47
+ - Tool calls ask for DSH approval unless the Server sets `autoAllow: true`.
48
+
49
+ ## Background
50
+
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
+
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).
4
54
 
5
- 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).
55
+ ## Install
56
+
57
+ Requires the `dsh` CLI and a web profile (the browser GUI you run DSH with).
58
+
59
+ ```sh
60
+ dsh plugin --profile web add @auggieteo/dsh-mcp-adapter
61
+ ```
62
+
63
+ Then restart DSH. The Settings panel (`⌘,` / sidebar foot) gains an **MCP** section.
64
+
65
+ ### Compatibility
66
+
67
+ | DSH release | Support |
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. |
72
+
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`, 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).
74
+
75
+ ### Local development
76
+
77
+ ```sh
78
+ pnpm install
79
+ pnpm build # produces lib/client.js (required before install)
80
+ dsh plugin --profile web add /absolute/path/to/dsh-mcp-adapter
81
+ ```
82
+
83
+ During client development, run `pnpm dev` alongside DSH. `dsh-client-hmr` reloads each rebuilt client bundle without a manual refresh.
84
+
85
+ Host changes need a DSH restart. The web profile disables host HMR for plugin rows.
86
+
87
+ ### Uninstall
88
+
89
+ ```sh
90
+ dsh plugin --profile web remove @auggieteo/dsh-mcp-adapter
91
+ ```
6
92
 
7
- ## Status
93
+ ## Usage
8
94
 
9
- The v1 implementation is complete through issues #1–#7. See the [v1 end-to-end verification](./docs/verification/v1-e2e.md). Issues #8–#14 track deferred follow-ups.
95
+ ### CLI
10
96
 
11
- ## Config
97
+ The Host registers three human commands. A bare `/mcp` or `/mcp status` prints one line per Server: name, live state, cached tool count, and the last message when one is present.
98
+
99
+ ```text
100
+ /mcp # status of every Server
101
+ /mcp status filesystem # status of one Server
102
+ /mcp reconnect filesystem # close and relist one Server now
103
+ /mcp disable hosted # keep the Config, stop the Server
104
+ /mcp enable hosted # restore the lazy lifecycle
105
+ /mcp-auth # list every OAuth Server
106
+ /mcp-auth hosted login # start (or print) one OAuth sign-in flow
107
+ /mcp-prompt docs review {"path": "src/index.js"}
108
+ ```
109
+
110
+ `/mcp reconnect` mirrors the `/mcp-adapter` RPC endpoint: it closes the Server's current connection and lists tools so it reconnects. `enable` and `disable` write the global Config's `disabled` flag — the same write as **Settings > MCP** — say so in their answer, and leave the Server entry otherwise untouched. A Server defined by the workspace `.dsh/mcp.json` is refused, because the workspace entry would shadow a global write ([ADR 0005](./docs/adr/0005-per-workspace-config.md)). An unknown subcommand or wrong argument count answers with `Usage: /mcp [status|reconnect|enable|disable] [server]`; an unknown Server answers with its unknown-server error.
111
+
112
+ `/mcp-auth [server] [login|logout|status]` drives the OAuth flow described in [OAuth](#oauth). `/mcp-prompt <server> <prompt> [json-args]` fetches one MCP prompt as described in [Resources and prompts](#resources-and-prompts); empty input, invalid JSON arguments, unknown Servers, and failed fetches answer with error text.
113
+
114
+ ### Config
12
115
 
13
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.
14
117
 
@@ -23,41 +126,74 @@ mcp:
23
126
  url: https://mcp.example.com/api
24
127
  headers:
25
128
  Authorization: Bearer example-token
129
+ github:
130
+ url: https://mcp.example.com/other
131
+ auth: oauth
132
+ scopes: [repo, read:org]
26
133
  ```
27
134
 
28
- Each Server configures exactly one Transport: `command` for stdio, or `url` for streamable HTTP with SSE fallback. Adapter extension fields are `disabled`, `autoAllow`, `lifecycle` (`lazy` only in v1), `idleTimeoutMinutes` (default `10`), and `promotedTools`.
135
+ 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`.
29
136
 
30
137
  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.
31
138
 
32
- ## Settings page
139
+ #### Workspace Config layer
140
+
141
+ 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`.
142
+
143
+ 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).
144
+
145
+ ### Settings page
33
146
 
34
- Open **Settings > MCP** to add, import, edit, reconnect, disable, or delete a Server. The Server list shows live connection state and cached tool counts. The detail panel manages Transport fields, secret values, Auto-allow, the idle timeout, and Promotions.
147
+ Open **Settings > MCP** to add, import, edit, reconnect, disable, or delete a Server. The Server list shows live connection state and cached tool counts. The detail panel manages Transport fields, secret values, Auto-allow, the idle timeout, and Promotions. HTTP Servers pick their HTTP authentication mode (static headers or OAuth 2.0 with PKCE) and, for OAuth, enter the scopes to request.
148
+
149
+ Servers whose Config comes from the workspace `.dsh/mcp.json` show a `workspace` badge in the Server list. Their detail panel explains that the workspace file defines or overrides the Server and disables the save control: edits there write the global layer, which the workspace file overrides. A Server defined only in the workspace file gets a read-only detail panel showing its non-secret Config; for an OAuth Server it offers Sign in / Sign out, because sign-in needs no global Config entry.
35
150
 
36
151
  JSON import accepts the standard `{ "mcpServers": { ... } }` shape. Import replaces matching Server entries and preserves Servers absent from the import.
37
152
 
38
- Every page write carries the latest namespace revision. Field edits use path mutations. Existing secret values remain saved when their value inputs stay blank.
153
+ Every page write carries the latest namespace revision. Field edits use path mutations.
154
+
155
+ ### Server lifecycle
156
+
157
+ Each Server's `lifecycle` setting controls when it connects and whether it idles out. `lazy` remains the default.
39
158
 
40
- ## Server lifecycle
159
+ - `lazy` (default): connects on the first tool-list or tool-call request and disconnects after the idle timeout; the next request reconnects it.
160
+ - `eager`: connects when the Adapter starts, when the Server is added or re-enabled, or when its connection Config changes, and still disconnects after the idle timeout.
161
+ - `keep-alive`: connects like `eager` but never idles out. When the connection closes unexpectedly, it reconnects after 30 seconds and keeps retrying every 30 seconds until it succeeds.
162
+ - `lazy-keep-alive`: connects on first use like `lazy`, but never idles out and auto-reconnects like `keep-alive`.
41
163
 
42
- The Host creates no unpromoted Server connection at startup. The first tool-list or tool-call request connects the Server and fills an in-memory metadata cache. A persisted Promotion can connect once to rebuild its native input schema. MCP `tools/list_changed` notifications refresh the cache. The default idle timeout closes the connection after 10 minutes without a request; the next request reconnects it.
164
+ A `lazy` or `lazy-keep-alive` Server creates no connection at startup. The first tool-list or tool-call request connects the Server and fills an in-memory metadata cache. Promotion discovery is the single documented exception to this startup laziness ([ADR 0003](./docs/adr/0003-promotion-discovery-lazy-exception.md)): when the Adapter starts up or re-enables a Server whose Config lists Promotions, the registry performs exactly one connect to rebuild the native input schema, then the connection follows the normal idle behavior. MCP `tools/list_changed` notifications refresh the cache. The default idle timeout closes the connection after 10 minutes without a request; the next request reconnects it.
43
165
 
44
- Changing, disabling, or removing a Server closes its connection and clears its cache. Re-enabling a Server restores the lazy behavior. A Connection RPC channel, `/mcp-adapter`, exposes detached `status`, `catalog`, and combined `overview` snapshots. Its `reconnect` endpoint restarts one named Server. These endpoints never expose Config secrets or SDK objects.
166
+ Changing, disabling, or removing a Server closes its connection, clears its cache, and cancels any pending keep-alive reconnect; a disabled Server never reconnects. Re-enabling a Server restores the configured lifecycle, so `eager` and `keep-alive` Servers connect again.
45
167
 
46
- ## Proxy Tool
168
+ ### Proxy Tool
47
169
 
48
- The Host registers one global `mcp` tool with three actions:
170
+ The Host registers one global `mcp` tool with seven actions:
49
171
 
50
172
  ```text
51
173
  mcp({ action: "search", query: "screenshot" })
52
174
  mcp({ action: "describe", server: "browser", tool: "take_screenshot" })
53
175
  mcp({ action: "call", server: "browser", tool: "take_screenshot", args: {} })
176
+ mcp({ action: "resources", server: "browser" })
177
+ mcp({ action: "read", server: "browser", uri: "file:///docs/readme.md" })
178
+ mcp({ action: "prompts", server: "browser" })
179
+ mcp({ action: "prompt", server: "browser", name: "review", args: { path: "src/index.js" } })
54
180
  ```
55
181
 
56
182
  Search results use `<server>__<tool>` names to avoid collisions. The first search fills empty metadata caches; later search and describe operations use the cache. Call requests ask for DSH approval unless that Server has `autoAllow: true`.
57
183
 
58
- Call, search, and describe output is limited to 50 KiB and 2,000 lines. Larger text uses `spillStore` for a full-result reference. Raw structured details larger than 16 KiB become a compact summary with their own spill reference.
184
+ ### Resources and prompts
185
+
186
+ Servers that expose MCP resources and prompts surface through the `mcp` Proxy Tool. There are no per-resource native tools and no browse UI.
187
+
188
+ `mcp({ action: "resources", server: "docs" })` lists one Server's resources as `uri`, `name`, `description`, and `mimeType`, plus its resource templates when the Server exposes them. `mcp({ action: "read", server: "docs", uri: "..." })` reads one resource. Text content flows inline through the same output guard as tool calls.
59
189
 
60
- ## Promotion
190
+ Binary (`blob`) resource content is never inlined as base64; see [Security](#security).
191
+
192
+ `mcp({ action: "prompts", server: "docs" })` lists one Server's prompts as `name`, `description`, and `arguments`. `mcp({ action: "prompt", server: "docs", name: "review", args: {} })` fetches one prompt and formats its messages as `role: text` lines. The same prompts surface to humans through `/mcp-prompt`.
193
+
194
+ Resource and prompt metadata is never cached: every action connects lazily and hits the live Server, so results reflect the Server's current state and an offline Server fails with a setup hint.
195
+
196
+ ### Promotion
61
197
 
62
198
  Add a raw MCP tool name to a Server's `promotedTools` list to register `<server>__<tool>` as a native DSH tool. The native tool uses the MCP input schema and the same approval, call, and output-guard path as `mcp({ action: "call" })`.
63
199
 
@@ -65,37 +201,27 @@ Promotion changes apply through `settings/updated`. They do not restart a connec
65
201
 
66
202
  DSH native tool names must match `[A-Za-z0-9_-]` and contain at most 64 characters. The Adapter logs a warning and skips an invalid or colliding Promotion name.
67
203
 
68
- ## Install
204
+ ### OAuth
69
205
 
70
- Requires the `dsh` CLI and a web profile (the browser GUI you run DSH with).
206
+ Remote HTTP Servers can authenticate with OAuth 2.0 authorization code flow and PKCE instead of static headers. Set `auth: oauth` on the Server (and optionally `scopes`), then sign in from **Settings > MCP** — the detail panel shows the sign-in state and Sign in / Sign out buttons — or with `/mcp-auth [server] [login|logout|status]`. Selecting `auth: oauth` does not connect by itself: until you sign in, a connect attempt fails fast with a sign-in hint instead of sending an unauthenticated request. Scopes are optional; a provider that defines none accepts an empty list — Context7, for example, serves OAuth at `https://mcp.context7.com/mcp/oauth` and works with no scope requested.
71
207
 
72
- ```sh
73
- dsh plugin --profile web add @auggieteo/dsh-mcp-adapter
74
- ```
208
+ Sign in opens the provider's authorization page in your browser through the DSH web session. The Adapter listens on a local loopback callback, validates the state parameter, exchanges the code, and finishes the flow in the background; the Server then reconnects with the fresh tokens. If the browser blocks the pop-up tab, allow pop-ups for the DSH page, or run `/mcp-auth <server> login`, which prints the authorization URL to open by hand.
75
209
 
76
- Then restart DSH. The Settings panel (`⌘,` / sidebar foot) gains an **MCP** section.
210
+ The SDK transports refresh expired access tokens through the stored refresh token on the next 401; when no refresh token remains, the Server reports that it needs re-authorization instead of starting a browser flow on its own. Sign out deletes the record and disconnects the Server. Exactly one sign-in flow may run per Server at a time. Token storage and transport details are covered under [Security](#security). See [ADR 0008](./docs/adr/0008-oauth-for-http-servers.md).
77
211
 
78
- ### Local development
79
-
80
- ```sh
81
- pnpm install
82
- pnpm build # produces lib/client.js (required before install)
83
- dsh plugin --profile web add /absolute/path/to/dsh-mcp-adapter
84
- ```
212
+ ### Agent skill
85
213
 
86
- During client development, run `pnpm dev` alongside DSH. `dsh-client-hmr` reloads each rebuilt client bundle without a manual refresh.
214
+ The package bundles a model-facing skill in `skills/mcp-adapter/SKILL.md`. When the DSH `skills` service is present, the Adapter registers it at startup as a runtime skill named `mcp-adapter`, so agents learn the search → describe → call workflow, how to read failure messages (including the OAuth sign-in hint), and which commands belong to the human. The frontmatter drives routing; edit the file to change the guidance, and a DSH restart picks the edit up.
87
215
 
88
- Host changes need a DSH restart. The web profile disables host HMR for plugin rows.
216
+ ## Architecture
89
217
 
90
- ## Uninstall
218
+ 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.
91
219
 
92
- ```sh
93
- dsh plugin --profile web remove @auggieteo/dsh-mcp-adapter
94
- ```
220
+ 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.
95
221
 
96
222
  ## Release
97
223
 
98
- The package uses Semantic Versioning. Update `package.json` before each release.
224
+ The package uses Semantic Versioning. Update `package.json` before each release, and list user-facing changes in [CHANGELOG.md](./CHANGELOG.md).
99
225
 
100
226
  ```sh
101
227
  pnpm version patch --no-git-tag-version # or minor, major, or an explicit version
@@ -108,12 +234,6 @@ The `Publish to npm` workflow rejects any different tag. It tests and builds the
108
234
 
109
235
  A prerelease GitHub Release publishes with the npm `next` tag. A regular GitHub Release publishes with the npm `latest` tag.
110
236
 
111
- The first publish creates the npm package. Add a short-lived granular npm token as the GitHub `NPM_TOKEN` secret for this publish.
112
-
113
- Then configure npm trusted publishing for `auggie246/dsh-mcp-adapter` and `publish.yml`. Allow `npm publish`, then delete `NPM_TOKEN`.
114
-
115
- Later releases use GitHub OIDC and need no long-lived npm token. npm requires the package to exist before trusted publishing configuration.
116
-
117
237
  ## Layout
118
238
 
119
239
  ```
@@ -123,5 +243,26 @@ src/client/ client half: JSX, bundled to lib/client.js
123
243
  build.mjs esbuild wrapper producing the loader-compatible bundle
124
244
  lib/client.js build artifact (gitignored), required at runtime
125
245
  docs/adr/ architecture decisions
246
+ docs/verification/ end-to-end verification records
126
247
  CONTEXT.md project vocabulary
248
+ CHANGELOG.md released changes
127
249
  ```
250
+
251
+ ## Maintainers
252
+
253
+ [@auggieteo](https://github.com/auggie246).
254
+
255
+ ## Thanks
256
+
257
+ [pi-mcp-adapter](https://github.com/nicobailon/pi-mcp-adapter) by [@nicobailon](https://github.com/nicobailon) — the workflow this project mirrors. The [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) team for the plugin surface this builds on.
258
+
259
+ ## Contributing
260
+
261
+ 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.
262
+
263
+ - 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`).
264
+ - New project vocabulary belongs in [CONTEXT.md](./CONTEXT.md); design decisions get an [ADR](./docs/adr/) before or with the change.
265
+
266
+ ## License
267
+
268
+ [MIT](./LICENSE) © The dsh-mcp-adapter authors
@@ -0,0 +1,7 @@
1
+ # Promotion discovery connects once at startup, as the documented Lazy Lifecycle exception
2
+
3
+ A Server whose persisted Config lists `promotedTools` connects when the Adapter starts up or the Server is re-enabled. `McpPromotionRegistry` must register each Promoted tool as a native DSH tool with its MCP input schema, and after a restart there is no in-memory metadata cache, so the registry's initial sync calls `manager.listTools()`, which connects the Server. We keep this behavior and document Promotion discovery as the single explicit exception to the Lazy Lifecycle rule that startup creates no Server connections. The connect is exactly one: it rebuilds the native input schema, and the connection then follows the normal Lazy Lifecycle and closes after the idle timeout.
4
+
5
+ Persisting validated tool metadata for promoted tools was rejected because it duplicates the trust model. Persisted metadata would outlive Config changes and MCP `tools/list_changed` notifications without revalidation, so a Promotion could register a stale input schema and accept wrong arguments. Deferring native registration until another request discovers the Server was rejected because it breaks the promise that Promoted tools exist after restart without user action: the tool card would be missing until an unrelated `mcp` Proxy Tool request happened to connect the Server.
6
+
7
+ Consequences: every process restart (or Server re-enable) performs one connect per Server that has Promotions, then leaves that connection to idle out per `idleTimeoutMinutes`. Servers without Promotions are never connected at startup. The cost is bounded by the number of Promotions in Config, and README's Server lifecycle section states this exception explicitly.
@@ -0,0 +1,7 @@
1
+ # Lifecycle modes: lazy by default, plus eager, keep-alive, and lazy-keep-alive
2
+
3
+ A Server's `lifecycle` setting accepts four values mirroring pi-mcp-adapter: `lazy` (default), `eager`, `keep-alive`, and `lazy-keep-alive`. The semantics are orthogonal, so each value is one cell of a two-axis matrix: startup eagerness (connect at startup vs on first use) × idle persistence (the idle timeout disconnects vs never idle out). `lazy` is first use + idle timeout; `eager` is startup + idle timeout; `keep-alive` is startup + never idle out; `lazy-keep-alive` is first use + never idle out. The keep-alive pair additionally auto-reconnects after an unexpected close, retrying every 30 seconds (`KEEP_ALIVE_RETRY_MS`) until the connection holds or the Server is disabled, removed, or disposed.
4
+
5
+ Considered options: pi runs a continuous 30-second health check that polls every Server and reconnects when the probe fails; we instead arm one reconnect timer only after an observed `onClose` on the current generation, so a healthy connection costs nothing and a failing one retries at the same 30-second cadence. `eager` keeping the idle timeout follows pi: an idle connection still holds a stdio subprocess or a remote socket, so permanence must be an explicit choice (`keep-alive`), not a side effect of eagerness. Startup connects are scheduled through the fiber-owned scheduler instead of awaited, so manager construction, Config installs, and reconcile never block on a server that may be down or slow.
6
+
7
+ Consequences: a disabled Server never reconnects, and every manager-initiated close (Config change, disable, removal, dispose) bumps the record generation and cancels the pending reconnect timer, so closes the Adapter itself caused cannot trigger a reconnect loop. Re-enabling a Server, adding one, or changing its connection Config re-applies the startup behavior for eager and keep-alive lifecycles. Eager and keep-alive Servers gain startup cost: every session start spawns their stdio subprocess or opens their remote connection even with zero requests.
@@ -0,0 +1,11 @@
1
+ # Per-workspace Config layered over the global namespace
2
+
3
+ The Adapter resolves Config from two sources: the registered `mcp` namespace (ADR-0002) plus an optional workspace file at `.dsh/mcp.json` under the Host's workspace root, merged at resolve time inside a layered settings scope. The workspace file is read-only to the Adapter: nothing ever writes it.
4
+
5
+ Layering rules: a workspace Server entry with the same name REPLACES the global entry wholesale — no field-level merge, because half-applied secrets, transports, or lifecycle flags are dangerous. A Server is disabled or overridden from the workspace by defining it there with `disabled: true`; a workspace-only name adds a Server that exists only while the file is valid. The workspace file goes through the same `mcpServers` schema and cross-field validation as the global Config, with one shape rule: unknown top-level keys (only `mcpServers` is allowed) or any 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, not an error. Values are never environment-variable-interpolated.
6
+
7
+ Settings page writes stay global. Settings > MCP marks workspace-sourced Servers with a `workspace` badge, shows a precedence notice in the detail panel, and disables saving for them, because every page write, import, and API write lands on the global namespace, which the workspace file overrides. The global entry underneath a workspace override therefore remains writable by other means, and the read-only treatment in the page is advisory, not enforced by the Adapter.
8
+
9
+ Considered options: pi's full merge chain with field-level merging and `${env:VAR}` interpolation (rejected: unpredictable precedence and accidental secret expansion); making the workspace file the primary source (rejected: DSH users configure through the web UI, and ADR-0002 keeps Config in the settings namespace); writing the workspace file back from the Settings page (rejected: the Host does not own the workspace directory and should not race the user's editor).
10
+
11
+ Consequences: the manager and the Promotion registry consume the layered scope through the same `get`/`watch` interface, so a workspace edit reconciles connections exactly like a Settings edit; `update` and `mutate` on the layered scope forward to the global namespace only. The Settings page learns the per-Server source through a `layers` endpoint on the existing `/mcp-adapter` Connection RPC channel, because the merged view is resolved on the Host. The `layers` snapshot also carries a `servers` map of the workspace-side Config with every `secret`-role field removed, so the page can render workspace-only Servers read-only and offer OAuth sign-in for them, and the endpoint refreshes the layer before answering, so the page poll re-reads a file created after startup. The workspace layer refreshes when the global scope notifies, when `fs.watch` reports a change in the `.dsh` directory (best effort — platforms differ; while `.dsh` does not exist the watcher arms on the workspace root and upgrades to the `.dsh` watch on its first event), and on `refreshLayers()`, and notifies watchers only when the merged result actually changed. The `/mcp` command refuses enable/disable for workspace-sourced Servers for the same shadowing reason ([ADR 0007](./0007-mcp-commands.md)).
@@ -0,0 +1,7 @@
1
+ # Resources and prompts surface through the Proxy Tool and one human command
2
+
3
+ MCP resources and prompts are read through the Proxy Tool as four new `mcp` actions — `resources` (list one Server's resources and templates), `read` (read one resource), `prompts` (list one Server's prompts), and `prompt` (fetch one prompt with JSON args) — and prompts also get a single DSH human command, `/mcp-prompt <server> <prompt> [json-args]`, registered on the DSH `commands` registry. Text resource content flows inline through the existing output guard with its spill behavior; binary (`blob`) content is base64-decoded and written to a file under a new `mcp-resource-` directory in the system temp root, with the file name sanitized from the resource URI, and the result reports that path and byte size instead of the blob.
4
+
5
+ Considered options: generating one native read tool per resource (pi-mcp-adapter's approach) was rejected because per-resource tool schemas burn exactly the context window the Proxy Tool exists to save, and a resource browse UI was deferred because it needs a Settings-page design of its own. One DSH human command per prompt was rejected for the same registry-bloat and name-collision reasons that favor the single Proxy Tool; `/mcp-prompt` resolves the prompt at call time instead.
6
+
7
+ Consequences: there is no resource or prompt metadata caching this iteration — every action connects lazily and hits the live Server, mirroring Lazy Lifecycle, so list results always reflect the Server's current state and an offline Server fails with the usual setup hint. MCP pagination cursors are not surfaced (each action returns the first page), a browse UI remains a later feature, and `/mcp-prompt` output is formatted `role: text` lines without the tool output guard, since it renders for the human, not the model.
@@ -0,0 +1,7 @@
1
+ # One `/mcp` human command with subcommands
2
+
3
+ The Adapter registers one DSH human command, `/mcp`, whose subcommands (`status`, `reconnect`, `enable <server>`, `disable <server>`) are parsed from the raw input, mirroring pi-mcp-adapter's panel-free command UX. A bare `/mcp` is a status-only read: it reports one line per Server from the live status snapshot and mutates nothing. `enable` and `disable` write the global Config layer through the Adapter's settings namespace — per ADR-0002 the Settings page owns global edits, so the command performs the same kind of write the page performs, not a second config surface. `/mcp-auth` is not part of this command; it belongs to the OAuth feature.
4
+
5
+ Considered options: per-feature commands (`/mcp-status`, `/mcp-reconnect`, `/mcp-enable`, …) were rejected as registry noise — every DSH command is advertised to the composer UI, so four near-identical entries cost more than they save; a panel-style command was rejected because pi's UX is textual subcommands and DSH already has the Settings page for forms.
6
+
7
+ Consequences: command and Settings > MCP writes share one Config path, so a per-workspace Config layer ([ADR 0005](./0005-per-workspace-config.md)) takes precedence at resolve time without changing the command. With that layer in place, `enable` and `disable` name the layer they wrote ("in the global Config") and refuse a workspace-sourced Server outright, because a global write would be silently shadowed by the workspace entry. Later `/mcp` verbs still extend the parser inside the single `mcp` registration, while commands that belong to other features keep their own registry rows: `/mcp-auth` (OAuth, [ADR 0008](./0008-oauth-for-http-servers.md)) and `/mcp-prompt` (prompts, [ADR 0006](./0006-resources-and-prompts.md)).
@@ -0,0 +1,9 @@
1
+ # OAuth 2.0 with PKCE for remote HTTP Servers, with a file-backed token store
2
+
3
+ OAuth Servers sign in through the MCP SDK's client auth helper (`auth()` in `@modelcontextprotocol/sdk/client/auth.js` 1.30.0): the Adapter runs authorization_code with PKCE as a public client (`token_endpoint_auth_method: 'none'`, client name `dsh-mcp-adapter`), listens on `http://127.0.0.1:<ephemeral>/callback` per RFC 8252, and stores issued tokens in a file under the DSH home. The consent handoff goes through the DSH browser session: an `oauth-login` RPC returns the authorization URL, the Settings page opens it with `window.open(url, '_blank')`, and the flow completes in the background before the Adapter reconnects the Server (`oauth login`).
4
+
5
+ Storage evaluation: the installed DSH harness exposes no credentials, keyring, or secret-storage service for plugins, so an OS keyring was not reachable from v1. The chosen backend is one JSON file at `${DSH_HOME:-~/.dsh}/mcp-auth/tokens.json` — directory `0700`, file written `0600` (best-effort; some filesystems ignore modes), temp file plus rename for atomic writes — behind a small `TokenStore` interface (`get`/`set`/`delete`/`has`) that a future OS-keyring backend can implement without touching the flow code. Records are keyed by Server name and bound to the Server URL they were issued for; consumers refuse records whose `url` differs from the configured Server URL, so editing a Server's URL forces a fresh sign-in. State and the PKCE code verifier never persist: they live only inside one flow.
6
+
7
+ Connection and flow lifecycle: connection-time providers omit `redirectUrl`, so the SDK treats them as non-interactive and a background connect can never start a browser flow; an OAuth Server without signed-in, URL-bound tokens fails fast with `OAuth authorization required for <server> — sign in via Settings > MCP or /mcp-auth`. Token refresh is driven by the SDK transports (401 → `auth()` → refresh through `tokens()`/`saveTokens()`); if refresh fails, the Adapter surfaces the error instead of hanging. Exactly one pending flow per Server: a second start is refused with `already in progress` until the first settles (refusal, not preemption, so a running callback cannot be silently orphaned). A login always produces a fresh authorization URL — the flow provider reports no tokens — while existing registrations and tokens stay untouched until the new exchange succeeds. Logout aborts a pending flow, deletes tokens, and disconnects the Server.
8
+
9
+ Considered alternatives: an OS keyring backend (rejected for now — no DSH service to build on; the `TokenStore` seam keeps it cheap later), storing tokens in the DSH settings namespace (rejected — Config is user-editable and synced, the wrong home for credentials), and reusing the MCP SDK's `finishAuth` transport flow (rejected — the Adapter, not a transport, owns the local callback listener and the background completion).
@@ -0,0 +1,11 @@
1
+ # Client resolves the settings write face across DSH generations
2
+
3
+ The Adapter's client bundle once read `ctx.connection.api.settings` directly. DSH 0.1.2-rc.1 removed the `connection` service's `api` face: settings writes moved onto the `remote` service (its `settings` namespace mounted by the `@deepseek-ai/dsh-api-remotes` client bundle), taking positional `(ns, patch | ops, expectedRevision)` arguments and answering a flat `{ ok, value | error }` envelope instead of a `{ result }` one. The same release dropped the host-side `settingsNamespace` export from `@deepseek-ai/dsh-settings` (its `register` now validates the raw string itself). Applying the old bundle on 0.1.2-rc.1 crashes with `can't access property "settings", ctx.connection.api is undefined` and the plugin fails to load.
4
+
5
+ We resolve the write face at apply time (`src/client/settings-api.js`) and prefer `ctx.remote.settings` when it exists; otherwise we fall back to the 0.1.1-rc.2 `ctx.connection.api.settings` face. Both normalize to the controller's existing contract — `update`/`mutate` take one request object and answer `{ result: { ok, value | error } }` — so `McpSettingsController` and the host stay generation-agnostic. The client declares `remote` in its `inject` (the service exists in both generations; `remote.settings` does not exist on 0.1.1-rc.2 and stays undeclared, so the runner never parks waiting for it), and `package.json` adds `@deepseek-ai/dsh-api-remotes` to `dsh.client.inject` so the typed settings namespace mounts before consumers apply. On the host, `MCP_SETTINGS_NAMESPACE` becomes the plain `'mcp'` string: branding was type-only on 0.1.1-rc.2, and 0.1.2-rc.1 accepts the validated raw string. The peer dependency widens to `^0.1.1-rc.2 || ^0.1.2-rc.1`.
6
+
7
+ *Correction:* the original resolution read the rc.1 face as `ctx.remote?.settings`, assuming a plain property. The rc.1 runner instead mounts the namespace as the traced dotted service `remote.settings`: reading through `ctx.remote` composes that key, and cordis throws `cannot get property "remote.settings" without inject` for any plugin that did not declare the dotted service — which crashed this plugin at apply with a failed loader entry. Declaring it is not an option either: a declared-but-never-mounted service keeps the fiber from ever applying, which is exactly the 0.1.1-rc.2 case. The face is therefore resolved through cordis's inject-free `ctx.get('remote.settings')`, which answers the mounted namespace on 0.1.2-rc.1 via the root isolate and undefined on 0.1.1-rc.2, keeping the fallback reachable on both generations; the plain-property read remains as a legacy fallback for runners that expose the face as a plain property.
8
+
9
+ Considered options: shipping a 0.1.2-only release and telling 0.1.1-rc.2 users to upgrade was rejected because the harness is in flux and plugin releases should not force harness moves; probing `ctx.remote.$host` or version strings was rejected because face presence is the actual capability signal and survives future renames better. A hard failure with upgrade guidance is kept for a hypothetical third shape, so a future break surfaces as one clear message instead of an `undefined` read.
10
+
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.