@aiwayds/dsh-mcp-adapter 0.5.0 → 0.6.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/lib/index.d.ts CHANGED
@@ -57,6 +57,9 @@ import type { Context } from '@deepseek-ai/cordis';
57
57
  import z from '@deepseek-ai/schemastery';
58
58
  import type { ContentBlock, ToolSchema } from '@deepseek-ai/dsh-llm';
59
59
  import type { ToolDefinition, ToolExecution, ToolExecutionResult } from '@deepseek-ai/dsh-tools';
60
+ import type { JevConfigValue } from './jev/config.ts';
61
+ import type { RankCandidate } from './rank.ts';
62
+ import type { SuggestionRecord } from './usage.ts';
60
63
  import type { ScopeKey } from '@deepseek-ai/dsh-scope';
61
64
  /** Cordis plugin name used by loader diagnostics. */
62
65
  export declare const name = "mcp-adapter";
@@ -195,8 +198,72 @@ export declare function resolveGateDir(configured: string): string;
195
198
  * synchronous (boot-time, once); writes land atomically (tmp file + rename)
196
199
  * so a crash mid-write can never leave a torn document, and the in-memory
197
200
  * mirror only advances after the file is durably renamed into place.
201
+ *
202
+ * Legacy absorption (one-time, 0.1.5 → 0.1.7): on a FRESH gate — no
203
+ * gate.json yet — the machine state from the removed settings registry is
204
+ * absorbed once from the host's legacy settings documents (see
205
+ * {@link absorbLegacyGate}). Opt-in through `options.home`: the legacy
206
+ * documents are a HOME-level concern (not this store's `dir`), and stores
207
+ * built without a home (bare test harnesses) stay exactly as isolated as
208
+ * before.
209
+ */
210
+ export declare function createFileGateStore(dir: string, warn: GateWarn, options?: GateStoreOptions): GateStore;
211
+ /**
212
+ * dsh 0.1.7 renamed the old `<home>/settings.yaml` to
213
+ * `settings.yaml.imported` after a one-shot import keyed by "section name =
214
+ * entry id". This plugin's old `mcp-adapter:` section (the machine-maintained
215
+ * id registry / disabled set) does not match the entry id `dsh-mcp-adapter`
216
+ * and was silently dropped by the host — the state lives on only in the
217
+ * renamed document. {@link absorbLegacyGate} recovers it once into the
218
+ * plugin's own gate.json.
219
+ */
220
+ /** Legacy host settings documents, in read priority order. */
221
+ export declare const LEGACY_SETTINGS_FILES: readonly ["settings.yaml.imported", "settings.yaml"];
222
+ /** Legacy top-level section name in the old settings document. */
223
+ export declare const LEGACY_GATE_SECTION = "mcp-adapter";
224
+ /** Audit marker file name: `<home>/storages/mcp-adapter/<name>`. */
225
+ export declare const LEGACY_IMPORT_MARKER = "legacy-import.json";
226
+ /** Marker outcomes written for every settled absorption pass. */
227
+ export type LegacyGateOutcome = 'imported' | 'no-op' | 'no-legacy' | 'no-section';
228
+ /**
229
+ * Extract the top-level `mcp-adapter:` section of the old settings document.
230
+ * Handles both shapes the 0.1.5 writer produced:
231
+ * - single-line values (`serverIds: '{"fs": 1}'`, `disabled: '[1]'`);
232
+ * - block collections (`serverIds:` followed by deeper-indented
233
+ * `name: <int>` lines; `disabled:` followed by `- <int>` lines).
234
+ * Everything else (comments, folded scalars, nested junk) is skipped.
235
+ * undefined = no section found. Pure function — tests run it against
236
+ * literal strings.
198
237
  */
199
- export declare function createFileGateStore(dir: string, warn: GateWarn): GateStore;
238
+ export declare function legacyGateSection(text: string): {
239
+ serverIds?: unknown;
240
+ disabled?: unknown;
241
+ } | undefined;
242
+ /**
243
+ * Turn one decoded section into the raw candidate {@link normalizeServerGate}
244
+ * sanitizes: flow-JSON strings are JSON-parsed (malformed → undefined, the
245
+ * key degrades to "absent"), already-decoded block collections pass through.
246
+ */
247
+ export declare function legacyGateCandidate(section: {
248
+ serverIds?: unknown;
249
+ disabled?: unknown;
250
+ }): {
251
+ serverIds?: unknown;
252
+ disabled?: unknown;
253
+ };
254
+ /** Construction options for {@link createFileGateStore} beyond the two
255
+ * historical arguments (all optional — bare harnesses change nothing). */
256
+ export interface GateStoreOptions {
257
+ /** Summary-line seam for the absorption (default: silent). */
258
+ info?: GateWarn;
259
+ /**
260
+ * The dsh home holding the legacy settings documents and the audit marker
261
+ * (production: `resolveDshHome()` at the call site). Absorption runs ONLY
262
+ * when a home is provided — a store built without one never reads outside
263
+ * its own directory.
264
+ */
265
+ home?: string;
266
+ }
200
267
  /**
201
268
  * Live config reference. 0.1.7 settings contract: volatile `Config` fields
202
269
  * reach `apply` as host-swapped references — `.get()` snapshots the current
@@ -210,8 +277,15 @@ export interface VolatileRef<T> {
210
277
  * Resolved adapter configuration as `apply` receives it. Every knob is
211
278
  * user-tunable from the settings page (entry `dsh-mcp-adapter`) and hot-reloads
212
279
  * without a plugin restart.
280
+ *
281
+ * The seven `jev*` keys are the System One decision settings (see
282
+ * ./jev/config.ts): the master switch, the backend/model to ask, the request
283
+ * timeout, the outbound secret list, and the local laya pace-maker. They are
284
+ * spread into {@link Config} from `JEV_CONFIG_FIELDS` so the settings schema
285
+ * and this interface cannot drift, and they are read through their volatile
286
+ * refs per use like every other knob.
213
287
  */
214
- export interface AdapterConfig {
288
+ export interface AdapterConfig extends JevConfigValue {
215
289
  /** Tool-name prefix to fold out of assembled prompts. */
216
290
  prefix: VolatileRef<string>;
217
291
  /** Name patterns ("*" wildcard) kept native in the prompt. */
@@ -228,7 +302,7 @@ export interface AdapterConfig {
228
302
  /**
229
303
  * Gate storage directory override; empty (default) =
230
304
  * `<dsh home>/storages/mcp-adapter`. Read once at apply time (the store is
231
- * built there); changing it takes effect on the next plugin restart.
305
+ * built there; changing it takes effect on the next plugin restart).
232
306
  */
233
307
  storageDir: VolatileRef<string>;
234
308
  }
@@ -322,6 +396,19 @@ export interface McpListOptions {
322
396
  * once at apply time and must observe later /mcp disable/enable writes.
323
397
  */
324
398
  getGate?: () => ServerGateState | undefined;
399
+ /**
400
+ * Optional async relevance reranker for `{ query }` calls ONLY (production:
401
+ * the System One seam). Called with the candidate pool — the lexical
402
+ * shortlist, or the whole visible catalog when the prefilter matched
403
+ * nothing — and resolves to the preferred order, or to `undefined` to keep
404
+ * the order it was given.
405
+ *
406
+ * Deliberately a promise-returning callback on a SYNCHRONOUS builder: the
407
+ * remote call happens in the async execute layer, and this builder stays a
408
+ * pure function of (args, schemas, options, ranking). Disabled, failed, or
409
+ * absent rerank ⇒ `undefined` ⇒ the given order, i.e. pre-seam behavior.
410
+ */
411
+ rankQuery?: (query: string, candidates: readonly RankCandidate[]) => Promise<readonly string[] | undefined>;
325
412
  }
326
413
  /** Normalized `mcp_list` arguments. */
327
414
  export interface McpListArgs {
@@ -331,6 +418,13 @@ export interface McpListArgs {
331
418
  server?: string;
332
419
  /** Inline every tool's schema in the catalog. */
333
420
  verbose?: boolean;
421
+ /**
422
+ * Free-text relevance query. Absent (or blank) = the unfiltered catalog,
423
+ * byte-identical to the pre-query behavior; present = a ranked SHORTLIST
424
+ * plus the total match count, never a silent narrowing of the only
425
+ * discovery path the model has.
426
+ */
427
+ query?: string;
334
428
  }
335
429
  /** One catalog entry; `parameters` is present only when expanded. */
336
430
  export interface CatalogToolEntry {
@@ -343,9 +437,24 @@ export interface CatalogServerEntry {
343
437
  server: string;
344
438
  tools: CatalogToolEntry[];
345
439
  }
346
- /** `mcp_list`'s canonical result: catalog, single expansion, or error. */
440
+ /**
441
+ * `mcp_list`'s canonical result: catalog, single expansion, or error.
442
+ *
443
+ * The `{ servers }` variant carries four OPTIONAL keys that are present only
444
+ * on a query answer: `query`, `ranked`, `total` and `note`. They are what
445
+ * makes the subset self-describing — the model can see that this is a ranked
446
+ * shortlist of N of M matches and that the full catalog is one no-argument
447
+ * call away, and `ranked: false` says the prefilter matched nothing at all
448
+ * and this is the semantic fallback rather than a ranking. An unqueried
449
+ * catalog omits them entirely, so the wire value stays byte-identical to the
450
+ * pre-seam shape.
451
+ */
347
452
  export type McpListResult = {
348
453
  servers: CatalogServerEntry[];
454
+ query?: string;
455
+ ranked?: boolean;
456
+ total?: number;
457
+ note?: string;
349
458
  } | {
350
459
  tool: {
351
460
  name: string;
@@ -355,9 +464,43 @@ export type McpListResult = {
355
464
  } | {
356
465
  error: string;
357
466
  };
467
+ /**
468
+ * A precomputed ranking for one `{ query }` catalog call — the seam between
469
+ * the async jev layer and the pure builder. `pool`/`total` are the LEXICAL
470
+ * result (so the builder does not have to redo the work and cannot disagree
471
+ * with the shortlist jev was asked about); `order` is jev's permutation of
472
+ * that pool, absent when there is no opinion (fail-open ⇒ `pool` order).
473
+ */
474
+ export interface ListRanking {
475
+ /** The query this ranking was computed for. */
476
+ query: string;
477
+ /**
478
+ * The candidate set, at most {@link RANK_CANDIDATE_LIMIT} names: the lexical
479
+ * shortlist, best first — or, under {@link ListRanking.semantic}, the whole
480
+ * visible catalog in registry order.
481
+ */
482
+ pool: readonly string[];
483
+ /** Every lexical match, uncapped — the denominator of the `total` field. */
484
+ total: number;
485
+ /** Reranked names, best first (production: the jev answer). */
486
+ order?: readonly string[];
487
+ /**
488
+ * True when the lexical prefilter matched NOTHING and `pool` is instead the
489
+ * SEMANTIC-FALLBACK set: the whole visible catalog, capped, handed to a
490
+ * reranker that can still tell a Chinese query from an English description
491
+ * and an ASCII one cannot. Absent (the ordinary case) = a lexical ranking,
492
+ * and the answer says `ranked: true` with no mention of a fallback.
493
+ */
494
+ semantic?: boolean;
495
+ }
358
496
  /**
359
497
  * Narrow arbitrary model arguments to the `mcp_list` shape, dropping
360
498
  * unknown or mistyped keys instead of throwing (the model misbehaves).
499
+ *
500
+ * A blank `query` is dropped rather than kept: an empty search is not a
501
+ * search, and honoring it would put the catalog on a different code path (the
502
+ * ranked one) for a request that means "show me everything".
503
+ *
361
504
  * @param args - raw model arguments.
362
505
  * @returns the normalized catalog query.
363
506
  */
@@ -385,21 +528,55 @@ export declare function serverOfToolName(name: string, prefix: string): string;
385
528
  * @returns the single-line truncated description.
386
529
  */
387
530
  export declare function truncateDescription(text: string, limit: number): string;
531
+ /**
532
+ * The lexical relevance shortlist for one `query` catalog call — the pure,
533
+ * synchronous half of seam B. This is both what the jev reranker is asked
534
+ * about and the order that stands when jev is disabled or fails.
535
+ *
536
+ * @param args - the normalized catalog query (must carry a `query`).
537
+ * @param schemas - schemas visible to the calling agent.
538
+ * @param options - catalog knobs.
539
+ * @param gate - live disabled-server snapshot (default: none disabled).
540
+ * @returns the shortlist (name + full description per entry, so a reranker
541
+ * can ask about them without re-reading the schemas) plus the uncapped
542
+ * match count. A query the prefilter cannot lexically match comes back
543
+ * `semantic` with the whole visible catalog as its pool. A structured error
544
+ * means the catalog itself is empty or fully gated.
545
+ */
546
+ export declare function listQueryRanking(args: McpListArgs, schemas: readonly ToolSchema[], options: McpListOptions, gate?: ServerGateState): {
547
+ pool: readonly RankCandidate[];
548
+ total: number;
549
+ semantic?: boolean;
550
+ error?: string;
551
+ };
388
552
  /**
389
553
  * Build the `mcp_list` result over the currently visible schemas.
390
554
  *
391
555
  * Priority: `tool` (full single-tool expansion) > catalog (optionally
392
- * filtered by `server`, optionally `verbose`). Unknown or non-MCP `tool`,
393
- * and any query that selects nothing, return a structured error — never a
394
- * throw — so the model can correct itself.
556
+ * filtered by `server`, optionally `verbose`, optionally ranked by `query`).
557
+ * Unknown or non-MCP `tool`, and any query that selects nothing out of an
558
+ * empty or fully gated catalog, return a structured error — never a throw —
559
+ * so the model can correct itself.
560
+ *
561
+ * A `query` that the LEXICAL prefilter cannot match is NOT an error: it
562
+ * degrades to the semantic fallback (the whole visible catalog, capped, and
563
+ * `ranked: false`), because "no word of the query appeared anywhere" is not
564
+ * "no tool matches" — see {@link semanticFallbackPool}.
565
+ *
566
+ * `query` is a MODEL-EXPLICIT narrowing: it is the only catalog path that
567
+ * consults `ranking` (production: the System One seam), and a missing/failed
568
+ * rerank degrades to the lexical order. With no `query`, this function is the
569
+ * pre-seim builder unchanged.
395
570
  *
396
571
  * @param args - the normalized catalog query.
397
572
  * @param schemas - schemas visible to the calling agent.
398
573
  * @param options - catalog knobs.
399
574
  * @param gate - live disabled-server snapshot (default: none disabled).
575
+ * @param ranking - precomputed relevance ranking for `args.query`
576
+ * (production: the async layer's jev answer).
400
577
  * @returns the canonical result value.
401
578
  */
402
- export declare function buildMcpListResult(args: McpListArgs, schemas: readonly ToolSchema[], options: McpListOptions, gate?: ServerGateState): McpListResult;
579
+ export declare function buildMcpListResult(args: McpListArgs, schemas: readonly ToolSchema[], options: McpListOptions, gate?: ServerGateState, ranking?: ListRanking): McpListResult;
403
580
  /** Structural slice of a resolvable tool: what dispatch actually consumes. */
404
581
  export interface DispatchableTool<E> {
405
582
  /** Cooperative timeout budget declared by the tool, if any. */
@@ -407,6 +584,36 @@ export interface DispatchableTool<E> {
407
584
  /** The tool body dispatch invokes. */
408
585
  execute(args: unknown, exec: E): Promise<unknown>;
409
586
  }
587
+ /**
588
+ * The near-miss seam of the unknown-tool branch (seam C).
589
+ *
590
+ * `candidates` is deliberately cheap and SYNCHRONOUS — it is called on a path
591
+ * the model is already reading an error on, and it must answer even when jev
592
+ * is off, timing out, or broken. `rerank` is the optional async refinement
593
+ * (production: the System One seam); `undefined`, a rejection, or an
594
+ * unusable answer all mean "keep the lexical order", which is the behavior
595
+ * the branch had before the seam existed.
596
+ */
597
+ export interface DidYouMeanHook {
598
+ /** Visible tool names a near-miss may point at, in the calling scope. */
599
+ candidates(): readonly RankCandidate[];
600
+ /** Optional async reorder; absent/undefined/throw ⇒ lexical order stands. */
601
+ rerank?(tool: string, candidates: readonly RankCandidate[]): Promise<readonly string[] | undefined>;
602
+ }
603
+ /** The near-miss pool cap — the same batch ceiling the jev seams use. */
604
+ export declare const DID_YOU_MEAN_POOL_LIMIT = 40;
605
+ /**
606
+ * The visible tools a near-miss may point at: this scope's prefix-matching
607
+ * MCP tools under the servers whitelist, minus the meta-tools and minus
608
+ * disabled servers (dispatch would refuse those anyway, so naming one would
609
+ * be advice the model cannot take).
610
+ *
611
+ * @param schemas - schemas visible to the calling agent.
612
+ * @param options - adapter knobs (prefix/servers).
613
+ * @param gate - live disabled-server snapshot (default: none disabled).
614
+ * @returns the pool, in registry order.
615
+ */
616
+ export declare function didYouMeanPool(schemas: readonly ToolSchema[], options: Pick<McpCommandOptions, 'prefix' | 'servers'>, gate?: ServerGateState): RankCandidate[];
410
617
  /**
411
618
  * Dispatch one `mcp_call` to the resolved child tool.
412
619
  *
@@ -433,6 +640,11 @@ export interface DispatchableTool<E> {
433
640
  * Errors never throw out of dispatch: a child rejection (or raced timeout)
434
641
  * is wrapped into a structured `{ error }` the model can correct and retry.
435
642
  *
643
+ * The unknown-tool branch additionally offers near-misses (seam C): a lexical
644
+ * prefilter over the scope's visible tools always answers, and an optional
645
+ * async rerank (production: System One) can reorder it. The base error text
646
+ * is unchanged, so this stays a pure addition to the message.
647
+ *
436
648
  * @param args - raw model arguments (`{ tool, arguments? }`, validated here).
437
649
  * @param prefix - the configured MCP prefix.
438
650
  * @param resolve - scope-aware tool resolver (production: `ctx.tools.get`).
@@ -442,9 +654,11 @@ export interface DispatchableTool<E> {
442
654
  * disabled branch sits after the prefix/whitelist validation: a disabled
443
655
  * server's tools are refused with an explicit `/mcp enable <id>` hint
444
656
  * before the child is ever resolved.
657
+ * @param didYouMean - the near-miss seam for the unknown-tool branch
658
+ * (default: none — the pre-seam message, unchanged).
445
659
  * @returns the child's resolved value verbatim, or a structured error.
446
660
  */
447
- export declare function dispatchMcpCall<E>(args: unknown, prefix: string, resolve: (name: string) => DispatchableTool<E> | undefined, exec: E, servers?: readonly string[], gate?: ServerGateState): Promise<unknown>;
661
+ export declare function dispatchMcpCall<E>(args: unknown, prefix: string, resolve: (name: string) => DispatchableTool<E> | undefined, exec: E, servers?: readonly string[], gate?: ServerGateState, didYouMean?: DidYouMeanHook): Promise<unknown>;
448
662
  /** Knobs shared by every /mcp renderer — plain data, no Cordis. */
449
663
  export interface McpCommandOptions {
450
664
  /** Tool-name prefix identifying MCP tools. */
@@ -470,6 +684,36 @@ export interface McpCommandOptions {
470
684
  * one in the first place.
471
685
  */
472
686
  unassignedServers?: readonly string[];
687
+ /**
688
+ * The seven System One settings as plain data (production: the live
689
+ * volatile config refs, resolved per invocation). Absent ⇒ the views stay
690
+ * byte-identical to the pre-jev plugin — a caller that does not wire the
691
+ * decision support never sees a jev line.
692
+ */
693
+ jev?: McpJevOptions;
694
+ /**
695
+ * Recorded usage counters for the views that talk about the suggestion
696
+ * cadence (production: the usage mirror). Absent ⇒ those views report "no
697
+ * usage recorded yet" instead of a number.
698
+ */
699
+ usage?: McpUsageOptions;
700
+ }
701
+ /** The seven jev settings, flattened for display. */
702
+ export interface McpJevOptions {
703
+ readonly jevEnabled: boolean;
704
+ readonly jevBackend: string;
705
+ readonly jevModel: string;
706
+ readonly jevTimeoutMs: number;
707
+ readonly jevSecretFile: string;
708
+ readonly jevLayaFallback: boolean;
709
+ readonly jevLayaUrl: string;
710
+ }
711
+ /** Recorded-usage counters, flattened for display. */
712
+ export interface McpUsageOptions {
713
+ /** Dispatches recorded across every tool. */
714
+ readonly totalCalls: number;
715
+ /** Tools carrying at least one recorded call. */
716
+ readonly toolCount: number;
473
717
  }
474
718
  /** Usage text returned for unparseable /mcp invocations. */
475
719
  export declare const MCP_COMMAND_USAGE: string;
@@ -481,6 +725,8 @@ export type ParsedMcpCommandInput = {
481
725
  name: string;
482
726
  } | {
483
727
  form: 'config';
728
+ } | {
729
+ form: 'suggest';
484
730
  } | {
485
731
  form: 'disable';
486
732
  id: number;
@@ -493,9 +739,10 @@ export type ParsedMcpCommandInput = {
493
739
  /**
494
740
  * Parse one /mcp raw input into its form. `''` and `'list'` are the tree
495
741
  * overview; `'list <name>'` is a server/tool detail; `'config'` shows the
496
- * effective configuration; `'disable <n>'` / `'enable <n>'` are whole-server
497
- * toggles (n is the stable id shown by `/mcp`, so a bare `0` or non-numeric
498
- * token is already usage). Everything else parses to the usage error.
742
+ * effective configuration; `'suggest'` shows the current keep suggestions;
743
+ * `'disable <n>'` / `'enable <n>'` are whole-server toggles (n is the stable id
744
+ * shown by `/mcp`, so a bare `0` or non-numeric token is already usage).
745
+ * Everything else parses to the usage error.
499
746
  * @param rawInput - exact text following the command name.
500
747
  * @returns the normalized command form.
501
748
  */
@@ -643,6 +890,24 @@ export interface McpDetailView {
643
890
  * @returns the rendered view.
644
891
  */
645
892
  export declare function renderMcpDetail(name: string, schemas: readonly ToolSchema[], options: McpCommandOptions): McpDetailView;
893
+ /**
894
+ * Render the keep-suggestion view (`/mcp suggest`): the current batch of
895
+ * System One verdicts about which FOLDED tools earned a permanent place in
896
+ * every prompt, each with its probability, the usage evidence behind it, and
897
+ * the exact `keep` pattern the operator would write.
898
+ *
899
+ * Strictly a DISPLAY (red line ③): nothing here writes keep, prefix or the
900
+ * gate — the operator decides, one settings-page edit, with the same manual
901
+ * authority they have over every other knob.
902
+ *
903
+ * Every state is explained rather than blank: jev off, jev on with no batch
904
+ * yet, and jev on with rows. Output is line-capped defensively.
905
+ *
906
+ * @param suggestions - the current batch (production: suggestions.json).
907
+ * @param options - adapter knobs; `jev.jevEnabled` and `usage` are read.
908
+ * @returns the rendered view.
909
+ */
910
+ export declare function renderMcpSuggest(suggestions: readonly SuggestionRecord[], options: Pick<McpCommandOptions, 'jev' | 'usage'>): string;
646
911
  /**
647
912
  * Render the effective configuration with each knob's hitting tools:
648
913
  * which names match the prefix, what each keep pattern holds back, and what
@@ -686,6 +951,13 @@ export interface McpCommandView {
686
951
  * finishes in the background — same-process code cannot hard-kill it.
687
952
  */
688
953
  signal?: AbortSignal;
954
+ /**
955
+ * The current keep-suggestion batch for `/mcp suggest` (production: read
956
+ * from `<gate dir>/suggestions.json` at invocation time). Absent ⇒ the view
957
+ * reports "no suggestions yet", which is also what a missing or unreadable
958
+ * document means.
959
+ */
960
+ suggestions?: readonly SuggestionRecord[];
689
961
  }
690
962
  /** CommandResult-compatible outcome (platform shape: success may omit text). */
691
963
  export interface McpCommandOutcome {
@@ -759,18 +1031,54 @@ export declare function renderDispatched(metaArgs: unknown, value: unknown, reso
759
1031
  export declare function delegateFinalizeContent(metaArgs: unknown, result: Readonly<ToolExecutionResult>, exec: Readonly<ToolExecution>, resolveChild: (name: string) => ToolDefinition | undefined): ContentBlock[] | undefined;
760
1032
  /**
761
1033
  * Build the `mcp_list` meta-tool definition.
762
- * @param options - catalog knobs (prefix, description limit). MAY be a live
763
- * options object (plain getters): the factory reads `prefix`, `servers`,
764
- * `descriptionLimit` and `getGate` through the object on EVERY execution,
765
- * so volatile config can be wired by reference — only the description text
766
- * freezes at factory time.
1034
+ *
1035
+ * The execute layer is where the async seam lives: a no-argument call is the
1036
+ * pre-seim builder verbatim (no promise, no remote), and only a `query` call
1037
+ * consults `options.rankQuery` — and even then only AFTER a candidate pool
1038
+ * exists, so a disabled/failed rerank changes the ORDER and nothing else.
1039
+ *
1040
+ * @param options - catalog knobs (prefix, description limit, optional
1041
+ * rankQuery). MAY be a live options object (plain getters): the factory
1042
+ * reads `prefix`, `servers`, `descriptionLimit`, `getGate` and
1043
+ * `rankQuery` through the object on EVERY execution, so volatile config can
1044
+ * be wired by reference — only the description text freezes at factory
1045
+ * time.
767
1046
  * @param listSchemas - visible-schemas source (production: `ctx.tools.schemas`,
768
1047
  * called with the calling agent so restrictions are respected).
769
1048
  * @returns the complete tool definition.
770
1049
  */
771
1050
  export declare function createMcpListTool(options: McpListOptions, listSchemas: (scope?: ScopeKey) => readonly ToolSchema[]): ToolDefinition;
1051
+ /**
1052
+ * Optional seams {@link createMcpCallTool} wires into mcp_call. All three are
1053
+ * contained: a throwing hook degrades to the pre-seam behavior and never
1054
+ * breaks a dispatch.
1055
+ */
1056
+ export interface McpCallHooks {
1057
+ /**
1058
+ * Near-miss pool for the unknown-tool branch, resolved in the CALLING
1059
+ * agent's scope (production: the scope's visible prefix tools). Absent ⇒
1060
+ * the pre-seam error text, unchanged.
1061
+ */
1062
+ didYouMean?: (scope: ScopeKey | undefined) => readonly RankCandidate[];
1063
+ /**
1064
+ * Async rerank of that pool (production: System One). Resolving to
1065
+ * `undefined` or rejecting keeps the lexical order.
1066
+ */
1067
+ didYouMeanRank?: (scope: ScopeKey | undefined, tool: string, candidates: readonly RankCandidate[]) => Promise<readonly string[] | undefined>;
1068
+ /**
1069
+ * Told about every finished dispatch, success and failure alike (production:
1070
+ * the usage store). Receives `tool: undefined` for a call that never named
1071
+ * one. Synchronous and best-effort: this is on the hot path, so it must not
1072
+ * return a promise and must never be awaited.
1073
+ */
1074
+ onDispatched?: (event: {
1075
+ tool: string | undefined;
1076
+ error: boolean;
1077
+ }) => void;
1078
+ }
772
1079
  /**
773
1080
  * Build the `mcp_call` meta-tool definition.
1081
+ *
774
1082
  * @param prefix - the configured MCP prefix (dispatch boundary). A thunk is
775
1083
  * accepted so volatile config is re-read on every dispatch; a plain string
776
1084
  * freezes the boundary at factory time.
@@ -782,9 +1090,10 @@ export declare function createMcpListTool(options: McpListOptions, listSchemas:
782
1090
  * gate store's current mirror; default/omitted: nothing is disabled).
783
1091
  * Deliberately a callback — the definition object outlives any one gate
784
1092
  * snapshot and must observe later /mcp enable/disable writes.
1093
+ * @param hooks - the usage/did-you-mean seams (default: none wired).
785
1094
  * @returns the complete tool definition.
786
1095
  */
787
- export declare function createMcpCallTool(prefix: string | (() => string), resolve: (name: string, scope?: ScopeKey) => ToolDefinition | undefined, servers?: readonly string[] | (() => readonly string[]), getGate?: () => ServerGateState | undefined): ToolDefinition;
1096
+ export declare function createMcpCallTool(prefix: string | (() => string), resolve: (name: string, scope?: ScopeKey) => ToolDefinition | undefined, servers?: readonly string[] | (() => readonly string[]), getGate?: () => ServerGateState | undefined, hooks?: McpCallHooks): ToolDefinition;
788
1097
  /**
789
1098
  * Register the two meta-tools and install the assembly folding listener.
790
1099
  *
@@ -799,11 +1108,19 @@ export declare function createMcpCallTool(prefix: string | (() => string), resol
799
1108
  * unreadable document degrades to the empty gate (everything enabled) at
800
1109
  * load time, keeping the assemble waterfall alive.
801
1110
  *
802
- * Config volatility: every knob (prefix/keep/servers/descriptionLimit) is a
803
- * volatile reference — 0.1.7 settings-page writes swap values in place
804
- * WITHOUT remounting the plugin, so nothing captured at apply time may go
805
- * stale: the waterfall, both meta-tools and the /mcp command all re-read
806
- * through {@link cfgNow} per use.
1111
+ * Config volatility: every knob (prefix/keep/servers/descriptionLimit AND the
1112
+ * seven jev* keys) is a volatile reference — 0.1.7 settings-page writes swap
1113
+ * values in place WITHOUT remounting the plugin, so nothing captured at apply
1114
+ * time may go stale: the waterfall, both meta-tools and the /mcp command all
1115
+ * re-read through {@link cfgNow} / {@link jevEnv} per use.
1116
+ *
1117
+ * System One wiring (the only remote calls in this plugin): the decision log,
1118
+ * the usage mirror and the suggestion document all live in the SAME gate
1119
+ * directory as gate.json. The seams are wired to exactly three places — the
1120
+ * background keep-suggestion batch ({@link maybeSuggestBatch}, fired by the
1121
+ * usage counter and awaited by nobody), `mcp_list`'s model-explicit `query`,
1122
+ * and `mcp_call`'s unknown-tool branch. Folding, an unqueried catalog, and a
1123
+ * successful dispatch touch none of them.
807
1124
  *
808
1125
  * Commands: `/mcp` mounts through a runtime `ctx.inject(['commands'], ...)`,
809
1126
  * so hosts without the commands service keep this plugin fully functional —