@aiwayds/dsh-mcp-adapter 0.4.2 → 0.5.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/README.md CHANGED
@@ -4,7 +4,7 @@ English | [简体中文](README.zh.md)
4
4
 
5
5
  Token-efficient MCP adapter for [DeepSeek Harness (dsh)](https://github.com/deepseek-ai/deepseek-harness) — a **prompt-side shim** inspired by [pi-mcp-adapter](https://github.com/nicobailon/pi-mcp-adapter).
6
6
 
7
- **Requires dsh >= 0.1.5-rc.2** — this plugin targets the dsh RC/stable line only (CI and releases resolve the newest of the `latest`/`next` dist-tags at runtime). **The alpha line is no longer supported.**
7
+ **Requires dsh >= 0.1.7-rc.1** — this plugin targets the dsh RC/stable line only (CI and releases resolve the newest of the `latest`/`next` dist-tags at runtime). **The alpha line is no longer supported.**
8
8
 
9
9
  ## The problem
10
10
 
@@ -29,6 +29,8 @@ Image results keep their native behavior: `mcp_call` delegates `output.render` t
29
29
 
30
30
  **Code Mode:** under `mode: 'code'` the wire already collapses to `run_code`; this plugin is a no-op there.
31
31
 
32
+ **Host resource tools (dsh ≥ 0.1.6):** the host ships `@deepseek-ai/dsh-mcp-resources`, which registers three shared tools — `list_mcp_resources`, `list_mcp_resource_templates`, `read_mcp_resource` — without the `mcp__` prefix. By design they never enter the fold path and surface to the model natively; the fold/keep semantics are unchanged, and `keep` only ever promotes names that match the prefix, so nothing here is affected.
33
+
32
34
  **Load position:** loaded through host composition (the `cordis.patch.yml` `insert` below) the adapter is global — every agent's assemblies are folded. Loaded through an agent-scoped context instead, it applies only to that agent.
33
35
 
34
36
  ## Setup
@@ -62,9 +64,11 @@ dsh plugin --profile <name> remove @aiwayds/dsh-mcp-adapter
62
64
 
63
65
  The host reconciles the profile automatically: the `dsh.profile.bundles` entry is spliced out and the package's patch layer drops.
64
66
 
65
- One thing deliberately **stays**: the `mcp-adapter:` section in `~/.dsh/settings.yaml` — the stable server ids (`1..99`) and the disabled gates. It is never pruned by design: if you reinstall the plugin, every server keeps the same id it had before.
67
+ One thing deliberately **stays**: the plugin's gate file at `~/.dsh/storages/mcp-adapter/gate.json` (`$DSH_HOME` honors an override) — the stable server ids (`1..99`) and the disabled gates. It is never pruned by design: if you reinstall the plugin, every server keeps the same id it had before.
68
+
69
+ To purge that state too, delete the `storages/mcp-adapter/` directory yourself; ids will be re-allocated from scratch on reinstall.
66
70
 
67
- To purge that state too, delete the `mcp-adapter:` section from `settings.yaml` yourself; ids will be re-allocated from scratch on reinstall.
71
+ Upgrading from a dsh 0.1.5 install: the old `mcp-adapter:` section of `settings.yaml` (which the 0.1.7 host renames to `settings.yaml.imported` after its one-shot import, since the section name does not match this plugin's entry id) is **absorbed once automatically**: on the next plugin boot, if no gate file exists yet, the stable ids and the disabled set are read from `settings.yaml.imported` (fallback: the still-present `settings.yaml`), sanitized and written straight into `gate.json` — no `/mcp disable <id>` re-latching needed. The pass is recorded in `<dsh home>/storages/mcp-adapter/legacy-import.json` (a present marker means it never re-runs; an existing gate.json is left untouched). Your `config:` values in the profile patch carry over unchanged.
68
72
 
69
73
  ## Config
70
74
 
@@ -74,6 +78,9 @@ To purge that state too, delete the `mcp-adapter:` section from `settings.yaml`
74
78
  | `keep` | `[]` | name patterns (`*` wildcard) kept native — pi-mcp-adapter's "direct mode", for high-frequency tools that deserve first-class schemas |
75
79
  | `servers` | `[]` | server-name whitelist: when non-empty, only these servers' tools are folded / cataloged / dispatchable (all three consult the same list) |
76
80
  | `descriptionLimit` | `200` | max chars per tool description in the `mcp_list` catalog |
81
+ | `storageDir` | `""` | gate storage directory override; empty = `<dsh home>/storages/mcp-adapter` (read once at plugin start — moving it takes effect on restart) |
82
+
83
+ Every field is declared **volatile** (the dsh 0.1.7 settings contract): all five appear on the plugin's settings page under the `dsh-mcp-adapter` entry, and editing them there applies **without restarting the plugin** — the fold boundary, catalog and dispatch pick the new values up on their very next use. The `config:` block in your profile patch keeps working exactly as before.
77
84
 
78
85
  ```yaml
79
86
  config:
@@ -107,9 +114,9 @@ The plugin registers one slash command on the platform `commands` service — co
107
114
  | `/mcp disable <id>` | Latch a whole server off: its tools force-fold out of every prompt — keep and `servers` exemptions included — it disappears from the `mcp_list` catalog, and `mcp_call` refuses it with an `/mcp enable <id>` hint |
108
115
  | `/mcp enable <id>` | Restore it under the same stable id |
109
116
 
110
- Any other form answers with usage. Every server observed by `/mcp` gets a stable numeric id (`1..99`, smallest free first), persisted in the dsh settings service under the `mcp-adapter:` section of your settings file (`~/.dsh/settings.yaml` on a stock install). Ids survive restarts and re-sync gaps and are never recycled — an id always names the same server; when all 99 are taken the `/mcp` views label it explicitly (`id space exhausted (99/99): N server(s) beyond the cap cannot be gated`, plus a marker on each affected group).
117
+ Any other form answers with usage. Every server observed by `/mcp` gets a stable numeric id (`1..99`, smallest free first), persisted in the plugin's own gate file at `<dsh home>/storages/mcp-adapter/gate.json` (machine state — deliberately a file, not a settings-page field). Ids survive restarts and re-sync gaps and are never recycled — an id always names the same server; when all 99 are taken the `/mcp` views label it explicitly (`id space exhausted (99/99): N server(s) beyond the cap cannot be gated`, plus a marker on each affected group).
111
118
 
112
- Disable is **gate-style, not a disconnect**: the official client exposes no disconnect API, so tools stay registered and connections keep running — gating only removes them from the prompt, the catalog, and dispatch. All three latches judge through one shared verdict, so they can never disagree with each other or with what `/mcp` displays. Enable/disable persist through the settings service; without one everything still works and only the toggles answer with an explanatory error.
119
+ Disable is **gate-style, not a disconnect**: the official client exposes no disconnect API, so tools stay registered and connections keep running — gating only removes them from the prompt, the catalog, and dispatch. All three latches judge through one shared verdict, so they can never disagree with each other or with what `/mcp` displays. Enable/disable persist through that file (atomic tmp+rename writes; a corrupt document warns once at startup and degrades to everything-enabled, and the next allocation write heals it). If the storage location cannot be written at all, everything still works and only the toggles answer with a persistence error.
113
120
 
114
121
  One real boundary: that latch is prompt-side (catalog/dispatch); native direct calls to a remembered `mcp__server__tool` name may still execute — for hard enforcement pair with pipeline guards or `tools.restrict()`.
115
122
 
package/README.zh.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  面向 [DeepSeek Harness (dsh)](https://github.com/deepseek-ai/deepseek-harness) 的省 token MCP 适配器——一个受 [pi-mcp-adapter](https://github.com/nicobailon/pi-mcp-adapter) 启发的 **prompt-side shim**(提示词侧垫片)。
6
6
 
7
- **要求 dsh >= 0.1.5-rc.2** — 本插件只跟随 dsh RC/stable 线(CI 与发版在运行时解析 latest/next 中更新的 dist-tag)。**不再支持 alpha 线。**
7
+ **要求 dsh >= 0.1.7-rc.1** — 本插件只跟随 dsh RC/stable 线(CI 与发版在运行时解析 latest/next 中更新的 dist-tag)。**不再支持 alpha 线。**
8
8
 
9
9
  ## 问题
10
10
 
@@ -29,6 +29,8 @@
29
29
 
30
30
  **Code Mode:** 在 `mode: 'code'` 下线上本来就折叠为 `run_code`,本插件天然 no-op。
31
31
 
32
+ **宿主资源工具(dsh ≥ 0.1.6):** 宿主内置的 `@deepseek-ai/dsh-mcp-resources` 注册三个共享工具 —— `list_mcp_resources`、`list_mcp_resource_templates`、`read_mcp_resource`,均不带 `mcp__` 前缀。它们按设计不进折叠路径,原生呈现给模型;折叠与 keep 语义保持不变,`keep` 也只会命中匹配前缀的名字,本插件无需任何额外配置。
33
+
32
34
  **加载位置:** 经宿主组合加载(即下方 `cordis.patch.yml` 的 `insert` 行)时全局生效——所有 agent 的装配都会被折叠;若经某个 agent 的 scoped context 加载,则只对该 agent 生效。
33
35
 
34
36
  ## 安装
@@ -54,9 +56,11 @@ dsh plugin --profile <name> remove @aiwayds/dsh-mcp-adapter
54
56
 
55
57
  宿主会自动完成清理:`dsh.profile.bundles` 里对应的条目被拼接移除,插件的 patch 层随之失效。
56
58
 
57
- 有一份状态被刻意**保留**:`~/.dsh/settings.yaml` 里的 `mcp-adapter:` 小节——stable server id(1..99)和 disabled 门闩。它按设计永不回收:重装本插件后,每个 server 仍沿用之前的 id。
59
+ 有一份状态被刻意**保留**:插件自己的门闩文件 `~/.dsh/storages/mcp-adapter/gate.json`(设置了 `DSH_HOME` 则以其为准)——stable server id(1..99)和 disabled 门闩。它按设计永不回收:重装本插件后,每个 server 仍沿用之前的 id。
60
+
61
+ 连这份状态也想清掉的话,请自行删除 `storages/mcp-adapter/` 目录;重装时 id 会重新分配。
58
62
 
59
- 连这份状态也想清掉的话,请自行删除 `settings.yaml` 里的 `mcp-adapter:` 小节;重装时 id 会重新分配。
63
+ 从 dsh 0.1.5 升级:旧 `settings.yaml` 里的 `mcp-adapter:` 小节(0.1.7 宿主首次启动时把该文件一次性导入并改名为 `settings.yaml.imported`,旧段名与本插件 entry id 不同名)会被**一次性自动承接**:下次插件启动时若尚无门闩文件,则从 `settings.yaml.imported`(兜底仍在的 `settings.yaml`)读取 stable id 与 disabled 门闩,消毒后直接写入 `gate.json`——无需再逐条 `/mcp disable <id>` 重新闩上。承接过程记录在 `<dsh home>/storages/mcp-adapter/legacy-import.json`(该 marker 存在即永不重跑;已有 gate.json 则完全不介入)。profile patch 里该 entry 的 `config:` 配置原样继承,不受影响。
60
64
 
61
65
  ## 配置
62
66
 
@@ -66,6 +70,7 @@ dsh plugin --profile <name> remove @aiwayds/dsh-mcp-adapter
66
70
  | `keep` | `[]` | 保持原生进 prompt 的名字模式(`*` 通配)——对应 pi-mcp-adapter 的 direct 模式,适合高频、值得占一等座 schema 的工具 |
67
71
  | `servers` | `[]` | server 白名单:非空时只有这些 server 的工具会被折叠 / 进目录 / 可分发(三处共用同一份名单) |
68
72
  | `descriptionLimit` | `200` | `mcp_list` 目录里每条工具描述的最大字符数 |
73
+ | `storageDir` | `""` | 门闩存储目录覆盖;空 = `<dsh home>/storages/mcp-adapter`(插件启动时读一次,改后需重启插件生效) |
69
74
 
70
75
  ```yaml
71
76
  config:
@@ -77,6 +82,8 @@ config:
77
82
  - github
78
83
  ```
79
84
 
85
+ 以上字段全部声明为 **volatile**(dsh 0.1.7 settings 契约):五个键都会出现在插件设置页(entry `dsh-mcp-adapter`)中,在设置页修改**无需重启插件即可生效**——折叠边界、目录与分发在下一次使用时立即采用新值。profile patch 里的 `config:` 写法照旧可用。
86
+
80
87
  **信任边界:** 默认所有匹配 `prefix` 的工具都会被折叠——前缀只是命名约定而非安全边界,第三方插件恰好用 `mcp__*` 注册的工具同样会折叠。若只信任官方 client 的 server,请在 `servers` 里显式列出;其余保持原生(仍可直调,只是不走 meta-tool)。
81
88
 
82
89
  ## 备注
@@ -99,9 +106,9 @@ config:
99
106
  | `/mcp disable <id>` | 把一个 server 整体闩上:工具强制折叠出 prompt(keep 与 `servers` 豁免一并覆盖)、从 `mcp_list` 目录消失、`mcp_call` 拒绝并给 `/mcp enable <id>` 指引 |
100
107
  | `/mcp enable <id>` | 用同一个 stable id 复原 |
101
108
 
102
- 其余形态一律回复用法说明。`/mcp` 观测到的每个 server 都会分到一个稳定数字 id(1..99,最小空闲优先),经 dsh settings 服务持久化到配置文件的 `mcp-adapter:` 小节(默认安装即 `~/.dsh/settings.yaml`)。id 跨重启、跨 re-sync 空窗保持不变,且永不回收——一个 id 永远指同一个 server;99 个用尽时由 `/mcp` 视图明确标注(`id space exhausted (99/99): N server(s) beyond the cap cannot be gated`,受影响的分组行会带标记)。
109
+ 其余形态一律回复用法说明。`/mcp` 观测到的每个 server 都会分到一个稳定数字 id(1..99,最小空闲优先),持久化到插件自己的门闩文件 `<dsh home>/storages/mcp-adapter/gate.json`(机器态——刻意用文件而非设置页字段)。id 跨重启、跨 re-sync 空窗保持不变,且永不回收——一个 id 永远指同一个 server;99 个用尽时由 `/mcp` 视图明确标注(`id space exhausted (99/99): N server(s) beyond the cap cannot be gated`,受影响的分组行会带标记)。
103
110
 
104
- disable 是**门闩式开关,不是真断连**:官方 client 不提供断连 API,工具仍留在注册表里、连接照常运行——门闩只把它请出 prompt、目录与分发。三层门闩共用同一个判定函数,彼此之间以及与 `/mcp` 的展示永远不会口径不一。enable/disable 经 settings 服务持久化;没有 settings 服务时其余功能照常,只有 toggle 会回一条说明性报错。
111
+ disable 是**门闩式开关,不是真断连**:官方 client 不提供断连 API,工具仍留在注册表里、连接照常运行——门闩只把它请出 prompt、目录与分发。三层门闩共用同一个判定函数,彼此之间以及与 `/mcp` 的展示永远不会口径不一。enable/disable 经该文件持久化(tmp+rename 原子写;文档损坏时启动告警一次并退化为全启,下一次分配写会自动修复)。若存储位置完全不可写,其余功能照常,只有 toggle 会回一条持久化报错。
105
112
 
106
113
  一条真实边界:门闩生效在 prompt 侧(目录/分发层);记得完整工具名的模型仍可能原生直调 `mcp__server__tool` 成功——需要硬性拦截时,请配合管线 guard 或 `tools.restrict()`。
107
114
 
package/cordis.patch.yml CHANGED
@@ -26,6 +26,12 @@
26
26
  # client's registrations)
27
27
  # descriptionLimit: max chars per tool description in the mcp_list
28
28
  # catalog (default 200)
29
+ # storageDir: gate storage directory override; empty (default) =
30
+ # <dsh home>/storages/mcp-adapter
31
+ #
32
+ # All five keys are volatile (dsh 0.1.7 settings contract): they appear on
33
+ # the plugin's settings page (entry `dsh-mcp-adapter`) and hot-reload there
34
+ # without a plugin restart. This `config:` form keeps working too.
29
35
 
30
36
  - insert:
31
37
  - id: dsh-mcp-adapter
package/icon.svg ADDED
@@ -0,0 +1,5 @@
1
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M9 2.5V7M15 2.5V7"/>
3
+ <path d="M6.5 7h11v4.5a5.5 5.5 0 0 1-11 0z"/>
4
+ <path d="M12 17v4.5"/>
5
+ </svg>
package/lib/index.d.ts CHANGED
@@ -33,8 +33,9 @@
33
33
  * three latches at once — its tools force-fold out of every prompt (keep and
34
34
  * `servers` whitelist exemptions included), it disappears from the catalog,
35
35
  * and `mcp_call` refuses it with an enable hint. Each server gets a stable
36
- * numeric id (1..99, smallest free first) persisted in the dsh settings
37
- * service (`mcp-adapter` namespace), so ids survive restarts and re-syncs.
36
+ * numeric id (1..99, smallest free first) persisted in the plugin's own gate
37
+ * file under the dsh home (`storages/mcp-adapter/gate.json`), so ids survive
38
+ * restarts and re-syncs.
38
39
  *
39
40
  * Child-owned projections are preserved: `mcp_call` delegates its
40
41
  * `output.render` to the dispatched child and forwards the child's
@@ -84,14 +85,19 @@ export declare const MCP_CALL_TOOL_NAME = "mcp_call";
84
85
  /** Upper bound of the stable per-server id space. */
85
86
  export declare const MCP_SERVER_ID_LIMIT = 99;
86
87
  /**
87
- * dsh settings namespace persisting the stable ids and the disabled set.
88
- * Plain literal: dsh-settings 0.1.2-alpha.3 removed the runtime
89
- * settingsNamespace() helper this constant used to call (same adaptation as
90
- * dsh-model-sync / dsh-cron / dsh-vault).
88
+ * dsh 0.1.7 note: the runtime settings registry this state used to live in
89
+ * (`settings.register('mcp-adapter', …)` on the 0.1.5 `SettingsScope`) is
90
+ * gone — 0.1.7 settings are the projection of a plugin's `Config` schema and
91
+ * volatile fields are user-editable values, which a machine-maintained id
92
+ * registry must never be. The gate therefore persists through the plugin's
93
+ * OWN file under the dsh home (same seam as dsh-cron/dsh-vault):
94
+ * `<dsh home>/storages/mcp-adapter/gate.json`.
91
95
  */
92
- export declare const MCP_ADAPTER_SETTINGS_NAMESPACE = "mcp-adapter";
96
+ export declare const MCP_ADAPTER_STORAGE_DIRNAME = "mcp-adapter";
97
+ /** The gate document file name inside {@link MCP_ADAPTER_STORAGE_DIRNAME}. */
98
+ export declare const GATE_FILE_NAME = "gate.json";
93
99
  /**
94
- * The persisted gate state (one settings section). `serverIds` maps every
100
+ * The persisted gate state (one gate.json document). `serverIds` maps every
95
101
  * OBSERVED server name to its stable id and is never pruned — enable/disable
96
102
  * never recycles an id, so an id always means the same server, even across
97
103
  * restarts and temporary re-sync gaps.
@@ -105,8 +111,8 @@ export interface ServerIdRegistry {
105
111
  /**
106
112
  * Read-side snapshot of the registry the three latches consult. Structurally
107
113
  * a superset of {@link ServerIdRegistry}; everything (config objects included)
108
- * shaped like this qualifies, so production hands frozen `scope.get()` values
109
- * straight in.
114
+ * shaped like this qualifies, so production hands the gate store's frozen
115
+ * mirror straight in.
110
116
  */
111
117
  export interface ServerGateState {
112
118
  readonly serverIds: Readonly<Record<string, number>>;
@@ -114,8 +120,6 @@ export interface ServerGateState {
114
120
  }
115
121
  /** Gate snapshot meaning "nothing observed, nothing disabled". */
116
122
  export declare const EMPTY_SERVER_GATE: ServerGateState;
117
- /** Schema of the persisted `mcp-adapter` settings section. */
118
- export declare const SERVER_ID_REGISTRY_SCHEMA: z<ServerIdRegistry>;
119
123
  /**
120
124
  * Coerce any resolved settings value into a trustworthy gate snapshot. The
121
125
  * schema already validates at resolve time, but every latch judges from THIS
@@ -163,21 +167,134 @@ export interface ServerToggleOutcome {
163
167
  * removes the id mapping — re-disabling later reuses the same number.
164
168
  */
165
169
  export declare function setServerDisabledState(state: ServerGateState | undefined, server: string, disable: boolean): ServerToggleOutcome;
166
- /** Resolved adapter configuration. */
170
+ /**
171
+ * The live gate handle production hands to the three latches and the /mcp
172
+ * command: an in-memory mirror loaded once at apply time, written through to
173
+ * the gate file on every persist. Reads never throw and never touch the disk;
174
+ * writes may (and the command surface turns a failed write into a structured
175
+ * error while the mirror keeps its previous value — "state unchanged").
176
+ */
177
+ export interface GateStore {
178
+ /** Current normalized snapshot (corrupt input already sanitized away). */
179
+ get(): ServerGateState;
180
+ /** Persist one complete next registry; the mirror moves only after the file landed. */
181
+ write(next: ServerIdRegistry): Promise<void>;
182
+ }
183
+ /** The warning seam {@link createFileGateStore} reports boot-time problems through. */
184
+ export type GateWarn = (message: string) => void;
185
+ /** Resolve the dsh home (`DSH_HOME`, else `~/.dsh`) — same seam as dsh-cron/dsh-vault. */
186
+ export declare function resolveDshHome(): string;
187
+ /**
188
+ * Absolute gate directory for one `storageDir` config value: empty (default)
189
+ * means `<dsh home>/storages/mcp-adapter`, anything else is the override used
190
+ * verbatim (resolved).
191
+ */
192
+ export declare function resolveGateDir(configured: string): string;
193
+ /**
194
+ * Build the production gate store over one directory. The initial read is
195
+ * synchronous (boot-time, once); writes land atomically (tmp file + rename)
196
+ * so a crash mid-write can never leave a torn document, and the in-memory
197
+ * mirror only advances after the file is durably renamed into place.
198
+ *
199
+ * Legacy absorption (one-time, 0.1.5 → 0.1.7): on a FRESH gate — no
200
+ * gate.json yet — the machine state from the removed settings registry is
201
+ * absorbed once from the host's legacy settings documents (see
202
+ * {@link absorbLegacyGate}). Opt-in through `options.home`: the legacy
203
+ * documents are a HOME-level concern (not this store's `dir`), and stores
204
+ * built without a home (bare test harnesses) stay exactly as isolated as
205
+ * before.
206
+ */
207
+ export declare function createFileGateStore(dir: string, warn: GateWarn, options?: GateStoreOptions): GateStore;
208
+ /**
209
+ * dsh 0.1.7 renamed the old `<home>/settings.yaml` to
210
+ * `settings.yaml.imported` after a one-shot import keyed by "section name =
211
+ * entry id". This plugin's old `mcp-adapter:` section (the machine-maintained
212
+ * id registry / disabled set) does not match the entry id `dsh-mcp-adapter`
213
+ * and was silently dropped by the host — the state lives on only in the
214
+ * renamed document. {@link absorbLegacyGate} recovers it once into the
215
+ * plugin's own gate.json.
216
+ */
217
+ /** Legacy host settings documents, in read priority order. */
218
+ export declare const LEGACY_SETTINGS_FILES: readonly ["settings.yaml.imported", "settings.yaml"];
219
+ /** Legacy top-level section name in the old settings document. */
220
+ export declare const LEGACY_GATE_SECTION = "mcp-adapter";
221
+ /** Audit marker file name: `<home>/storages/mcp-adapter/<name>`. */
222
+ export declare const LEGACY_IMPORT_MARKER = "legacy-import.json";
223
+ /** Marker outcomes written for every settled absorption pass. */
224
+ export type LegacyGateOutcome = 'imported' | 'no-op' | 'no-legacy' | 'no-section';
225
+ /**
226
+ * Extract the top-level `mcp-adapter:` section of the old settings document.
227
+ * Handles both shapes the 0.1.5 writer produced:
228
+ * - single-line values (`serverIds: '{"fs": 1}'`, `disabled: '[1]'`);
229
+ * - block collections (`serverIds:` followed by deeper-indented
230
+ * `name: <int>` lines; `disabled:` followed by `- <int>` lines).
231
+ * Everything else (comments, folded scalars, nested junk) is skipped.
232
+ * undefined = no section found. Pure function — tests run it against
233
+ * literal strings.
234
+ */
235
+ export declare function legacyGateSection(text: string): {
236
+ serverIds?: unknown;
237
+ disabled?: unknown;
238
+ } | undefined;
239
+ /**
240
+ * Turn one decoded section into the raw candidate {@link normalizeServerGate}
241
+ * sanitizes: flow-JSON strings are JSON-parsed (malformed → undefined, the
242
+ * key degrades to "absent"), already-decoded block collections pass through.
243
+ */
244
+ export declare function legacyGateCandidate(section: {
245
+ serverIds?: unknown;
246
+ disabled?: unknown;
247
+ }): {
248
+ serverIds?: unknown;
249
+ disabled?: unknown;
250
+ };
251
+ /** Construction options for {@link createFileGateStore} beyond the two
252
+ * historical arguments (all optional — bare harnesses change nothing). */
253
+ export interface GateStoreOptions {
254
+ /** Summary-line seam for the absorption (default: silent). */
255
+ info?: GateWarn;
256
+ /**
257
+ * The dsh home holding the legacy settings documents and the audit marker
258
+ * (production: `resolveDshHome()` at the call site). Absorption runs ONLY
259
+ * when a home is provided — a store built without one never reads outside
260
+ * its own directory.
261
+ */
262
+ home?: string;
263
+ }
264
+ /**
265
+ * Live config reference. 0.1.7 settings contract: volatile `Config` fields
266
+ * reach `apply` as host-swapped references — `.get()` snapshots the current
267
+ * value, and a settings-page write swaps the value in place WITHOUT remounting
268
+ * the plugin, so every consumer must read through the reference per use.
269
+ */
270
+ export interface VolatileRef<T> {
271
+ get(): T;
272
+ }
273
+ /**
274
+ * Resolved adapter configuration as `apply` receives it. Every knob is
275
+ * user-tunable from the settings page (entry `dsh-mcp-adapter`) and hot-reloads
276
+ * without a plugin restart.
277
+ */
167
278
  export interface AdapterConfig {
168
279
  /** Tool-name prefix to fold out of assembled prompts. */
169
- prefix: string;
280
+ prefix: VolatileRef<string>;
170
281
  /** Name patterns ("*" wildcard) kept native in the prompt. */
171
- keep: string[];
282
+ keep: VolatileRef<string[]>;
172
283
  /**
173
284
  * Server-name whitelist. Empty (default) folds every prefix-matching tool —
174
285
  * the prefix is then a naming convention, not a trust boundary. Non-empty
175
286
  * folds/catalogs/dispatches ONLY tools of the listed servers; fold, catalog
176
287
  * and dispatch all consult the same list.
177
288
  */
178
- servers: string[];
289
+ servers: VolatileRef<string[]>;
179
290
  /** Max chars per tool description in the mcp_list catalog. */
180
- descriptionLimit: number;
291
+ descriptionLimit: VolatileRef<number>;
292
+ /**
293
+ * Gate storage directory override; empty (default) =
294
+ * `<dsh home>/storages/mcp-adapter`. Read once at apply time (the store is
295
+ * built there); changing it takes effect on the next plugin restart.
296
+ */
297
+ storageDir: VolatileRef<string>;
181
298
  }
182
299
  export declare const Config: z<AdapterConfig>;
183
300
  /**
@@ -264,9 +381,9 @@ export interface McpListOptions {
264
381
  /** Server-name whitelist; empty or omitted admits every server. */
265
382
  servers?: readonly string[];
266
383
  /**
267
- * Live gate reader (production: fresh `settingsScope.get()` snapshot per
268
- * execution). A callback, not a value: the factory is built once at apply
269
- * time and must observe later /mcp disable/enable writes.
384
+ * Live gate reader (production: the file-backed gate store's current
385
+ * mirror per execution). A callback, not a value: the factory is built
386
+ * once at apply time and must observe later /mcp disable/enable writes.
270
387
  */
271
388
  getGate?: () => ServerGateState | undefined;
272
389
  }
@@ -403,10 +520,10 @@ export interface McpCommandOptions {
403
520
  /** Max chars per tool description in rendered listings. */
404
521
  descriptionLimit: number;
405
522
  /**
406
- * Live-resolved gate snapshot at invocation time (undefined = the settings
407
- * service is not composed, so ids/toggles are absent from all views and
408
- * everything renders enabled). A snapshot per invocation keeps these
409
- * renderers pure and never stale within one command execution.
523
+ * Live-resolved gate snapshot at invocation time (undefined = no gate store
524
+ * is available, so ids/toggles are absent from all views and everything
525
+ * renders enabled). A snapshot per invocation keeps these renderers pure
526
+ * and never stale within one command execution.
410
527
  */
411
528
  gate?: ServerGateState;
412
529
  /**
@@ -485,7 +602,7 @@ export interface McpHealthStats {
485
602
  *
486
603
  * @param schemas - schemas visible to the receiving agent.
487
604
  * @param options - adapter knobs (prefix/keep/servers) plus the live gate
488
- * snapshot (`gate`) when the settings service is composed.
605
+ * snapshot (`gate`) when a gate store is available.
489
606
  * @param metaToolsLive - whether both meta-tools resolve to this plugin's
490
607
  * definitions in the receiving agent's scope.
491
608
  * @returns the folding health plus the partition behind it.
@@ -615,16 +732,15 @@ export interface McpCommandView {
615
732
  /** Whether both meta-tools resolve to this plugin's definitions there. */
616
733
  metaToolsLive: boolean;
617
734
  /**
618
- * Gate snapshot resolved when the invocation started; `undefined` means the
619
- * dsh settings service is not composed in this process (stable ids and
620
- * toggles are unavailable; views degrade to enabled-everything).
735
+ * Gate snapshot resolved when the invocation started; `undefined` means no
736
+ * gate store is available in this process (stable ids and toggles are
737
+ * unavailable; views degrade to enabled-everything).
621
738
  */
622
739
  gate?: ServerGateState;
623
740
  /**
624
- * Persist one complete next registry (production: a wholesale `replace` of
625
- * the `mcp-adapter` settings section, mirroring agent-default-model's
626
- * save-selection precedent). Absent ⇒ /mcp disable/enable answers with an
627
- * explanatory error instead of silently dropping the intent.
741
+ * Persist one complete next registry (production: an atomic write of the
742
+ * plugin's gate.json document). Absent ⇒ /mcp disable/enable answers with
743
+ * an explanatory error instead of silently dropping the intent.
628
744
  */
629
745
  writeGate?: (next: ServerIdRegistry) => Promise<void>;
630
746
  /**
@@ -707,7 +823,11 @@ export declare function renderDispatched(metaArgs: unknown, value: unknown, reso
707
823
  export declare function delegateFinalizeContent(metaArgs: unknown, result: Readonly<ToolExecutionResult>, exec: Readonly<ToolExecution>, resolveChild: (name: string) => ToolDefinition | undefined): ContentBlock[] | undefined;
708
824
  /**
709
825
  * Build the `mcp_list` meta-tool definition.
710
- * @param options - catalog knobs (prefix, description limit).
826
+ * @param options - catalog knobs (prefix, description limit). MAY be a live
827
+ * options object (plain getters): the factory reads `prefix`, `servers`,
828
+ * `descriptionLimit` and `getGate` through the object on EVERY execution,
829
+ * so volatile config can be wired by reference — only the description text
830
+ * freezes at factory time.
711
831
  * @param listSchemas - visible-schemas source (production: `ctx.tools.schemas`,
712
832
  * called with the calling agent so restrictions are respected).
713
833
  * @returns the complete tool definition.
@@ -715,17 +835,20 @@ export declare function delegateFinalizeContent(metaArgs: unknown, result: Reado
715
835
  export declare function createMcpListTool(options: McpListOptions, listSchemas: (scope?: ScopeKey) => readonly ToolSchema[]): ToolDefinition;
716
836
  /**
717
837
  * Build the `mcp_call` meta-tool definition.
718
- * @param prefix - the configured MCP prefix (dispatch boundary).
838
+ * @param prefix - the configured MCP prefix (dispatch boundary). A thunk is
839
+ * accepted so volatile config is re-read on every dispatch; a plain string
840
+ * freezes the boundary at factory time.
719
841
  * @param resolve - scope-aware definition resolver (production:
720
842
  * `ctx.tools.get`, called with the calling agent).
721
- * @param servers - server-name whitelist (default: no filtering).
722
- * @param getGate - live disabled-server reader (production: fresh
723
- * `settingsScope.get()` snapshot per dispatch; default/omitted: nothing is
724
- * disabled). Deliberately a callback — the definition object outlives any
725
- * one settings snapshot and must observe later /mcp enable/disable writes.
843
+ * @param servers - server-name whitelist (default: no filtering). A thunk is
844
+ * accepted for live volatile config, same as `prefix`.
845
+ * @param getGate - live disabled-server reader (production: the file-backed
846
+ * gate store's current mirror; default/omitted: nothing is disabled).
847
+ * Deliberately a callback — the definition object outlives any one gate
848
+ * snapshot and must observe later /mcp enable/disable writes.
726
849
  * @returns the complete tool definition.
727
850
  */
728
- export declare function createMcpCallTool(prefix: string, resolve: (name: string, scope?: ScopeKey) => ToolDefinition | undefined, servers?: readonly string[], getGate?: () => ServerGateState | undefined): ToolDefinition;
851
+ export declare function createMcpCallTool(prefix: string | (() => string), resolve: (name: string, scope?: ScopeKey) => ToolDefinition | undefined, servers?: readonly string[] | (() => readonly string[]), getGate?: () => ServerGateState | undefined): ToolDefinition;
729
852
  /**
730
853
  * Register the two meta-tools and install the assembly folding listener.
731
854
  *
@@ -733,19 +856,25 @@ export declare function createMcpCallTool(prefix: string, resolve: (name: string
733
856
  * both registrations are rolled back, a warning is logged, and NO listener
734
857
  * is installed — the official full-schema passthrough stays intact.
735
858
  *
736
- * Server gating: when a dsh settings service is composed, the `mcp-adapter`
737
- * namespace is registered on it and every gate consumer reads FRESH resolved
738
- * values through small callbacks — the plugin keeps no in-memory copy that
739
- * could drift from settings. The read itself is defended too: if the
740
- * resolved-value lookup throws, consumers see an absent gate (everything
741
- * enabled) instead of the error, keeping the assemble waterfall alive.
859
+ * Server gating: the gate mirror loads synchronously once from the plugin's
860
+ * gate file (`resolveGateDir(config.storageDir.get())`), and every gate
861
+ * consumer reads the CURRENT mirror through {@link getGate} — the plugin
862
+ * keeps no stale copy that could drift from /mcp writes. A corrupt or
863
+ * unreadable document degrades to the empty gate (everything enabled) at
864
+ * load time, keeping the assemble waterfall alive.
865
+ *
866
+ * Config volatility: every knob (prefix/keep/servers/descriptionLimit) is a
867
+ * volatile reference — 0.1.7 settings-page writes swap values in place
868
+ * WITHOUT remounting the plugin, so nothing captured at apply time may go
869
+ * stale: the waterfall, both meta-tools and the /mcp command all re-read
870
+ * through {@link cfgNow} per use.
742
871
  *
743
- * Commands: `/mcp` mounts through a runtime `ctx.inject(['commands'], ...)`
744
- * (same seam as settings), so hosts without the commands service keep this
745
- * plugin fully functional — one warning, no command. A registration name
746
- * conflict degrades to the same warning independently.
872
+ * Commands: `/mcp` mounts through a runtime `ctx.inject(['commands'], ...)`,
873
+ * so hosts without the commands service keep this plugin fully functional —
874
+ * one warning, no command. A registration name conflict degrades to the same
875
+ * warning independently.
747
876
  *
748
877
  * @param ctx - plugin context carrying the tool registry.
749
- * @param config - resolved adapter configuration.
878
+ * @param config - resolved adapter configuration (volatile live references).
750
879
  */
751
880
  export declare function apply(ctx: Context, config: AdapterConfig): void;