@aiwayds/dsh-mcp-adapter 0.1.1 → 0.2.0

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,101 @@ 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;
53
74
  /** Registered name of the catalog meta-tool. */
54
75
  export declare const MCP_LIST_TOOL_NAME = "mcp_list";
55
76
  /** Registered name of the dispatch meta-tool. */
56
77
  export declare const MCP_CALL_TOOL_NAME = "mcp_call";
78
+ /** Upper bound of the stable per-server id space. */
79
+ export declare const MCP_SERVER_ID_LIMIT = 99;
80
+ /** dsh settings namespace persisting the stable ids and the disabled set. */
81
+ export declare const MCP_ADAPTER_SETTINGS_NAMESPACE: SettingsNamespace;
82
+ /**
83
+ * The persisted gate state (one settings section). `serverIds` maps every
84
+ * OBSERVED server name to its stable id and is never pruned — enable/disable
85
+ * never recycles an id, so an id always means the same server, even across
86
+ * restarts and temporary re-sync gaps.
87
+ */
88
+ export interface ServerIdRegistry {
89
+ /** Observed server name -> stable id (1..99), assigned smallest-free-first. */
90
+ serverIds: Record<string, number>;
91
+ /** Ids currently gated off; entries whose mapping exists nowhere are inert. */
92
+ disabled: number[];
93
+ }
94
+ /**
95
+ * Read-side snapshot of the registry the three latches consult. Structurally
96
+ * a superset of {@link ServerIdRegistry}; everything (config objects included)
97
+ * shaped like this qualifies, so production hands frozen `scope.get()` values
98
+ * straight in.
99
+ */
100
+ export interface ServerGateState {
101
+ readonly serverIds: Readonly<Record<string, number>>;
102
+ readonly disabled: readonly number[];
103
+ }
104
+ /** Gate snapshot meaning "nothing observed, nothing disabled". */
105
+ export declare const EMPTY_SERVER_GATE: ServerGateState;
106
+ /** Schema of the persisted `mcp-adapter` settings section. */
107
+ export declare const SERVER_ID_REGISTRY_SCHEMA: z<ServerIdRegistry>;
108
+ /**
109
+ * Coerce any resolved settings value into a trustworthy gate snapshot. The
110
+ * schema already validates at resolve time, but every latch judges from THIS
111
+ * function's output, so corrupt or hand-edited documents degrade to "some
112
+ * entries ignored" instead of wedging folding or dispatch.
113
+ */
114
+ export declare function normalizeServerGate(raw: unknown): ServerGateState;
115
+ /**
116
+ * THE shared disabled verdict — all three latches (waterfall fold, catalog,
117
+ * mcp_call dispatch) and every /mcp renderer judge through this one function,
118
+ * so the gating can never disagree with itself. An unmapped server (or an
119
+ * absent snapshot, or the degenerate empty server segment) is ENABLED:
120
+ * gating requires positive evidence, keeping fail-open direction.
121
+ */
122
+ export declare function isServerDisabled(server: string, gate?: ServerGateState): boolean;
123
+ /** Outcome of {@link allocateMissingServerIds}. */
124
+ export interface ServerIdAllocation {
125
+ /** Next registry: a fresh object; inputs are never mutated. */
126
+ registry: ServerIdRegistry;
127
+ /** Servers left unassigned because the 1..99 space ran out. */
128
+ unassigned: string[];
129
+ /** Names that received a NEW id in this pass (empty in steady state). */
130
+ added: string[];
131
+ }
132
+ /**
133
+ * Assign the smallest free id (1..99) to every listed server not yet mapped.
134
+ * Existing mappings are kept verbatim (id stability across restarts); the
135
+ * allocation never mutates its input. Call this whenever /mcp observes the
136
+ * live server set, then persist `{@link ServerIdAllocation.registry}` when
137
+ * `added` is non-empty.
138
+ */
139
+ export declare function allocateMissingServerIds(state: ServerGateState | undefined, servers: readonly string[]): ServerIdAllocation;
140
+ /** Reverse lookup: which server (if any) carries this id today. */
141
+ export declare function serverNameById(registry: ServerGateState | undefined, id: number): string | undefined;
142
+ /** Outcome of {@link setServerDisabledState}. */
143
+ export interface ServerToggleOutcome {
144
+ /** Next registry (fresh object); unchanged content when nothing applied. */
145
+ registry: ServerIdRegistry;
146
+ /** Whether the disabled set actually moved. */
147
+ changed: boolean;
148
+ }
149
+ /**
150
+ * Apply one enable/disable toggle for a MAPPED server onto a copy of the
151
+ * registry. Unmapped servers are no-ops (`changed: false`). Enabling never
152
+ * removes the id mapping — re-disabling later reuses the same number.
153
+ */
154
+ export declare function setServerDisabledState(state: ServerGateState | undefined, server: string, disable: boolean): ServerToggleOutcome;
57
155
  /** Resolved adapter configuration. */
58
156
  export interface AdapterConfig {
59
157
  /** Tool-name prefix to fold out of assembled prompts. */
@@ -96,13 +194,19 @@ export declare function isAllowedServer(name: string, prefix: string, servers: r
96
194
  * keep list — and it must never be one of this plugin's own meta-tools,
97
195
  * whatever the prefix is configured to (folding them would make every folded
98
196
  * MCP tool undiscoverable).
197
+ *
198
+ * Gate override: when the tool's server is DISABLED
199
+ * ({@link isServerDisabled}, shared with catalog/dispatch), its schema folds
200
+ * OUT regardless of keep patterns or the servers whitelist — disablement is
201
+ * strictly stronger than any config exemption.
99
202
  * @param name - the registered tool name.
100
203
  * @param prefix - tool-name prefix to fold.
101
204
  * @param keep - keep patterns from config.
102
205
  * @param servers - server-name whitelist from config (default: no filtering).
206
+ * @param gate - live disabled-server snapshot (default/undefined: none).
103
207
  * @returns whether the schema leaves the prompt (the tool stays callable).
104
208
  */
105
- export declare function shouldFold(name: string, prefix: string, keep: readonly string[], servers?: readonly string[]): boolean;
209
+ export declare function shouldFold(name: string, prefix: string, keep: readonly string[], servers?: readonly string[], gate?: ServerGateState): boolean;
106
210
  /** Folding knobs resolved from config. */
107
211
  export interface FoldOptions {
108
212
  /** Tool-name prefix to fold. */
@@ -135,9 +239,11 @@ export interface AssemblyWithTools {
135
239
  * @param options - folding knobs.
136
240
  * @param metaToolsLive - whether both meta-tools currently resolve to this
137
241
  * plugin's own definitions in the assembling scope.
242
+ * @param gate - live disabled-server snapshot, fresh per assembly (default:
243
+ * none disabled).
138
244
  * @returns the folded assembly, or the input unchanged when nothing folds.
139
245
  */
140
- export declare function foldPromptAssembly<T extends AssemblyWithTools>(assembly: T, options: FoldOptions, metaToolsLive: boolean): T;
246
+ export declare function foldPromptAssembly<T extends AssemblyWithTools>(assembly: T, options: FoldOptions, metaToolsLive: boolean, gate?: ServerGateState): T;
141
247
  /** Options for the catalog builder. */
142
248
  export interface McpListOptions {
143
249
  /** Tool-name prefix identifying MCP tools. */
@@ -146,6 +252,12 @@ export interface McpListOptions {
146
252
  descriptionLimit: number;
147
253
  /** Server-name whitelist; empty or omitted admits every server. */
148
254
  servers?: readonly string[];
255
+ /**
256
+ * Live gate reader (production: fresh `settingsScope.get()` snapshot per
257
+ * execution). A callback, not a value: the factory is built once at apply
258
+ * time and must observe later /mcp disable/enable writes.
259
+ */
260
+ getGate?: () => ServerGateState | undefined;
149
261
  }
150
262
  /** Normalized `mcp_list` arguments. */
151
263
  export interface McpListArgs {
@@ -217,9 +329,10 @@ export declare function truncateDescription(text: string, limit: number): string
217
329
  * @param args - the normalized catalog query.
218
330
  * @param schemas - schemas visible to the calling agent.
219
331
  * @param options - catalog knobs.
332
+ * @param gate - live disabled-server snapshot (default: none disabled).
220
333
  * @returns the canonical result value.
221
334
  */
222
- export declare function buildMcpListResult(args: McpListArgs, schemas: readonly ToolSchema[], options: McpListOptions): McpListResult;
335
+ export declare function buildMcpListResult(args: McpListArgs, schemas: readonly ToolSchema[], options: McpListOptions, gate?: ServerGateState): McpListResult;
223
336
  /** Structural slice of a resolvable tool: what dispatch actually consumes. */
224
337
  export interface DispatchableTool<E> {
225
338
  /** Cooperative timeout budget declared by the tool, if any. */
@@ -258,9 +371,275 @@ export interface DispatchableTool<E> {
258
371
  * @param resolve - scope-aware tool resolver (production: `ctx.tools.get`).
259
372
  * @param exec - the meta-tool's run context, forwarded to the child.
260
373
  * @param servers - server-name whitelist (default: no filtering).
374
+ * @param gate - live disabled-server snapshot (default: none disabled). The
375
+ * disabled branch sits after the prefix/whitelist validation: a disabled
376
+ * server's tools are refused with an explicit `/mcp enable <id>` hint
377
+ * before the child is ever resolved.
261
378
  * @returns the child's resolved value verbatim, or a structured error.
262
379
  */
263
- export declare function dispatchMcpCall<E>(args: unknown, prefix: string, resolve: (name: string) => DispatchableTool<E> | undefined, exec: E, servers?: readonly string[]): Promise<unknown>;
380
+ export declare function dispatchMcpCall<E>(args: unknown, prefix: string, resolve: (name: string) => DispatchableTool<E> | undefined, exec: E, servers?: readonly string[], gate?: ServerGateState): Promise<unknown>;
381
+ /** Knobs shared by every /mcp renderer — plain data, no Cordis. */
382
+ export interface McpCommandOptions {
383
+ /** Tool-name prefix identifying MCP tools. */
384
+ prefix: string;
385
+ /** Name patterns kept native in the prompt ("*" wildcard). */
386
+ keep: readonly string[];
387
+ /** Server-name whitelist; empty or omitted admits every server. */
388
+ servers?: readonly string[];
389
+ /** Max chars per tool description in rendered listings. */
390
+ descriptionLimit: number;
391
+ /**
392
+ * Live-resolved gate snapshot at invocation time (undefined = the settings
393
+ * service is not composed, so ids/toggles are absent from all views and
394
+ * everything renders enabled). A snapshot per invocation keeps these
395
+ * renderers pure and never stale within one command execution.
396
+ */
397
+ gate?: ServerGateState;
398
+ /**
399
+ * Live server names left WITHOUT a stable id because the 1..99 space was
400
+ * full when they appeared (production:
401
+ * {@link ServerIdAllocation.unassigned}). Views — not an erroring toggle —
402
+ * surface that fact, since a server without an id cannot be targeted by
403
+ * one in the first place.
404
+ */
405
+ unassignedServers?: readonly string[];
406
+ }
407
+ /** Usage text returned for unparseable /mcp invocations. */
408
+ export declare const MCP_COMMAND_USAGE: string;
409
+ /** One parsed /mcp invocation. */
410
+ export type ParsedMcpCommandInput = {
411
+ form: 'overview';
412
+ } | {
413
+ form: 'detail';
414
+ name: string;
415
+ } | {
416
+ form: 'config';
417
+ } | {
418
+ form: 'disable';
419
+ id: number;
420
+ } | {
421
+ form: 'enable';
422
+ id: number;
423
+ } | {
424
+ form: 'usage';
425
+ };
426
+ /**
427
+ * Parse one /mcp raw input into its form. `''` and `'list'` are the tree
428
+ * overview; `'list <name>'` is a server/tool detail; `'config'` shows the
429
+ * effective configuration; `'disable <n>'` / `'enable <n>'` are whole-server
430
+ * toggles (n is the stable id shown by `/mcp`, so a bare `0` or non-numeric
431
+ * token is already usage). Everything else parses to the usage error.
432
+ * @param rawInput - exact text following the command name.
433
+ * @returns the normalized command form.
434
+ */
435
+ export declare function parseMcpCommandInput(rawInput: string): ParsedMcpCommandInput;
436
+ /**
437
+ * Distinct server names among one view's candidates, in first-seen order —
438
+ * the live set stable ids are allocated against.
439
+ */
440
+ export declare function liveServerNames(schemas: readonly ToolSchema[], options: Pick<McpCommandOptions, 'prefix' | 'servers'>): string[];
441
+ /** Three-state folding health of the /mcp overview footer. */
442
+ export type McpFoldingHealth = {
443
+ state: 'folding';
444
+ folded: number;
445
+ kept: number;
446
+ schemaChars: number;
447
+ } | {
448
+ state: 'fail-open';
449
+ } | {
450
+ state: 'idle';
451
+ matched: number;
452
+ };
453
+ /** Which tools count toward which health bucket. */
454
+ export interface McpHealthStats {
455
+ /** Prefix-matching tools whose schema was folded out of the prompt. */
456
+ folded: ToolSchema[];
457
+ /**
458
+ * Prefix-matching tools that stay native despite matching the prefix —
459
+ * exempted either by a keep pattern or by the servers whitelist decision.
460
+ */
461
+ kept: ToolSchema[];
462
+ }
463
+ /**
464
+ * Partition one scope's schemas into folded vs kept MCP tools and measure
465
+ * how many schema characters leave the prompt. Mirrors {@link shouldFold}
466
+ * exactly; the meta-tools themselves are never counted.
467
+ *
468
+ * Health semantics follow the waterfall: folding counts as ACTIVE only when
469
+ * the meta-tools are live AND at least one tool actually folds. Live but
470
+ * nothing to fold is the idle state; not-live is fail-open.
471
+ *
472
+ * @param schemas - schemas visible to the receiving agent.
473
+ * @param options - adapter knobs (prefix/keep/servers) plus the live gate
474
+ * snapshot (`gate`) when the settings service is composed.
475
+ * @param metaToolsLive - whether both meta-tools resolve to this plugin's
476
+ * definitions in the receiving agent's scope.
477
+ * @returns the folding health plus the partition behind it.
478
+ */
479
+ export declare function collectMcpHealth(schemas: readonly ToolSchema[], options: McpCommandOptions, metaToolsLive: boolean): {
480
+ stats: McpHealthStats;
481
+ health: McpFoldingHealth;
482
+ };
483
+ /**
484
+ * Render the overview footer line for one health reading. Exactly one of:
485
+ * `meta-tools: ... live · folding ACTIVE — folded N, kept M · ~X chars of
486
+ * schema out of prompt`, the fail-open notice, or the nothing-to-fold notice.
487
+ * @param health - the collected health reading.
488
+ * @returns the single footer line.
489
+ */
490
+ export declare function renderMcpHealthLine(health: McpFoldingHealth): string;
491
+ /** One server group in the /mcp views. */
492
+ export interface McpServerGroup {
493
+ server: string;
494
+ schemas: ToolSchema[];
495
+ }
496
+ /**
497
+ * Header line of one server group in the tree views. Without a gate snapshot
498
+ * the line is the plain legacy form (`name (n)`); with one, every group is
499
+ * prefixed by its stable id — `[1] fs (4)` — and a DISABLED group renders as
500
+ * `[2] gh ⏸ disabled — hides 3 tool(s)` and lists no tools at all.
501
+ */
502
+ export declare function renderServerGroupHeader(group: Pick<McpServerGroup, 'server' | 'schemas'>, gate?: ServerGateState): string;
503
+ /**
504
+ * Group one scope's candidate schemas by server, first-seen order — the same
505
+ * filtering contract as the `mcp_list` catalog loop.
506
+ * @param schemas - schemas visible to the receiving agent.
507
+ * @param options - adapter knobs (prefix/servers).
508
+ * @returns server groups in first-seen order.
509
+ */
510
+ export declare function groupSchemasByServer(schemas: readonly ToolSchema[], options: Pick<McpCommandOptions, 'prefix' | 'servers'>): McpServerGroup[];
511
+ /**
512
+ * Cap rendered /mcp output so a giant deployment cannot flood the transcript.
513
+ * At most `limit` lines survive; the last becomes a truncation notice naming
514
+ * the narrowing subcommand.
515
+ * @param text - the rendered multi-line output.
516
+ * @param limit - max output lines (default {@link MCP_OUTPUT_LINE_LIMIT}).
517
+ * @returns the capped text (identical when already within the limit).
518
+ */
519
+ export declare function capRenderedLines(text: string, limit?: number): string;
520
+ /**
521
+ * The one-line id-exhaustion notice for /mcp views. `undefined` while the
522
+ * stable id space has room. This is where the 99-cap promise lands: a server
523
+ * beyond the cap never gets an id, so no toggle could ever target it — the
524
+ * views say so explicitly instead of pretending otherwise.
525
+ * @param unassigned - live servers observed without an assignable id.
526
+ * @returns the notice line, or nothing when the space is not exhausted.
527
+ */
528
+ export declare function renderIdCapNotice(unassigned?: readonly string[]): string | undefined;
529
+ /**
530
+ * Render the `/mcp` / `/mcp list` tree overview: one block per server with
531
+ * each tool's name and truncated description (disabled servers show only a
532
+ * `⏸ disabled` header line — their tools are hidden), closed by the
533
+ * folding-health footer line. Servers beyond the id cap carry a marker on
534
+ * their group header, and full exhaustion appends an explicit notice after
535
+ * the footer. Output is line-capped defensively.
536
+ * @param schemas - schemas visible to the receiving agent.
537
+ * @param options - adapter knobs (prefix/keep/servers/descriptionLimit/gate/
538
+ * unassignedServers).
539
+ * @param metaToolsLive - whether both meta-tools are live in that scope.
540
+ * @returns the complete overview text.
541
+ */
542
+ export declare function renderMcpOverview(schemas: readonly ToolSchema[], options: McpCommandOptions, metaToolsLive: boolean): string;
543
+ /** One resolved `/mcp list <name>` target. */
544
+ export type McpDetailTarget = {
545
+ kind: 'server';
546
+ server: string;
547
+ schemas: ToolSchema[];
548
+ } | {
549
+ kind: 'tool';
550
+ schema: ToolSchema;
551
+ };
552
+ /**
553
+ * Resolve `<name>` against one scope's candidates: a server segment first
554
+ * (all of its tools), then an exact registered tool name.
555
+ * @param name - server name or full registered tool name.
556
+ * @param schemas - schemas visible to the receiving agent.
557
+ * @param options - adapter knobs (prefix/servers).
558
+ * @returns the resolved target, or undefined when nothing matches.
559
+ */
560
+ export declare function resolveMcpDetailTarget(name: string, schemas: readonly ToolSchema[], options: Pick<McpCommandOptions, 'prefix' | 'servers'>): McpDetailTarget | undefined;
561
+ /** Outcome of rendering `/mcp list <name>`; `unknown` maps to an error result. */
562
+ export interface McpDetailView {
563
+ outcome: 'server' | 'tool' | 'unknown';
564
+ text: string;
565
+ }
566
+ /**
567
+ * Render `/mcp list <name>`: for a server, its full tool list (names +
568
+ * truncated descriptions); for a single tool, the FULL untruncated
569
+ * description plus the complete input schema. Unknown names produce a
570
+ * descriptive miss rather than generic usage. Human inspection is never
571
+ * gated, but a target belonging to a disabled server says so — with its
572
+ * `/mcp enable <id>` hint.
573
+ * @param name - the requested server or tool name.
574
+ * @param schemas - schemas visible to the receiving agent.
575
+ * @param options - adapter knobs (prefix/keep/servers/descriptionLimit/gate).
576
+ * @returns the rendered view.
577
+ */
578
+ export declare function renderMcpDetail(name: string, schemas: readonly ToolSchema[], options: McpCommandOptions): McpDetailView;
579
+ /**
580
+ * Render the effective configuration with each knob's hitting tools:
581
+ * which names match the prefix, what each keep pattern holds back, and what
582
+ * each whitelisted server contributes. Read-only inventory, no mutations.
583
+ * Tools of disabled servers stay LISTED but carry a `⏸` marker — views
584
+ * inform, they never hide.
585
+ * @param schemas - schemas visible to the receiving agent.
586
+ * @param options - adapter knobs (prefix/keep/servers/descriptionLimit/gate/
587
+ * unassignedServers).
588
+ * @returns the rendered configuration view.
589
+ */
590
+ export declare function renderMcpConfig(schemas: readonly ToolSchema[], options: McpCommandOptions): string;
591
+ /** Plain-data view one /mcp handler execution reads; production wiring fills
592
+ * it from the receiving agent's scope before calling {@link executeMcpCommand}.
593
+ */
594
+ export interface McpCommandView {
595
+ /** Exact text following the command name. */
596
+ rawInput: string;
597
+ /** Schemas visible to the receiving agent (`ctx.tools.schemas(scope)`). */
598
+ schemas: readonly ToolSchema[];
599
+ /** Effective adapter knobs plus the invocation-time gate snapshot (`gate`). */
600
+ config: McpCommandOptions;
601
+ /** Whether both meta-tools resolve to this plugin's definitions there. */
602
+ metaToolsLive: boolean;
603
+ /**
604
+ * Gate snapshot resolved when the invocation started; `undefined` means the
605
+ * dsh settings service is not composed in this process (stable ids and
606
+ * toggles are unavailable; views degrade to enabled-everything).
607
+ */
608
+ gate?: ServerGateState;
609
+ /**
610
+ * Persist one complete next registry (production: a wholesale `replace` of
611
+ * the `mcp-adapter` settings section, mirroring agent-default-model's
612
+ * save-selection precedent). Absent ⇒ /mcp disable/enable answers with an
613
+ * explanatory error instead of silently dropping the intent.
614
+ */
615
+ writeGate?: (next: ServerIdRegistry) => Promise<void>;
616
+ /**
617
+ * Cancellation signal owned by the dispatching UI request (production:
618
+ * `invocation.signal`). Only the WAIT rides it: an aborted toggle answers
619
+ * immediately that the outcome is unconfirmed while the write still
620
+ * finishes in the background — same-process code cannot hard-kill it.
621
+ */
622
+ signal?: AbortSignal;
623
+ }
624
+ /** CommandResult-compatible outcome (platform shape: success may omit text). */
625
+ export interface McpCommandOutcome {
626
+ kind: 'success' | 'error';
627
+ text: string;
628
+ }
629
+ /**
630
+ * Execute one /mcp invocation over plain data. Read forms stay side-effect-
631
+ * free except for stable-id bookkeeping: any form that displays ids ensures
632
+ * every newly observed server has one and persists fresh assignments through
633
+ * `writeGate` (best-effort — a failed persistence still renders, ids merely
634
+ * reallocate identically next time). The mutating forms validate their target
635
+ * against the LIVE server set, then persist one combined write (allocations +
636
+ * toggle); every failure surfaces as a structured `{ kind: 'error', text }`
637
+ * outcome, never a throw.
638
+ *
639
+ * @param view - the invocation's resolved inputs.
640
+ * @returns a platform-shaped command outcome.
641
+ */
642
+ export declare function executeMcpCommand(view: McpCommandView): Promise<McpCommandOutcome>;
264
643
  /**
265
644
  * Render one mcp_call outcome by delegating to the dispatched child's own
266
645
  * `output.render`, so model-facing text is exactly what a native call of the
@@ -326,9 +705,13 @@ export declare function createMcpListTool(options: McpListOptions, listSchemas:
326
705
  * @param resolve - scope-aware definition resolver (production:
327
706
  * `ctx.tools.get`, called with the calling agent).
328
707
  * @param servers - server-name whitelist (default: no filtering).
708
+ * @param getGate - live disabled-server reader (production: fresh
709
+ * `settingsScope.get()` snapshot per dispatch; default/omitted: nothing is
710
+ * disabled). Deliberately a callback — the definition object outlives any
711
+ * one settings snapshot and must observe later /mcp enable/disable writes.
329
712
  * @returns the complete tool definition.
330
713
  */
331
- export declare function createMcpCallTool(prefix: string, resolve: (name: string, scope?: ScopeKey) => ToolDefinition | undefined, servers?: readonly string[]): ToolDefinition;
714
+ export declare function createMcpCallTool(prefix: string, resolve: (name: string, scope?: ScopeKey) => ToolDefinition | undefined, servers?: readonly string[], getGate?: () => ServerGateState | undefined): ToolDefinition;
332
715
  /**
333
716
  * Register the two meta-tools and install the assembly folding listener.
334
717
  *
@@ -336,6 +719,18 @@ export declare function createMcpCallTool(prefix: string, resolve: (name: string
336
719
  * both registrations are rolled back, a warning is logged, and NO listener
337
720
  * is installed — the official full-schema passthrough stays intact.
338
721
  *
722
+ * Server gating: when a dsh settings service is composed, the `mcp-adapter`
723
+ * namespace is registered on it and every gate consumer reads FRESH resolved
724
+ * values through small callbacks — the plugin keeps no in-memory copy that
725
+ * could drift from settings. The read itself is defended too: if the
726
+ * resolved-value lookup throws, consumers see an absent gate (everything
727
+ * enabled) instead of the error, keeping the assemble waterfall alive.
728
+ *
729
+ * Commands: `/mcp` mounts through a runtime `ctx.inject(['commands'], ...)`
730
+ * (same seam as settings), so hosts without the commands service keep this
731
+ * plugin fully functional — one warning, no command. A registration name
732
+ * conflict degrades to the same warning independently.
733
+ *
339
734
  * @param ctx - plugin context carrying the tool registry.
340
735
  * @param config - resolved adapter configuration.
341
736
  */