@aiwayds/dsh-mcp-adapter 0.1.1 → 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/README.md CHANGED
@@ -81,6 +81,26 @@ config:
81
81
  - Coexists with [ben7am1n/dsh-mcp-proxy](https://github.com/ben7am1n/dsh-mcp-proxy) (connection-side proxy with its own servers — different, non-colliding tool names). That project credits pi-mcp-adapter as prior art too; this repo is an independent prompt-side take that reuses the official client instead of re-implementing connections.
82
82
  - Trade-offs (same as pi-mcp-adapter): one extra discovery round-trip before the first call, and expanded schemas still occupy context once the model pulls them in.
83
83
 
84
+ ## Commands
85
+
86
+ The plugin registers one slash command on the platform `commands` service — consumed softly, so a host without that service still gets folding and both meta-tools (one log warning instead of `/mcp`). `/mcp` reports status, and since v0.2.0 it is also the control surface for taking a whole MCP server in or out of the adapter:
87
+
88
+ | Form | Output |
89
+ |---|---|
90
+ | `/mcp` or `/mcp list` | Tree overview — every server line carries its stable `[<id>]`; disabled ones show `⏸ disabled` and hide their tools; closed by a folding-health footer |
91
+ | `/mcp list <name>` | `<name>` matching a server → that server's full tool list (a disabled server adds a `⏸` note); matching a full tool name → full description + complete input schema |
92
+ | `/mcp config` | The effective `prefix` / `keep` / `servers` / `descriptionLimit`, each with its hitting tools, plus the persistent enable/disable inventory |
93
+ | `/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 |
94
+ | `/mcp enable <id>` | Restore it under the same stable id |
95
+
96
+ 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).
97
+
98
+ 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.
99
+
100
+ 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()`.
101
+
102
+ Status remains **two-state**: a server appears when its tools are visible in your scope — absence does not prove it is disabled (it may be reconnecting); the official client exposes no connection state. The footer reads `meta-tools: mcp_list/mcp_call live · folding ACTIVE — folded N, kept M · ~X chars of schema out of prompt` while the meta-tools are live and at least one tool folds, degrading to a fail-open (or nothing-to-fold) notice otherwise. X counts raw JSON-schema characters, deliberately not tokens. Output beyond 400 lines is truncated with a hint to narrow via `/mcp list <server>`.
103
+
84
104
  ## Acknowledgments
85
105
 
86
106
  With full credit to **[pi-mcp-adapter](https://github.com/nicobailon/pi-mcp-adapter)** by [@nicobailon](https://github.com/nicobailon): the core idea behind this plugin — collapsing an unbounded MCP tool surface into constant meta-tools whose schemas expand on demand, so the standing prompt cost stays O(1) no matter how many servers you run — is entirely theirs, and it reframed what MCP integration should cost. This repository is our port of that idea to DeepSeek Harness; the prompt-side-shim mechanism differs (by design), but the inspiration and the concept belong to the original. If you are on pi, go use theirs.
package/README.zh.md CHANGED
@@ -73,6 +73,26 @@ config:
73
73
  - 与 [ben7am1n/dsh-mcp-proxy](https://github.com/ben7am1n/dsh-mcp-proxy) 可共存(它是 connection-side 代理、自带连接管理,工具名互不冲突)。该项目同样致谢 pi-mcp-adapter;本仓库是独立的 prompt-side 实现:复用官方 client 而不是重造连接层。
74
74
  - 权衡(与 pi-mcp-adapter 相同):首次调用多一次发现往返;模型展开过的 schema 会占据后续上下文。
75
75
 
76
+ ## 命令
77
+
78
+ 本插件在平台 `commands` 服务上注册一条斜杠命令——该依赖是软性的:宿主若没有命令服务,折叠与两个 meta-tool 照常工作(只记一条日志警告,代价是没有 `/mcp`)。`/mcp` 展示状态;v0.2.0 起它同时也是整个 MCP server 进/出适配器的唯一控制面:
79
+
80
+ | 形态 | 输出 |
81
+ |---|---|
82
+ | `/mcp` 或 `/mcp list` | 树形总览——每行 server 带 stable `[<id>]` 前缀;disabled 的标 `⏸ disabled` 且不列工具;尾部附折叠健康行 |
83
+ | `/mcp list <name>` | `<name>` 匹配某 server → 该 server 全部工具(disabled 的附加 `⏸` 说明行);匹配完整工具名 → 完整描述 + 完整 input schema |
84
+ | `/mcp config` | 当前生效的 `prefix` / `keep` / `servers` / `descriptionLimit` 及各自命中清单,外加持久 enable/disable 台账 |
85
+ | `/mcp disable <id>` | 把一个 server 整体闩上:工具强制折叠出 prompt(keep 与 `servers` 豁免一并覆盖)、从 `mcp_list` 目录消失、`mcp_call` 拒绝并给 `/mcp enable <id>` 指引 |
86
+ | `/mcp enable <id>` | 用同一个 stable id 复原 |
87
+
88
+ 其余形态一律回复用法说明。`/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`,受影响的分组行会带标记)。
89
+
90
+ disable 是**门闩式开关,不是真断连**:官方 client 不提供断连 API,工具仍留在注册表里、连接照常运行——门闩只把它请出 prompt、目录与分发。三层门闩共用同一个判定函数,彼此之间以及与 `/mcp` 的展示永远不会口径不一。enable/disable 经 settings 服务持久化;没有 settings 服务时其余功能照常,只有 toggle 会回一条说明性报错。
91
+
92
+ 一条真实边界:门闩生效在 prompt 侧(目录/分发层);记得完整工具名的模型仍可能原生直调 `mcp__server__tool` 成功——需要硬性拦截时,请配合管线 guard 或 `tools.restrict()`。
93
+
94
+ 状态是**二态语义**:server 出现在列表里 = 它的工具在本 scope 可见——不可见不代表未启用(可能正在重连退避);官方 client 不暴露连接状态。健康行形如 `meta-tools: mcp_list/mcp_call live · folding ACTIVE — folded N, kept M · ~X chars of schema out of prompt`(meta-tools 存活且至少折叠一个工具时),否则降级为 fail-open(或无可折叠)提示。X 是被折叠 schema 的 JSON 字符数,刻意标注为字符而非 token。输出超过 400 行会被截断,并提示用 `/mcp list <server>` 收窄。
95
+
76
96
  ## 致谢
77
97
 
78
98
  特别致敬 **[pi-mcp-adapter](https://github.com/nicobailon/pi-mcp-adapter)** 及其作者 [@nicobailon](https://github.com/nicobailon):本插件的核心思想——把无界的 MCP 工具面折叠为恒定的 meta-tool、schema 按需展开,从而无论挂多少 server 常驻 prompt 成本都是 O(1)——完全来自该项目;正是它重新定义了 MCP 集成应有的代价。本仓库是这个理念在 DeepSeek Harness 上的移植,机制上刻意走了不同的路线(复用官方连接层的 prompt-side shim),但灵感与理念的功劳归于原作。如果你用的是 pi,请直接用原版。
package/lib/index.d.ts CHANGED
@@ -25,6 +25,17 @@
25
25
  * child tool's name never fire for folded calls (the registry only sees the
26
26
  * outer `mcp_call`) — gate MCP usage by guarding `mcp_call` itself.
27
27
  *
28
+ * A read-only `/mcp` slash command (platform `commands` service) surfaces the
29
+ * live state human-side: the server/tool tree, per-server/per-tool details,
30
+ * the effective configuration, and whether folding is active, idle, or in
31
+ * fail-open passthrough. The same command carries the one mutating surface:
32
+ * `/mcp disable <id>` / `/mcp enable <id>` gate a whole server behind all
33
+ * three latches at once — its tools force-fold out of every prompt (keep and
34
+ * `servers` whitelist exemptions included), it disappears from the catalog,
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.
38
+ *
28
39
  * Child-owned projections are preserved: `mcp_call` delegates its
29
40
  * `output.render` to the dispatched child and forwards the child's
30
41
  * `finalizeContent` with the exact same run-execution object, so
@@ -46,14 +57,108 @@ import z from '@deepseek-ai/schemastery';
46
57
  import type { ContentBlock, ToolSchema } from '@deepseek-ai/dsh-llm';
47
58
  import type { ToolDefinition, ToolExecution, ToolExecutionResult } from '@deepseek-ai/dsh-tools';
48
59
  import type { ScopeKey } from '@deepseek-ai/dsh-scope';
60
+ import type { SettingsNamespace } from '@deepseek-ai/dsh-settings';
49
61
  /** Cordis plugin name used by loader diagnostics. */
50
62
  export declare const name = "mcp-adapter";
51
- /** Services required by this plugin. */
63
+ /**
64
+ * Services required by this plugin — deliberately just `tools`. The platform
65
+ * `commands` service is consumed SOFTLY at runtime ({@link apply} mounts it
66
+ * via `ctx.inject`): listing it here would refuse to load the whole plugin on
67
+ * hosts that do not compose commands, taking folding down with the command.
68
+ */
52
69
  export declare const inject: string[];
70
+ /** Registered slash-command name (read-only status surface). */
71
+ export declare const MCP_COMMAND_NAME = "mcp";
72
+ /** Max rendered lines of any /mcp output before truncation kicks in. */
73
+ export declare const MCP_OUTPUT_LINE_LIMIT = 400;
74
+ /**
75
+ * Description cap for TREE views (`/mcp` overview and server listing): keep
76
+ * each tool to one readable line. The configurable `descriptionLimit`
77
+ * remains the ceiling for catalog-shaped data (`mcp_list`, `/mcp config`);
78
+ * full multi-paragraph descriptions belong in `/mcp list <tool>` detail.
79
+ */
80
+ export declare const TREE_DESCRIPTION_LIMIT = 100;
53
81
  /** Registered name of the catalog meta-tool. */
54
82
  export declare const MCP_LIST_TOOL_NAME = "mcp_list";
55
83
  /** Registered name of the dispatch meta-tool. */
56
84
  export declare const MCP_CALL_TOOL_NAME = "mcp_call";
85
+ /** Upper bound of the stable per-server id space. */
86
+ export declare const MCP_SERVER_ID_LIMIT = 99;
87
+ /** dsh settings namespace persisting the stable ids and the disabled set. */
88
+ export declare const MCP_ADAPTER_SETTINGS_NAMESPACE: SettingsNamespace;
89
+ /**
90
+ * The persisted gate state (one settings section). `serverIds` maps every
91
+ * OBSERVED server name to its stable id and is never pruned — enable/disable
92
+ * never recycles an id, so an id always means the same server, even across
93
+ * restarts and temporary re-sync gaps.
94
+ */
95
+ export interface ServerIdRegistry {
96
+ /** Observed server name -> stable id (1..99), assigned smallest-free-first. */
97
+ serverIds: Record<string, number>;
98
+ /** Ids currently gated off; entries whose mapping exists nowhere are inert. */
99
+ disabled: number[];
100
+ }
101
+ /**
102
+ * Read-side snapshot of the registry the three latches consult. Structurally
103
+ * a superset of {@link ServerIdRegistry}; everything (config objects included)
104
+ * shaped like this qualifies, so production hands frozen `scope.get()` values
105
+ * straight in.
106
+ */
107
+ export interface ServerGateState {
108
+ readonly serverIds: Readonly<Record<string, number>>;
109
+ readonly disabled: readonly number[];
110
+ }
111
+ /** Gate snapshot meaning "nothing observed, nothing disabled". */
112
+ export declare const EMPTY_SERVER_GATE: ServerGateState;
113
+ /** Schema of the persisted `mcp-adapter` settings section. */
114
+ export declare const SERVER_ID_REGISTRY_SCHEMA: z<ServerIdRegistry>;
115
+ /**
116
+ * Coerce any resolved settings value into a trustworthy gate snapshot. The
117
+ * schema already validates at resolve time, but every latch judges from THIS
118
+ * function's output, so corrupt or hand-edited documents degrade to "some
119
+ * entries ignored" instead of wedging folding or dispatch.
120
+ */
121
+ export declare function normalizeServerGate(raw: unknown): ServerGateState;
122
+ /**
123
+ * THE shared disabled verdict — all three latches (waterfall fold, catalog,
124
+ * mcp_call dispatch) and every /mcp renderer judge through this one function,
125
+ * so the gating can never disagree with itself. An unmapped server (or an
126
+ * absent snapshot, or the degenerate empty server segment) is ENABLED:
127
+ * gating requires positive evidence, keeping fail-open direction.
128
+ */
129
+ export declare function isServerDisabled(server: string, gate?: ServerGateState): boolean;
130
+ /** Outcome of {@link allocateMissingServerIds}. */
131
+ export interface ServerIdAllocation {
132
+ /** Next registry: a fresh object; inputs are never mutated. */
133
+ registry: ServerIdRegistry;
134
+ /** Servers left unassigned because the 1..99 space ran out. */
135
+ unassigned: string[];
136
+ /** Names that received a NEW id in this pass (empty in steady state). */
137
+ added: string[];
138
+ }
139
+ /**
140
+ * Assign the smallest free id (1..99) to every listed server not yet mapped.
141
+ * Existing mappings are kept verbatim (id stability across restarts); the
142
+ * allocation never mutates its input. Call this whenever /mcp observes the
143
+ * live server set, then persist `{@link ServerIdAllocation.registry}` when
144
+ * `added` is non-empty.
145
+ */
146
+ export declare function allocateMissingServerIds(state: ServerGateState | undefined, servers: readonly string[]): ServerIdAllocation;
147
+ /** Reverse lookup: which server (if any) carries this id today. */
148
+ export declare function serverNameById(registry: ServerGateState | undefined, id: number): string | undefined;
149
+ /** Outcome of {@link setServerDisabledState}. */
150
+ export interface ServerToggleOutcome {
151
+ /** Next registry (fresh object); unchanged content when nothing applied. */
152
+ registry: ServerIdRegistry;
153
+ /** Whether the disabled set actually moved. */
154
+ changed: boolean;
155
+ }
156
+ /**
157
+ * Apply one enable/disable toggle for a MAPPED server onto a copy of the
158
+ * registry. Unmapped servers are no-ops (`changed: false`). Enabling never
159
+ * removes the id mapping — re-disabling later reuses the same number.
160
+ */
161
+ export declare function setServerDisabledState(state: ServerGateState | undefined, server: string, disable: boolean): ServerToggleOutcome;
57
162
  /** Resolved adapter configuration. */
58
163
  export interface AdapterConfig {
59
164
  /** Tool-name prefix to fold out of assembled prompts. */
@@ -96,13 +201,19 @@ export declare function isAllowedServer(name: string, prefix: string, servers: r
96
201
  * keep list — and it must never be one of this plugin's own meta-tools,
97
202
  * whatever the prefix is configured to (folding them would make every folded
98
203
  * MCP tool undiscoverable).
204
+ *
205
+ * Gate override: when the tool's server is DISABLED
206
+ * ({@link isServerDisabled}, shared with catalog/dispatch), its schema folds
207
+ * OUT regardless of keep patterns or the servers whitelist — disablement is
208
+ * strictly stronger than any config exemption.
99
209
  * @param name - the registered tool name.
100
210
  * @param prefix - tool-name prefix to fold.
101
211
  * @param keep - keep patterns from config.
102
212
  * @param servers - server-name whitelist from config (default: no filtering).
213
+ * @param gate - live disabled-server snapshot (default/undefined: none).
103
214
  * @returns whether the schema leaves the prompt (the tool stays callable).
104
215
  */
105
- export declare function shouldFold(name: string, prefix: string, keep: readonly string[], servers?: readonly string[]): boolean;
216
+ export declare function shouldFold(name: string, prefix: string, keep: readonly string[], servers?: readonly string[], gate?: ServerGateState): boolean;
106
217
  /** Folding knobs resolved from config. */
107
218
  export interface FoldOptions {
108
219
  /** Tool-name prefix to fold. */
@@ -135,9 +246,11 @@ export interface AssemblyWithTools {
135
246
  * @param options - folding knobs.
136
247
  * @param metaToolsLive - whether both meta-tools currently resolve to this
137
248
  * plugin's own definitions in the assembling scope.
249
+ * @param gate - live disabled-server snapshot, fresh per assembly (default:
250
+ * none disabled).
138
251
  * @returns the folded assembly, or the input unchanged when nothing folds.
139
252
  */
140
- export declare function foldPromptAssembly<T extends AssemblyWithTools>(assembly: T, options: FoldOptions, metaToolsLive: boolean): T;
253
+ export declare function foldPromptAssembly<T extends AssemblyWithTools>(assembly: T, options: FoldOptions, metaToolsLive: boolean, gate?: ServerGateState): T;
141
254
  /** Options for the catalog builder. */
142
255
  export interface McpListOptions {
143
256
  /** Tool-name prefix identifying MCP tools. */
@@ -146,6 +259,12 @@ export interface McpListOptions {
146
259
  descriptionLimit: number;
147
260
  /** Server-name whitelist; empty or omitted admits every server. */
148
261
  servers?: readonly string[];
262
+ /**
263
+ * Live gate reader (production: fresh `settingsScope.get()` snapshot per
264
+ * execution). A callback, not a value: the factory is built once at apply
265
+ * time and must observe later /mcp disable/enable writes.
266
+ */
267
+ getGate?: () => ServerGateState | undefined;
149
268
  }
150
269
  /** Normalized `mcp_list` arguments. */
151
270
  export interface McpListArgs {
@@ -200,10 +319,13 @@ export declare function serverOfToolName(name: string, prefix: string): string;
200
319
  * Truncate one description to the catalog limit, marking the cut with an
201
320
  * ellipsis character (so one char of headroom is reserved). A cut that would
202
321
  * land on the LOW half of a surrogate pair backs off one code unit so no
203
- * orphaned high surrogate is emitted.
322
+ * orphaned high surrogate is emitted. Whitespace runs (newlines, indents)
323
+ * are collapsed to single spaces FIRST — MCP vendors routinely ship
324
+ * multi-paragraph usage guides as descriptions and catalog lines must stay
325
+ * single-line.
204
326
  * @param text - the full tool description.
205
327
  * @param limit - max chars in the catalog.
206
- * @returns the truncated description.
328
+ * @returns the single-line truncated description.
207
329
  */
208
330
  export declare function truncateDescription(text: string, limit: number): string;
209
331
  /**
@@ -217,9 +339,10 @@ export declare function truncateDescription(text: string, limit: number): string
217
339
  * @param args - the normalized catalog query.
218
340
  * @param schemas - schemas visible to the calling agent.
219
341
  * @param options - catalog knobs.
342
+ * @param gate - live disabled-server snapshot (default: none disabled).
220
343
  * @returns the canonical result value.
221
344
  */
222
- export declare function buildMcpListResult(args: McpListArgs, schemas: readonly ToolSchema[], options: McpListOptions): McpListResult;
345
+ export declare function buildMcpListResult(args: McpListArgs, schemas: readonly ToolSchema[], options: McpListOptions, gate?: ServerGateState): McpListResult;
223
346
  /** Structural slice of a resolvable tool: what dispatch actually consumes. */
224
347
  export interface DispatchableTool<E> {
225
348
  /** Cooperative timeout budget declared by the tool, if any. */
@@ -258,9 +381,275 @@ export interface DispatchableTool<E> {
258
381
  * @param resolve - scope-aware tool resolver (production: `ctx.tools.get`).
259
382
  * @param exec - the meta-tool's run context, forwarded to the child.
260
383
  * @param servers - server-name whitelist (default: no filtering).
384
+ * @param gate - live disabled-server snapshot (default: none disabled). The
385
+ * disabled branch sits after the prefix/whitelist validation: a disabled
386
+ * server's tools are refused with an explicit `/mcp enable <id>` hint
387
+ * before the child is ever resolved.
261
388
  * @returns the child's resolved value verbatim, or a structured error.
262
389
  */
263
- export declare function dispatchMcpCall<E>(args: unknown, prefix: string, resolve: (name: string) => DispatchableTool<E> | undefined, exec: E, servers?: readonly string[]): Promise<unknown>;
390
+ export declare function dispatchMcpCall<E>(args: unknown, prefix: string, resolve: (name: string) => DispatchableTool<E> | undefined, exec: E, servers?: readonly string[], gate?: ServerGateState): Promise<unknown>;
391
+ /** Knobs shared by every /mcp renderer — plain data, no Cordis. */
392
+ export interface McpCommandOptions {
393
+ /** Tool-name prefix identifying MCP tools. */
394
+ prefix: string;
395
+ /** Name patterns kept native in the prompt ("*" wildcard). */
396
+ keep: readonly string[];
397
+ /** Server-name whitelist; empty or omitted admits every server. */
398
+ servers?: readonly string[];
399
+ /** Max chars per tool description in rendered listings. */
400
+ descriptionLimit: number;
401
+ /**
402
+ * Live-resolved gate snapshot at invocation time (undefined = the settings
403
+ * service is not composed, so ids/toggles are absent from all views and
404
+ * everything renders enabled). A snapshot per invocation keeps these
405
+ * renderers pure and never stale within one command execution.
406
+ */
407
+ gate?: ServerGateState;
408
+ /**
409
+ * Live server names left WITHOUT a stable id because the 1..99 space was
410
+ * full when they appeared (production:
411
+ * {@link ServerIdAllocation.unassigned}). Views — not an erroring toggle —
412
+ * surface that fact, since a server without an id cannot be targeted by
413
+ * one in the first place.
414
+ */
415
+ unassignedServers?: readonly string[];
416
+ }
417
+ /** Usage text returned for unparseable /mcp invocations. */
418
+ export declare const MCP_COMMAND_USAGE: string;
419
+ /** One parsed /mcp invocation. */
420
+ export type ParsedMcpCommandInput = {
421
+ form: 'overview';
422
+ } | {
423
+ form: 'detail';
424
+ name: string;
425
+ } | {
426
+ form: 'config';
427
+ } | {
428
+ form: 'disable';
429
+ id: number;
430
+ } | {
431
+ form: 'enable';
432
+ id: number;
433
+ } | {
434
+ form: 'usage';
435
+ };
436
+ /**
437
+ * Parse one /mcp raw input into its form. `''` and `'list'` are the tree
438
+ * overview; `'list <name>'` is a server/tool detail; `'config'` shows the
439
+ * effective configuration; `'disable <n>'` / `'enable <n>'` are whole-server
440
+ * toggles (n is the stable id shown by `/mcp`, so a bare `0` or non-numeric
441
+ * token is already usage). Everything else parses to the usage error.
442
+ * @param rawInput - exact text following the command name.
443
+ * @returns the normalized command form.
444
+ */
445
+ export declare function parseMcpCommandInput(rawInput: string): ParsedMcpCommandInput;
446
+ /**
447
+ * Distinct server names among one view's candidates, in first-seen order —
448
+ * the live set stable ids are allocated against.
449
+ */
450
+ export declare function liveServerNames(schemas: readonly ToolSchema[], options: Pick<McpCommandOptions, 'prefix' | 'servers'>): string[];
451
+ /** Three-state folding health of the /mcp overview footer. */
452
+ export type McpFoldingHealth = {
453
+ state: 'folding';
454
+ folded: number;
455
+ kept: number;
456
+ schemaChars: number;
457
+ } | {
458
+ state: 'fail-open';
459
+ } | {
460
+ state: 'idle';
461
+ matched: number;
462
+ };
463
+ /** Which tools count toward which health bucket. */
464
+ export interface McpHealthStats {
465
+ /** Prefix-matching tools whose schema was folded out of the prompt. */
466
+ folded: ToolSchema[];
467
+ /**
468
+ * Prefix-matching tools that stay native despite matching the prefix —
469
+ * exempted either by a keep pattern or by the servers whitelist decision.
470
+ */
471
+ kept: ToolSchema[];
472
+ }
473
+ /**
474
+ * Partition one scope's schemas into folded vs kept MCP tools and measure
475
+ * how many schema characters leave the prompt. Mirrors {@link shouldFold}
476
+ * exactly; the meta-tools themselves are never counted.
477
+ *
478
+ * Health semantics follow the waterfall: folding counts as ACTIVE only when
479
+ * the meta-tools are live AND at least one tool actually folds. Live but
480
+ * nothing to fold is the idle state; not-live is fail-open.
481
+ *
482
+ * @param schemas - schemas visible to the receiving agent.
483
+ * @param options - adapter knobs (prefix/keep/servers) plus the live gate
484
+ * snapshot (`gate`) when the settings service is composed.
485
+ * @param metaToolsLive - whether both meta-tools resolve to this plugin's
486
+ * definitions in the receiving agent's scope.
487
+ * @returns the folding health plus the partition behind it.
488
+ */
489
+ export declare function collectMcpHealth(schemas: readonly ToolSchema[], options: McpCommandOptions, metaToolsLive: boolean): {
490
+ stats: McpHealthStats;
491
+ health: McpFoldingHealth;
492
+ };
493
+ /**
494
+ * Render the overview footer line for one health reading. Exactly one of:
495
+ * `meta-tools: ... live · folding ACTIVE — folded N, kept M · ~X chars of
496
+ * schema out of prompt`, the fail-open notice, or the nothing-to-fold notice.
497
+ * @param health - the collected health reading.
498
+ * @returns the single footer line.
499
+ */
500
+ export declare function renderMcpHealthLine(health: McpFoldingHealth): string;
501
+ /** One server group in the /mcp views. */
502
+ export interface McpServerGroup {
503
+ server: string;
504
+ schemas: ToolSchema[];
505
+ }
506
+ /**
507
+ * Header line of one server group in the tree views. Without a gate snapshot
508
+ * the line is the plain legacy form (`name (n)`); with one, every group is
509
+ * prefixed by its stable id — `[1] fs (4)` — and a DISABLED group renders as
510
+ * `[2] gh ⏸ disabled — hides 3 tool(s)` and lists no tools at all.
511
+ */
512
+ export declare function renderServerGroupHeader(group: Pick<McpServerGroup, 'server' | 'schemas'>, gate?: ServerGateState): string;
513
+ /**
514
+ * Group one scope's candidate schemas by server, first-seen order — the same
515
+ * filtering contract as the `mcp_list` catalog loop.
516
+ * @param schemas - schemas visible to the receiving agent.
517
+ * @param options - adapter knobs (prefix/servers).
518
+ * @returns server groups in first-seen order.
519
+ */
520
+ export declare function groupSchemasByServer(schemas: readonly ToolSchema[], options: Pick<McpCommandOptions, 'prefix' | 'servers'>): McpServerGroup[];
521
+ /**
522
+ * Cap rendered /mcp output so a giant deployment cannot flood the transcript.
523
+ * At most `limit` lines survive; the last becomes a truncation notice naming
524
+ * the narrowing subcommand.
525
+ * @param text - the rendered multi-line output.
526
+ * @param limit - max output lines (default {@link MCP_OUTPUT_LINE_LIMIT}).
527
+ * @returns the capped text (identical when already within the limit).
528
+ */
529
+ export declare function capRenderedLines(text: string, limit?: number): string;
530
+ /**
531
+ * The one-line id-exhaustion notice for /mcp views. `undefined` while the
532
+ * stable id space has room. This is where the 99-cap promise lands: a server
533
+ * beyond the cap never gets an id, so no toggle could ever target it — the
534
+ * views say so explicitly instead of pretending otherwise.
535
+ * @param unassigned - live servers observed without an assignable id.
536
+ * @returns the notice line, or nothing when the space is not exhausted.
537
+ */
538
+ export declare function renderIdCapNotice(unassigned?: readonly string[]): string | undefined;
539
+ /**
540
+ * Render the `/mcp` / `/mcp list` tree overview: one block per server with
541
+ * each tool's name and truncated description (disabled servers show only a
542
+ * `⏸ disabled` header line — their tools are hidden), closed by the
543
+ * folding-health footer line. Servers beyond the id cap carry a marker on
544
+ * their group header, and full exhaustion appends an explicit notice after
545
+ * the footer. Output is line-capped defensively.
546
+ * @param schemas - schemas visible to the receiving agent.
547
+ * @param options - adapter knobs (prefix/keep/servers/descriptionLimit/gate/
548
+ * unassignedServers).
549
+ * @param metaToolsLive - whether both meta-tools are live in that scope.
550
+ * @returns the complete overview text.
551
+ */
552
+ export declare function renderMcpOverview(schemas: readonly ToolSchema[], options: McpCommandOptions, metaToolsLive: boolean): string;
553
+ /** One resolved `/mcp list <name>` target. */
554
+ export type McpDetailTarget = {
555
+ kind: 'server';
556
+ server: string;
557
+ schemas: ToolSchema[];
558
+ } | {
559
+ kind: 'tool';
560
+ schema: ToolSchema;
561
+ };
562
+ /**
563
+ * Resolve `<name>` against one scope's candidates: a server segment first
564
+ * (all of its tools), then an exact registered tool name.
565
+ * @param name - server name or full registered tool name.
566
+ * @param schemas - schemas visible to the receiving agent.
567
+ * @param options - adapter knobs (prefix/servers).
568
+ * @returns the resolved target, or undefined when nothing matches.
569
+ */
570
+ export declare function resolveMcpDetailTarget(name: string, schemas: readonly ToolSchema[], options: Pick<McpCommandOptions, 'prefix' | 'servers'>): McpDetailTarget | undefined;
571
+ /** Outcome of rendering `/mcp list <name>`; `unknown` maps to an error result. */
572
+ export interface McpDetailView {
573
+ outcome: 'server' | 'tool' | 'unknown';
574
+ text: string;
575
+ }
576
+ /**
577
+ * Render `/mcp list <name>`: for a server, its full tool list (names +
578
+ * truncated descriptions); for a single tool, the FULL untruncated
579
+ * description plus the complete input schema. Unknown names produce a
580
+ * descriptive miss rather than generic usage. Human inspection is never
581
+ * gated, but a target belonging to a disabled server says so — with its
582
+ * `/mcp enable <id>` hint.
583
+ * @param name - the requested server or tool name.
584
+ * @param schemas - schemas visible to the receiving agent.
585
+ * @param options - adapter knobs (prefix/keep/servers/descriptionLimit/gate).
586
+ * @returns the rendered view.
587
+ */
588
+ export declare function renderMcpDetail(name: string, schemas: readonly ToolSchema[], options: McpCommandOptions): McpDetailView;
589
+ /**
590
+ * Render the effective configuration with each knob's hitting tools:
591
+ * which names match the prefix, what each keep pattern holds back, and what
592
+ * each whitelisted server contributes. Read-only inventory, no mutations.
593
+ * Tools of disabled servers stay LISTED but carry a `⏸` marker — views
594
+ * inform, they never hide.
595
+ * @param schemas - schemas visible to the receiving agent.
596
+ * @param options - adapter knobs (prefix/keep/servers/descriptionLimit/gate/
597
+ * unassignedServers).
598
+ * @returns the rendered configuration view.
599
+ */
600
+ export declare function renderMcpConfig(schemas: readonly ToolSchema[], options: McpCommandOptions): string;
601
+ /** Plain-data view one /mcp handler execution reads; production wiring fills
602
+ * it from the receiving agent's scope before calling {@link executeMcpCommand}.
603
+ */
604
+ export interface McpCommandView {
605
+ /** Exact text following the command name. */
606
+ rawInput: string;
607
+ /** Schemas visible to the receiving agent (`ctx.tools.schemas(scope)`). */
608
+ schemas: readonly ToolSchema[];
609
+ /** Effective adapter knobs plus the invocation-time gate snapshot (`gate`). */
610
+ config: McpCommandOptions;
611
+ /** Whether both meta-tools resolve to this plugin's definitions there. */
612
+ metaToolsLive: boolean;
613
+ /**
614
+ * Gate snapshot resolved when the invocation started; `undefined` means the
615
+ * dsh settings service is not composed in this process (stable ids and
616
+ * toggles are unavailable; views degrade to enabled-everything).
617
+ */
618
+ gate?: ServerGateState;
619
+ /**
620
+ * Persist one complete next registry (production: a wholesale `replace` of
621
+ * the `mcp-adapter` settings section, mirroring agent-default-model's
622
+ * save-selection precedent). Absent ⇒ /mcp disable/enable answers with an
623
+ * explanatory error instead of silently dropping the intent.
624
+ */
625
+ writeGate?: (next: ServerIdRegistry) => Promise<void>;
626
+ /**
627
+ * Cancellation signal owned by the dispatching UI request (production:
628
+ * `invocation.signal`). Only the WAIT rides it: an aborted toggle answers
629
+ * immediately that the outcome is unconfirmed while the write still
630
+ * finishes in the background — same-process code cannot hard-kill it.
631
+ */
632
+ signal?: AbortSignal;
633
+ }
634
+ /** CommandResult-compatible outcome (platform shape: success may omit text). */
635
+ export interface McpCommandOutcome {
636
+ kind: 'success' | 'error';
637
+ text: string;
638
+ }
639
+ /**
640
+ * Execute one /mcp invocation over plain data. Read forms stay side-effect-
641
+ * free except for stable-id bookkeeping: any form that displays ids ensures
642
+ * every newly observed server has one and persists fresh assignments through
643
+ * `writeGate` (best-effort — a failed persistence still renders, ids merely
644
+ * reallocate identically next time). The mutating forms validate their target
645
+ * against the LIVE server set, then persist one combined write (allocations +
646
+ * toggle); every failure surfaces as a structured `{ kind: 'error', text }`
647
+ * outcome, never a throw.
648
+ *
649
+ * @param view - the invocation's resolved inputs.
650
+ * @returns a platform-shaped command outcome.
651
+ */
652
+ export declare function executeMcpCommand(view: McpCommandView): Promise<McpCommandOutcome>;
264
653
  /**
265
654
  * Render one mcp_call outcome by delegating to the dispatched child's own
266
655
  * `output.render`, so model-facing text is exactly what a native call of the
@@ -326,9 +715,13 @@ export declare function createMcpListTool(options: McpListOptions, listSchemas:
326
715
  * @param resolve - scope-aware definition resolver (production:
327
716
  * `ctx.tools.get`, called with the calling agent).
328
717
  * @param servers - server-name whitelist (default: no filtering).
718
+ * @param getGate - live disabled-server reader (production: fresh
719
+ * `settingsScope.get()` snapshot per dispatch; default/omitted: nothing is
720
+ * disabled). Deliberately a callback — the definition object outlives any
721
+ * one settings snapshot and must observe later /mcp enable/disable writes.
329
722
  * @returns the complete tool definition.
330
723
  */
331
- export declare function createMcpCallTool(prefix: string, resolve: (name: string, scope?: ScopeKey) => ToolDefinition | undefined, servers?: readonly string[]): ToolDefinition;
724
+ export declare function createMcpCallTool(prefix: string, resolve: (name: string, scope?: ScopeKey) => ToolDefinition | undefined, servers?: readonly string[], getGate?: () => ServerGateState | undefined): ToolDefinition;
332
725
  /**
333
726
  * Register the two meta-tools and install the assembly folding listener.
334
727
  *
@@ -336,6 +729,18 @@ export declare function createMcpCallTool(prefix: string, resolve: (name: string
336
729
  * both registrations are rolled back, a warning is logged, and NO listener
337
730
  * is installed — the official full-schema passthrough stays intact.
338
731
  *
732
+ * Server gating: when a dsh settings service is composed, the `mcp-adapter`
733
+ * namespace is registered on it and every gate consumer reads FRESH resolved
734
+ * values through small callbacks — the plugin keeps no in-memory copy that
735
+ * could drift from settings. The read itself is defended too: if the
736
+ * resolved-value lookup throws, consumers see an absent gate (everything
737
+ * enabled) instead of the error, keeping the assemble waterfall alive.
738
+ *
739
+ * Commands: `/mcp` mounts through a runtime `ctx.inject(['commands'], ...)`
740
+ * (same seam as settings), so hosts without the commands service keep this
741
+ * plugin fully functional — one warning, no command. A registration name
742
+ * conflict degrades to the same warning independently.
743
+ *
339
744
  * @param ctx - plugin context carrying the tool registry.
340
745
  * @param config - resolved adapter configuration.
341
746
  */