@auggieteo/dsh-mcp-adapter 0.2.1 → 0.2.2
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 +11 -0
- package/README.md +10 -1
- package/package.json +1 -1
- package/src/host/index.js +7 -2
- package/src/host/settings.js +10 -1
- package/src/host/skill.js +125 -10
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,16 @@ 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.2.2] - 2026-09-06
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `skillInstall` Config field (`file` by default, `runtime`, or `off`) controlling how the bundled `mcp-adapter` agent skill is installed. `file` copies `skills/mcp-adapter/SKILL.md` verbatim to `$DSH_HOME/skills/mcp-adapter/SKILL.md` at startup (atomic temp-file-then-rename write, never a symlink), so the DSH filesystem skill provider lists it under Settings > Skills > DSH skills. `runtime` keeps the previous in-memory registration; `off` installs nothing. The mode is read once at Adapter startup.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- The bundled agent skill now installs as a real file by default instead of registering only in memory. The installed file is never overwritten: an identical file is left untouched, and a file edited by a human wins with one Host warning naming the installed path and the package version — delete `$DSH_HOME/skills/mcp-adapter/` and restart DSH to refresh it from the package. A failed file install (for example a read-only `$DSH_HOME`) falls back to the runtime registration with a warning; the skill stays a convenience and never throws.
|
|
16
|
+
|
|
7
17
|
## [v0.2.1] - 2026-09-06
|
|
8
18
|
|
|
9
19
|
### Added
|
|
@@ -56,5 +66,6 @@ Everything from v0.1.2 through this release: the six deferred v1 features (#8–
|
|
|
56
66
|
|
|
57
67
|
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
68
|
|
|
69
|
+
[v0.2.2]: https://github.com/auggie246/dsh-mcp-adapter/compare/v0.2.1...v0.2.2
|
|
59
70
|
[v0.2.0]: https://github.com/auggie246/dsh-mcp-adapter/compare/v0.1.2...v0.2.0
|
|
60
71
|
[v0.1.2]: https://github.com/auggie246/dsh-mcp-adapter/releases/tag/v0.1.2
|
package/README.md
CHANGED
|
@@ -130,10 +130,13 @@ mcp:
|
|
|
130
130
|
url: https://mcp.example.com/other
|
|
131
131
|
auth: oauth
|
|
132
132
|
scopes: [repo, read:org]
|
|
133
|
+
skillInstall: file
|
|
133
134
|
```
|
|
134
135
|
|
|
135
136
|
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`.
|
|
136
137
|
|
|
138
|
+
The one top-level Adapter field is `skillInstall` (`file` by default, `runtime`, or `off`); see [Agent skill](#agent-skill).
|
|
139
|
+
|
|
137
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.
|
|
138
141
|
|
|
139
142
|
#### Workspace Config layer
|
|
@@ -211,7 +214,13 @@ The SDK transports refresh expired access tokens through the stored refresh toke
|
|
|
211
214
|
|
|
212
215
|
### Agent skill
|
|
213
216
|
|
|
214
|
-
The package bundles a model-facing skill in `skills/mcp-adapter/SKILL.md
|
|
217
|
+
The package bundles a model-facing skill in `skills/mcp-adapter/SKILL.md`, 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 top-level `skillInstall` Config field controls how the Adapter installs it; the mode is read once at Adapter startup, so a change takes effect on the next DSH restart.
|
|
218
|
+
|
|
219
|
+
- `file` (default): on startup the Adapter copies the bundled `SKILL.md` verbatim to `$DSH_HOME/skills/mcp-adapter/SKILL.md` (an atomic temp-file-then-rename write, never a symlink). The DSH filesystem skill provider discovers the file, so the skill shows under **Settings > Skills > DSH skills** and joins the session catalog without a restart of an already-running session. When the file install fails, the Adapter falls back to the runtime registration below and logs one warning.
|
|
220
|
+
- `runtime`: the Adapter registers the skill in memory on the DSH `skills` service only. It reaches agents but never appears on the Settings > Skills page.
|
|
221
|
+
- `off`: the Adapter installs no skill.
|
|
222
|
+
|
|
223
|
+
The installed file is never overwritten. If `$DSH_HOME/skills/mcp-adapter/SKILL.md` already exists, your edit wins: an identical file is left untouched, and a differing file gets one Host warning naming the installed path and the package version. To refresh the installed copy from the package, delete `$DSH_HOME/skills/mcp-adapter/` and restart DSH. Editing the installed file changes what the session catalog serves; the edit survives plugin restarts.
|
|
215
224
|
|
|
216
225
|
## Architecture
|
|
217
226
|
|
package/package.json
CHANGED
package/src/host/index.js
CHANGED
|
@@ -19,9 +19,13 @@ export const inject = []
|
|
|
19
19
|
|
|
20
20
|
export function apply(ctx) {
|
|
21
21
|
// The bundled agent skill is independent of every other Adapter feature: a
|
|
22
|
-
// deployment without the skills service simply gets no skill entry.
|
|
23
|
-
|
|
22
|
+
// deployment without the skills service simply gets no skill entry. Sibling
|
|
23
|
+
// inject fibers run in registration order while the services exist, so the
|
|
24
|
+
// settings callback reads `skillInstall` before the skills callback runs;
|
|
25
|
+
// without the settings service the skill module's 'file' default applies.
|
|
26
|
+
let skillInstall
|
|
24
27
|
installMcpSettings(ctx, (settingsCtx, scope) => {
|
|
28
|
+
skillInstall = scope.get()?.skillInstall
|
|
25
29
|
const layeredScope = installWorkspaceLayer(settingsCtx, scope)
|
|
26
30
|
settingsCtx.inject(['timer'], (managerCtx) => {
|
|
27
31
|
const store = createFileTokenStore()
|
|
@@ -59,6 +63,7 @@ export function apply(ctx) {
|
|
|
59
63
|
})
|
|
60
64
|
})
|
|
61
65
|
})
|
|
66
|
+
ctx.inject(['skills'], (skillCtx) => installMcpSkill(skillCtx, { skillInstall }))
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
export * from './commands.js'
|
package/src/host/settings.js
CHANGED
|
@@ -69,9 +69,18 @@ export const McpSettingsSchema = z.object({
|
|
|
69
69
|
.dict(McpServerSchema)
|
|
70
70
|
.default({})
|
|
71
71
|
.description('Global MCP servers, keyed by their unique server name.'),
|
|
72
|
+
skillInstall: z
|
|
73
|
+
.union(['file', 'runtime', 'off'])
|
|
74
|
+
.default('file')
|
|
75
|
+
.description(
|
|
76
|
+
'How the bundled mcp-adapter agent skill is installed: file (copy to '
|
|
77
|
+
+ '$DSH_HOME/skills/mcp-adapter/ so Settings > Skills lists it; an '
|
|
78
|
+
+ 'existing file is never overwritten), runtime (register in memory '
|
|
79
|
+
+ 'only), or off (no skill). Read once at Adapter startup.',
|
|
80
|
+
),
|
|
72
81
|
})
|
|
73
82
|
|
|
74
|
-
const TOP_LEVEL_KEYS = new Set(['mcpServers'])
|
|
83
|
+
const TOP_LEVEL_KEYS = new Set(['mcpServers', 'skillInstall'])
|
|
75
84
|
const SERVER_KEYS = new Set([
|
|
76
85
|
'command',
|
|
77
86
|
'args',
|
package/src/host/skill.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
mkdirSync as defaultMkdirSync,
|
|
3
|
+
readFileSync as defaultReadFileSync,
|
|
4
|
+
renameSync as defaultRenameSync,
|
|
5
|
+
writeFileSync as defaultWriteFileSync,
|
|
6
|
+
} from 'node:fs'
|
|
7
|
+
import { createRequire } from 'node:module'
|
|
8
|
+
import { homedir as defaultHomedir } from 'node:os'
|
|
9
|
+
import { join } from 'node:path'
|
|
2
10
|
import { fileURLToPath } from 'node:url'
|
|
3
11
|
|
|
4
12
|
import { errorMessage } from './errors.js'
|
|
@@ -7,8 +15,26 @@ import { errorMessage } from './errors.js'
|
|
|
7
15
|
export const SKILL_DIR = fileURLToPath(new URL('../../skills/mcp-adapter/', import.meta.url))
|
|
8
16
|
|
|
9
17
|
const SKILL_FILE = 'SKILL.md'
|
|
18
|
+
const SKILL_TEMP_FILE = '.SKILL.md.tmp'
|
|
10
19
|
const SKILL_NAME = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/
|
|
11
20
|
|
|
21
|
+
/** The Adapter package version, read lazily so a missing manifest never breaks load. */
|
|
22
|
+
function packageVersion() {
|
|
23
|
+
try {
|
|
24
|
+
return createRequire(import.meta.url)('../../package.json').version
|
|
25
|
+
} catch {
|
|
26
|
+
return 'unknown'
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolve the DSH skills root the same way the harness does: `$DSH_HOME`
|
|
32
|
+
* when set, otherwise `.dsh` under the OS home directory.
|
|
33
|
+
*/
|
|
34
|
+
export function resolveSkillsRoot(env = process.env, homedir = defaultHomedir) {
|
|
35
|
+
return join(env.DSH_HOME ?? join(homedir(), '.dsh'), 'skills')
|
|
36
|
+
}
|
|
37
|
+
|
|
12
38
|
/**
|
|
13
39
|
* Split one `SKILL.md` into its frontmatter fields and body. The block must
|
|
14
40
|
* open the file: `---`, flat `key: value` lines, then a closing `---`. Only
|
|
@@ -34,16 +60,96 @@ export function parseSkillFrontmatter(text) {
|
|
|
34
60
|
}
|
|
35
61
|
|
|
36
62
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
63
|
+
* Copy the bundled `SKILL.md` to `$DSH_HOME/skills/<name>/SKILL.md` so the
|
|
64
|
+
* filesystem skill provider (and with it Settings > Skills) discovers it.
|
|
65
|
+
* The copy is atomic — temp file in the target directory, then rename — and
|
|
66
|
+
* never a symlink or hardlink. An installed file is never overwritten: when
|
|
67
|
+
* it exists and matches, nothing happens; when it differs, the human's edit
|
|
68
|
+
* wins and one warning names the path and the package version. Returns true
|
|
69
|
+
* when the file now carries the skill (install done or already present).
|
|
70
|
+
*/
|
|
71
|
+
function installSkillFile(ctx, text, name, {
|
|
72
|
+
readInstalledFileSync,
|
|
73
|
+
writeFileSync,
|
|
74
|
+
mkdirSync,
|
|
75
|
+
renameSync,
|
|
76
|
+
env,
|
|
77
|
+
homedir,
|
|
78
|
+
}) {
|
|
79
|
+
const warn = (reason) => {
|
|
80
|
+
ctx.logger?.warn?.(`dsh-mcp-adapter: ${reason}`)
|
|
81
|
+
}
|
|
82
|
+
const targetDir = join(resolveSkillsRoot(env, homedir), name)
|
|
83
|
+
const target = join(targetDir, SKILL_FILE)
|
|
84
|
+
let installed
|
|
85
|
+
try {
|
|
86
|
+
installed = String(readInstalledFileSync(target, 'utf8'))
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (error?.code !== 'ENOENT') {
|
|
89
|
+
warn(
|
|
90
|
+
`cannot read the installed skill file ${target} (${errorMessage(error)}); `
|
|
91
|
+
+ 'registering the bundled skill at runtime instead',
|
|
92
|
+
)
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (installed !== undefined) {
|
|
97
|
+
if (installed === text) return true
|
|
98
|
+
// The human's edit wins: leave the file untouched and treat it as the
|
|
99
|
+
// one definition for this name.
|
|
100
|
+
warn(
|
|
101
|
+
`the installed skill file ${target} differs from the bundled copy `
|
|
102
|
+
+ `(dsh-mcp-adapter ${packageVersion()}); delete ${targetDir} to refresh `
|
|
103
|
+
+ 'it from the package on the next start',
|
|
104
|
+
)
|
|
105
|
+
return true
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
mkdirSync(targetDir, { recursive: true })
|
|
109
|
+
const temp = join(targetDir, SKILL_TEMP_FILE)
|
|
110
|
+
writeFileSync(temp, text, 'utf8')
|
|
111
|
+
renameSync(temp, target)
|
|
112
|
+
return true
|
|
113
|
+
} catch (error) {
|
|
114
|
+
warn(
|
|
115
|
+
`cannot install the skill file at ${target} (${errorMessage(error)}); `
|
|
116
|
+
+ 'registering the bundled skill at runtime instead',
|
|
117
|
+
)
|
|
118
|
+
return false
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Install the bundled `mcp-adapter` skill so agents discover how to drive the
|
|
124
|
+
* Proxy Tool. The body lives in `skills/mcp-adapter/SKILL.md` next to the
|
|
125
|
+
* package root, so a human can edit the wording without touching code;
|
|
126
|
+
* frontmatter still drives routing.
|
|
41
127
|
*
|
|
42
|
-
* The
|
|
43
|
-
*
|
|
44
|
-
*
|
|
128
|
+
* The `skillInstall` mode comes from the Adapter Config (default `'file'`).
|
|
129
|
+
* `'file'` copies the skill to `$DSH_HOME/skills/<name>/SKILL.md` and lets
|
|
130
|
+
* the filesystem provider pick it up — one definition per name, so the
|
|
131
|
+
* runtime registration is skipped. `'runtime'` registers directly on the
|
|
132
|
+
* `skills` service as before. `'off'` installs nothing.
|
|
133
|
+
*
|
|
134
|
+
* The skill is a convenience, never a dependency: an unreadable file, bad
|
|
135
|
+
* frontmatter, or a failed install logs a warning and never throws; a failed
|
|
136
|
+
* file install falls back to the runtime registration. Every filesystem
|
|
137
|
+
* access (`readFileSync` for the bundled copy, `readInstalledFileSync` for
|
|
138
|
+
* the installed one, plus `writeFileSync`, `mkdirSync`, and `renameSync`)
|
|
139
|
+
* and `dir`, `env`, and `homedir` are injectable for tests.
|
|
45
140
|
*/
|
|
46
|
-
export function installMcpSkill(ctx, {
|
|
141
|
+
export function installMcpSkill(ctx, {
|
|
142
|
+
readFileSync = defaultReadFileSync,
|
|
143
|
+
readInstalledFileSync = defaultReadFileSync,
|
|
144
|
+
writeFileSync = defaultWriteFileSync,
|
|
145
|
+
mkdirSync = defaultMkdirSync,
|
|
146
|
+
renameSync = defaultRenameSync,
|
|
147
|
+
env = process.env,
|
|
148
|
+
homedir = defaultHomedir,
|
|
149
|
+
dir = SKILL_DIR,
|
|
150
|
+
skillInstall = 'file',
|
|
151
|
+
} = {}) {
|
|
152
|
+
if (skillInstall === 'off') return undefined
|
|
47
153
|
const fail = (reason) => {
|
|
48
154
|
ctx.logger?.warn?.(`dsh-mcp-adapter: bundled skill unavailable (${reason})`)
|
|
49
155
|
return undefined
|
|
@@ -57,7 +163,7 @@ export function installMcpSkill(ctx, { readFileSync = defaultReadFileSync, dir =
|
|
|
57
163
|
if (typeof description !== 'string' || description === '') {
|
|
58
164
|
return fail('frontmatter requires a non-empty description')
|
|
59
165
|
}
|
|
60
|
-
|
|
166
|
+
const register = () => ctx.skills.register({
|
|
61
167
|
name,
|
|
62
168
|
description,
|
|
63
169
|
...(whenToUse === undefined ? {} : { whenToUse }),
|
|
@@ -65,6 +171,15 @@ export function installMcpSkill(ctx, { readFileSync = defaultReadFileSync, dir =
|
|
|
65
171
|
content: body,
|
|
66
172
|
resourceBase: { kind: 'directory', path: dir },
|
|
67
173
|
})
|
|
174
|
+
if (skillInstall !== 'runtime') {
|
|
175
|
+
const installed = installSkillFile(ctx, text, name, {
|
|
176
|
+
readInstalledFileSync, writeFileSync, mkdirSync, renameSync, env, homedir,
|
|
177
|
+
})
|
|
178
|
+
// One definition per name: the filesystem provider owns the skill once
|
|
179
|
+
// the file is in place (or the human's edit already covers it).
|
|
180
|
+
if (installed) return undefined
|
|
181
|
+
}
|
|
182
|
+
return register()
|
|
68
183
|
} catch (error) {
|
|
69
184
|
return fail(errorMessage(error))
|
|
70
185
|
}
|