@savvy-web/mcp 3.3.0 → 3.4.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/errors.js CHANGED
@@ -1,4 +1,4 @@
1
- import { ToolFailure } from "@effected/mcp";
1
+ import { ToolFailure, ToolRefusal } from "@effected/mcp";
2
2
  import { Schema } from "effect";
3
3
 
4
4
  //#region src/errors.ts
@@ -14,18 +14,6 @@ var WorkspaceNotFound = class extends Schema.TaggedError()("WorkspaceNotFound",
14
14
  cwd: Schema.String
15
15
  }) {};
16
16
  /**
17
- * A tool argument was structurally acceptable but semantically invalid — a
18
- * `repos_manage` action missing the field it needs, a `biome_check` path
19
- * outside the workspace, a `changeset_validate` directory that does not
20
- * exist. `message` is composed through `ToolFailure.message`.
21
- *
22
- * @public
23
- */
24
- var InvalidArgument = class extends Schema.TaggedError()("InvalidArgument", {
25
- ...ToolFailure.fields,
26
- argument: Schema.String
27
- }) {};
28
- /**
29
17
  * A silk-effects engine program failed with one of its own typed errors
30
18
  * (`TurboError`, `GitError`, `ReposConfigError`, …). `source` carries that
31
19
  * error's `_tag`; `message` is its own rendering plus the remediation,
@@ -38,13 +26,6 @@ var EngineError = class extends Schema.TaggedError()("EngineError", {
38
26
  source: Schema.String
39
27
  }) {};
40
28
  /**
41
- * No Biome binary could be located. `message` is composed through
42
- * `ToolFailure.message`.
43
- *
44
- * @public
45
- */
46
- var BiomeUnavailable = class extends Schema.TaggedError()("BiomeUnavailable", { ...ToolFailure.fields }) {};
47
- /**
48
29
  * Biome itself failed (exit status above 1, a spawn error, or a timeout) —
49
30
  * distinct from "lint issues found", which is a successful result. `message`
50
31
  * is composed through `ToolFailure.message`.
@@ -58,10 +39,9 @@ var BiomeFailed = class extends Schema.TaggedError()("BiomeFailed", {
58
39
  /** The one failure schema every savvy-mcp tool declares. @public */
59
40
  const McpToolError = Schema.Union([
60
41
  WorkspaceNotFound,
61
- InvalidArgument,
62
42
  EngineError,
63
- BiomeUnavailable,
64
- BiomeFailed
43
+ BiomeFailed,
44
+ ToolRefusal
65
45
  ]);
66
46
  /** The remediation every `WorkspaceNotFound` carries. */
67
47
  const WORKSPACE_REMEDIATION = {
@@ -82,30 +62,28 @@ const workspaceNotFound = (cwd) => new WorkspaceNotFound({
82
62
  remediation: WORKSPACE_REMEDIATION
83
63
  });
84
64
  /**
85
- * Build an {@link InvalidArgument}: `raw` is the human message (already
86
- * truncated by the caller where it echoes an argument).
87
- *
88
- * @public
89
- */
90
- const invalidArgument = (argument, raw, remediation) => new InvalidArgument({
91
- argument,
92
- message: ToolFailure.message(raw, remediation),
93
- remediation
94
- });
95
- /**
96
65
  * Build an {@link EngineError} from a silk-effects typed error. Every engine
97
66
  * error in this server renders itself through a `message` getter, so that
98
67
  * rendering is the raw message — passed through `ToolFailure.truncate` at
99
68
  * `ToolFailure.ENGINE_ECHO_LIMIT`, since the kit embeds caller values in it;
100
69
  * `source` keeps the tag for anything that inspects the typed error directly.
70
+ * An `Error` in the engine error's own `cause` is appended to that rendering
71
+ * unless the rendering already contains it: a kit error that renders only a
72
+ * one-line summary keeps the actionable detail in `cause`, while one that
73
+ * already folds its cause in (`CatalogAssemblyError` since `@effected/npm`
74
+ * 0.17.0) would otherwise print the detail twice.
101
75
  *
102
76
  * @public
103
77
  */
104
- const engineError = (cause, remediation) => new EngineError({
105
- source: cause._tag,
106
- message: ToolFailure.message(ToolFailure.truncate(cause.message, ToolFailure.ENGINE_ECHO_LIMIT), remediation),
107
- remediation
108
- });
78
+ const engineError = (cause, remediation) => {
79
+ const detail = cause.cause instanceof Error ? cause.cause.message : "";
80
+ const rendered = detail && !cause.message.includes(detail) ? `${cause.message}: ${detail}` : cause.message;
81
+ return new EngineError({
82
+ source: cause._tag,
83
+ message: ToolFailure.message(ToolFailure.truncate(rendered, ToolFailure.ENGINE_ECHO_LIMIT), remediation),
84
+ remediation
85
+ });
86
+ };
109
87
  /**
110
88
  * The shared mapping every handler applies to its engine error channel: the
111
89
  * kit's `WorkspaceRootNotFoundError` becomes {@link WorkspaceNotFound} for the
@@ -121,4 +99,4 @@ const engineError = (cause, remediation) => new EngineError({
121
99
  const mapEngineError = (requestedCwd, remediation) => (cause) => cause._tag === "WorkspaceRootNotFoundError" ? workspaceNotFound(requestedCwd) : engineError(cause, remediation);
122
100
 
123
101
  //#endregion
124
- export { BiomeFailed, BiomeUnavailable, EngineError, InvalidArgument, McpToolError, WorkspaceNotFound, engineError, invalidArgument, mapEngineError, workspaceNotFound };
102
+ export { BiomeFailed, EngineError, McpToolError, WorkspaceNotFound, engineError, mapEngineError, workspaceNotFound };
package/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Distribution } from "@effected/engine";
2
+ import { ToolRefusal } from "@effected/mcp";
2
3
  import { FileSystem, Layer, Path, Schema, Stdio } from "effect";
3
4
  import { WorkspaceRoot } from "@effected/workspaces";
4
5
  import { Changesets, Repos, SilkWorkspaceAnalyzer, Turbo } from "@savvy-web/silk-effects";
@@ -22,24 +23,6 @@ declare const WorkspaceNotFound_base: Schema.Class<WorkspaceNotFound, Schema.Tag
22
23
  * @public
23
24
  */
24
25
  export declare class WorkspaceNotFound extends WorkspaceNotFound_base {}
25
- declare const InvalidArgument_base: Schema.Class<InvalidArgument, Schema.TaggedStruct<"InvalidArgument", {
26
- readonly message: Schema.String;
27
- readonly remediation: Schema.Struct<{
28
- readonly hint: Schema.String;
29
- readonly suggestedTool: Schema.optionalKey<Schema.String>;
30
- readonly suggestedArgs: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Unknown>>;
31
- }>;
32
- readonly argument: Schema.String;
33
- }>, import("effect/Cause").YieldableError>;
34
- /**
35
- * A tool argument was structurally acceptable but semantically invalid — a
36
- * `repos_manage` action missing the field it needs, a `biome_check` path
37
- * outside the workspace, a `changeset_validate` directory that does not
38
- * exist. `message` is composed through `ToolFailure.message`.
39
- *
40
- * @public
41
- */
42
- export declare class InvalidArgument extends InvalidArgument_base {}
43
26
  declare const EngineError_base: Schema.Class<EngineError, Schema.TaggedStruct<"EngineError", {
44
27
  readonly message: Schema.String;
45
28
  readonly remediation: Schema.Struct<{
@@ -58,21 +41,6 @@ declare const EngineError_base: Schema.Class<EngineError, Schema.TaggedStruct<"E
58
41
  * @public
59
42
  */
60
43
  export declare class EngineError extends EngineError_base {}
61
- declare const BiomeUnavailable_base: Schema.Class<BiomeUnavailable, Schema.TaggedStruct<"BiomeUnavailable", {
62
- readonly message: Schema.String;
63
- readonly remediation: Schema.Struct<{
64
- readonly hint: Schema.String;
65
- readonly suggestedTool: Schema.optionalKey<Schema.String>;
66
- readonly suggestedArgs: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Unknown>>;
67
- }>;
68
- }>, import("effect/Cause").YieldableError>;
69
- /**
70
- * No Biome binary could be located. `message` is composed through
71
- * `ToolFailure.message`.
72
- *
73
- * @public
74
- */
75
- export declare class BiomeUnavailable extends BiomeUnavailable_base {}
76
44
  declare const BiomeFailed_base: Schema.Class<BiomeFailed, Schema.TaggedStruct<"BiomeFailed", {
77
45
  readonly message: Schema.String;
78
46
  readonly remediation: Schema.Struct<{
@@ -91,7 +59,7 @@ declare const BiomeFailed_base: Schema.Class<BiomeFailed, Schema.TaggedStruct<"B
91
59
  */
92
60
  export declare class BiomeFailed extends BiomeFailed_base {}
93
61
  /** The one failure schema every savvy-mcp tool declares. @public */
94
- export declare const McpToolError: Schema.Union<readonly [typeof WorkspaceNotFound, typeof InvalidArgument, typeof EngineError, typeof BiomeUnavailable, typeof BiomeFailed]>;
62
+ export declare const McpToolError: Schema.Union<readonly [typeof WorkspaceNotFound, typeof EngineError, typeof BiomeFailed, typeof ToolRefusal]>;
95
63
  /** @public */
96
64
  export type McpToolError = typeof McpToolError.Type;
97
65
  //#endregion
@@ -207,7 +175,7 @@ declare const biomeCheckTool: Tool.Tool<"biome_check", {
207
175
  readonly wrote: Schema.Boolean;
208
176
  readonly guidance: Schema.String;
209
177
  }>;
210
- readonly failure: Schema.Union<readonly [typeof WorkspaceNotFound, typeof InvalidArgument, typeof EngineError, typeof BiomeUnavailable, typeof BiomeFailed]>;
178
+ readonly failure: Schema.Union<readonly [typeof WorkspaceNotFound, typeof EngineError, typeof BiomeFailed, typeof ToolRefusal]>;
211
179
  readonly failureMode: "error";
212
180
  }, never>;
213
181
  //#endregion
@@ -239,7 +207,7 @@ declare const changesetDepsDetectTool: Tool.Tool<"changeset_deps_detect", {
239
207
  readonly packages: Schema.$Array<Schema.String>;
240
208
  }>>;
241
209
  }>;
242
- readonly failure: Schema.Union<readonly [typeof WorkspaceNotFound, typeof InvalidArgument, typeof EngineError, typeof BiomeUnavailable, typeof BiomeFailed]>;
210
+ readonly failure: Schema.Union<readonly [typeof WorkspaceNotFound, typeof EngineError, typeof BiomeFailed, typeof import("@effected/mcp").ToolRefusal]>;
243
211
  readonly failureMode: "error";
244
212
  }, Changesets.DepsRegen | WorkspaceRoot>;
245
213
  //#endregion
@@ -265,7 +233,7 @@ declare const changesetDepsRegenTool: Tool.Tool<"changeset_deps_regen", {
265
233
  }>>;
266
234
  readonly dryRun: Schema.Boolean;
267
235
  }>;
268
- readonly failure: Schema.Union<readonly [typeof WorkspaceNotFound, typeof InvalidArgument, typeof EngineError, typeof BiomeUnavailable, typeof BiomeFailed]>;
236
+ readonly failure: Schema.Union<readonly [typeof WorkspaceNotFound, typeof EngineError, typeof BiomeFailed, typeof import("@effected/mcp").ToolRefusal]>;
269
237
  readonly failureMode: "error";
270
238
  }, Changesets.DepsRegen | WorkspaceRoot>;
271
239
  //#endregion
@@ -341,7 +309,7 @@ declare const changesetInspectTool: Tool.Tool<"changeset_inspect", {
341
309
  }>, Schema.Null]>;
342
310
  }>>;
343
311
  }>]>;
344
- readonly failure: Schema.Union<readonly [typeof WorkspaceNotFound, typeof InvalidArgument, typeof EngineError, typeof BiomeUnavailable, typeof BiomeFailed]>;
312
+ readonly failure: Schema.Union<readonly [typeof WorkspaceNotFound, typeof EngineError, typeof BiomeFailed, typeof import("@effected/mcp").ToolRefusal]>;
345
313
  readonly failureMode: "error";
346
314
  }, Changesets.BranchAnalyzer | Changesets.ConfigInspector | WorkspaceRoot>;
347
315
  //#endregion
@@ -370,7 +338,7 @@ declare const changesetPreviewTool: Tool.Tool<"changeset_preview", {
370
338
  }>>;
371
339
  }>>;
372
340
  }>;
373
- readonly failure: Schema.Union<readonly [typeof WorkspaceNotFound, typeof InvalidArgument, typeof EngineError, typeof BiomeUnavailable, typeof BiomeFailed]>;
341
+ readonly failure: Schema.Union<readonly [typeof WorkspaceNotFound, typeof EngineError, typeof BiomeFailed, typeof import("@effected/mcp").ToolRefusal]>;
374
342
  readonly failureMode: "error";
375
343
  }, Changesets.ReleasePlanner | WorkspaceRoot>;
376
344
  //#endregion
@@ -393,7 +361,7 @@ declare const changesetValidateTool: Tool.Tool<"changeset_validate", {
393
361
  readonly message: Schema.String;
394
362
  }>>;
395
363
  }>;
396
- readonly failure: Schema.Union<readonly [typeof WorkspaceNotFound, typeof InvalidArgument, typeof EngineError, typeof BiomeUnavailable, typeof BiomeFailed]>;
364
+ readonly failure: Schema.Union<readonly [typeof WorkspaceNotFound, typeof EngineError, typeof BiomeFailed, typeof ToolRefusal]>;
397
365
  readonly failureMode: "error";
398
366
  }, WorkspaceRoot>;
399
367
  //#endregion
@@ -458,128 +426,154 @@ declare const reposInspectTool: Tool.Tool<"repos_inspect", {
458
426
  }>>;
459
427
  readonly parseError: Schema.optionalKey<Schema.String>;
460
428
  }>]>;
461
- readonly failure: Schema.Union<readonly [typeof WorkspaceNotFound, typeof InvalidArgument, typeof EngineError, typeof BiomeUnavailable, typeof BiomeFailed]>;
429
+ readonly failure: Schema.Union<readonly [typeof WorkspaceNotFound, typeof EngineError, typeof BiomeFailed, typeof import("@effected/mcp").ToolRefusal]>;
462
430
  readonly failureMode: "error";
463
431
  }, FileSystem.FileSystem | Path.Path | Repos.ReposConfigStore | Repos.ReposDrift | Repos.ReposManager | WorkspaceRoot>;
464
432
  //#endregion
465
433
  //#region src/tools/repos-manage.d.ts
466
- /** The `repos_manage` tool value. Mutating: not read-only, not idempotent. */
467
- declare const reposManageTool: Tool.Tool<"repos_manage", {
468
- readonly parameters: Schema.Struct<{
469
- readonly action: Schema.Literals<readonly ["sync", "pin", "add", "note", "remove", "rename", "restore", "deregister"]>;
470
- readonly name: Schema.optionalKey<Schema.String>;
471
- readonly newName: Schema.optionalKey<Schema.String>;
472
- readonly ref: Schema.optionalKey<Schema.String>;
473
- readonly url: Schema.optionalKey<Schema.String>;
474
- readonly purpose: Schema.optionalKey<Schema.String>;
475
- readonly sparse: Schema.optionalKey<Schema.$Array<Schema.String>>;
476
- readonly orientation: Schema.optionalKey<Schema.Struct<{
477
- readonly layout: Schema.optional<Schema.String>;
478
- readonly keyPaths: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
479
- readonly startHere: Schema.optional<Schema.String>;
480
- }>>;
481
- readonly op: Schema.optionalKey<Schema.Literals<readonly ["add", "remove", "promote"]>>;
482
- readonly note: Schema.optionalKey<Schema.String>;
483
- readonly id: Schema.optionalKey<Schema.String>;
484
- readonly into: Schema.optionalKey<Schema.Literals<readonly ["layout", "startHere"]>>;
485
- readonly names: Schema.optionalKey<Schema.$Array<Schema.String>>;
486
- readonly section: Schema.optionalKey<Schema.String>;
487
- readonly cwd: Schema.optionalKey<Schema.String>;
434
+ /**
435
+ * The `repos_manage` tool value: a `McpToolkit.unionTool`, so the served input
436
+ * schema is the request union's strict `oneOf` keyed by `action`, and a bad
437
+ * call is rejected as invalid parameters before the handler runs. Mutating:
438
+ * not read-only, not idempotent.
439
+ */
440
+ declare const reposManageTool: import("@effected/mcp").UnionTool<"repos_manage", Schema.Union<readonly [Schema.Struct<{
441
+ readonly action: Schema.Literal<"sync">;
442
+ readonly cwd: Schema.optionalKey<Schema.String>;
443
+ }>, Schema.Struct<{
444
+ readonly action: Schema.Literal<"pin">;
445
+ readonly name: Schema.String;
446
+ readonly ref: Schema.String;
447
+ readonly cwd: Schema.optionalKey<Schema.String>;
448
+ }>, Schema.Struct<{
449
+ readonly action: Schema.Literal<"add">;
450
+ readonly url: Schema.String;
451
+ readonly ref: Schema.String;
452
+ readonly purpose: Schema.String;
453
+ readonly name: Schema.optionalKey<Schema.String>;
454
+ readonly sparse: Schema.optionalKey<Schema.$Array<Schema.String>>;
455
+ readonly orientation: Schema.optionalKey<Schema.Struct<{
456
+ readonly layout: Schema.optional<Schema.String>;
457
+ readonly keyPaths: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
458
+ readonly startHere: Schema.optional<Schema.String>;
459
+ }>>;
460
+ readonly cwd: Schema.optionalKey<Schema.String>;
461
+ }>, Schema.Struct<{
462
+ readonly action: Schema.Literal<"note">;
463
+ readonly name: Schema.String;
464
+ readonly op: Schema.Literals<readonly ["add", "remove", "promote"]>;
465
+ readonly note: Schema.optionalKey<Schema.String>;
466
+ readonly id: Schema.optionalKey<Schema.String>;
467
+ readonly into: Schema.optionalKey<Schema.Literals<readonly ["layout", "startHere"]>>;
468
+ readonly cwd: Schema.optionalKey<Schema.String>;
469
+ }>, Schema.Struct<{
470
+ readonly action: Schema.Literal<"remove">;
471
+ readonly name: Schema.String;
472
+ readonly cwd: Schema.optionalKey<Schema.String>;
473
+ }>, Schema.Struct<{
474
+ readonly action: Schema.Literal<"rename">;
475
+ readonly name: Schema.String;
476
+ readonly newName: Schema.String;
477
+ readonly cwd: Schema.optionalKey<Schema.String>;
478
+ }>, Schema.Struct<{
479
+ readonly action: Schema.Literal<"restore">;
480
+ readonly names: Schema.optionalKey<Schema.$Array<Schema.String>>;
481
+ readonly cwd: Schema.optionalKey<Schema.String>;
482
+ }>, Schema.Struct<{
483
+ readonly action: Schema.Literal<"deregister">;
484
+ readonly section: Schema.String;
485
+ readonly cwd: Schema.optionalKey<Schema.String>;
486
+ }>]>, Schema.Union<readonly [Schema.Struct<{
487
+ readonly action: Schema.Literal<"sync">;
488
+ readonly result: Schema.Struct<{
489
+ readonly initialized: Schema.$Array<Schema.String>;
490
+ readonly sparseApplied: Schema.$Array<Schema.String>;
491
+ readonly upToDate: Schema.$Array<Schema.String>;
492
+ readonly clearedLocks: Schema.$Array<Schema.String>;
493
+ readonly urlSynced: Schema.$Array<Schema.String>;
494
+ readonly registered: Schema.$Array<Schema.String>;
495
+ readonly boundaryMarked: Schema.$Array<Schema.String>;
488
496
  }>;
489
- readonly success: Schema.Union<readonly [Schema.Struct<{
490
- readonly action: Schema.Literal<"sync">;
491
- readonly result: Schema.Struct<{
492
- readonly initialized: Schema.$Array<Schema.String>;
493
- readonly sparseApplied: Schema.$Array<Schema.String>;
494
- readonly upToDate: Schema.$Array<Schema.String>;
495
- readonly clearedLocks: Schema.$Array<Schema.String>;
496
- readonly urlSynced: Schema.$Array<Schema.String>;
497
- readonly registered: Schema.$Array<Schema.String>;
498
- readonly boundaryMarked: Schema.$Array<Schema.String>;
499
- }>;
500
- }>, Schema.Struct<{
501
- readonly action: Schema.Literal<"pin">;
502
- readonly result: Schema.Struct<{
503
- readonly name: Schema.String;
497
+ }>, Schema.Struct<{
498
+ readonly action: Schema.Literal<"pin">;
499
+ readonly result: Schema.Struct<{
500
+ readonly name: Schema.String;
501
+ readonly ref: Schema.String;
502
+ readonly oldCommit: Schema.NullOr<Schema.String>;
503
+ readonly newCommit: Schema.String;
504
+ readonly commitMessage: Schema.String;
505
+ readonly staleNoteIds: Schema.$Array<Schema.String>;
506
+ }>;
507
+ }>, Schema.Struct<{
508
+ readonly action: Schema.Literal<"add">;
509
+ readonly result: Schema.Struct<{
510
+ readonly name: Schema.String;
511
+ readonly ref: Schema.String;
512
+ readonly path: Schema.String;
513
+ }>;
514
+ }>, Schema.Struct<{
515
+ readonly action: Schema.Literal<"note">;
516
+ readonly result: Schema.Struct<{
517
+ readonly name: Schema.String;
518
+ readonly op: Schema.Literals<readonly ["add", "remove", "promote"]>;
519
+ readonly id: Schema.String;
520
+ readonly noteCount: Schema.Number;
521
+ }>;
522
+ }>, Schema.Struct<{
523
+ readonly action: Schema.Literal<"remove">;
524
+ readonly result: Schema.Struct<{
525
+ readonly name: Schema.String;
526
+ readonly path: Schema.String;
527
+ readonly commitMessage: Schema.String;
528
+ readonly removedNotes: Schema.$Array<Schema.Struct<{
529
+ readonly id: Schema.String;
530
+ readonly date: Schema.String;
504
531
  readonly ref: Schema.String;
505
- readonly oldCommit: Schema.NullOr<Schema.String>;
506
- readonly newCommit: Schema.String;
507
- readonly commitMessage: Schema.String;
508
- readonly staleNoteIds: Schema.$Array<Schema.String>;
509
- }>;
510
- }>, Schema.Struct<{
511
- readonly action: Schema.Literal<"add">;
512
- readonly result: Schema.Struct<{
513
- readonly name: Schema.String;
532
+ readonly note: Schema.String;
533
+ }>>;
534
+ readonly removedEntry: Schema.Struct<{
535
+ readonly url: Schema.String;
514
536
  readonly ref: Schema.String;
515
- readonly path: Schema.String;
516
- }>;
517
- }>, Schema.Struct<{
518
- readonly action: Schema.Literal<"note">;
519
- readonly result: Schema.Struct<{
520
- readonly name: Schema.String;
521
- readonly op: Schema.Literals<readonly ["add", "remove", "promote"]>;
522
- readonly id: Schema.String;
523
- readonly noteCount: Schema.Number;
524
- }>;
525
- }>, Schema.Struct<{
526
- readonly action: Schema.Literal<"remove">;
527
- readonly result: Schema.Struct<{
528
- readonly name: Schema.String;
529
- readonly path: Schema.String;
530
- readonly commitMessage: Schema.String;
531
- readonly removedNotes: Schema.$Array<Schema.Struct<{
537
+ readonly purpose: Schema.String;
538
+ readonly sparse: Schema.optional<Schema.$Array<Schema.String>>;
539
+ readonly orientation: Schema.optional<Schema.Struct<{
540
+ readonly layout: Schema.optional<Schema.String>;
541
+ readonly keyPaths: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
542
+ readonly startHere: Schema.optional<Schema.String>;
543
+ }>>;
544
+ readonly notes: Schema.optional<Schema.$Array<Schema.Struct<{
532
545
  readonly id: Schema.String;
533
546
  readonly date: Schema.String;
534
547
  readonly ref: Schema.String;
535
548
  readonly note: Schema.String;
536
- }>>;
537
- readonly removedEntry: Schema.Struct<{
538
- readonly url: Schema.String;
539
- readonly ref: Schema.String;
540
- readonly purpose: Schema.String;
541
- readonly sparse: Schema.optional<Schema.$Array<Schema.String>>;
542
- readonly orientation: Schema.optional<Schema.Struct<{
543
- readonly layout: Schema.optional<Schema.String>;
544
- readonly keyPaths: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
545
- readonly startHere: Schema.optional<Schema.String>;
546
- }>>;
547
- readonly notes: Schema.optional<Schema.$Array<Schema.Struct<{
548
- readonly id: Schema.String;
549
- readonly date: Schema.String;
550
- readonly ref: Schema.String;
551
- readonly note: Schema.String;
552
- }>>>;
553
- }>;
554
- }>;
555
- }>, Schema.Struct<{
556
- readonly action: Schema.Literal<"rename">;
557
- readonly result: Schema.Struct<{
558
- readonly oldName: Schema.String;
559
- readonly newName: Schema.String;
560
- readonly path: Schema.String;
561
- readonly commitMessage: Schema.String;
562
- }>;
563
- }>, Schema.Struct<{
564
- readonly action: Schema.Literal<"restore">;
565
- readonly result: Schema.Struct<{
566
- readonly restored: Schema.$Array<Schema.Struct<{
567
- readonly name: Schema.String;
568
- readonly commit: Schema.String;
569
- }>>;
570
- readonly skippedClean: Schema.$Array<Schema.String>;
571
- readonly stillDirty: Schema.$Array<Schema.String>;
549
+ }>>>;
572
550
  }>;
573
- }>, Schema.Struct<{
574
- readonly action: Schema.Literal<"deregister">;
575
- readonly result: Schema.Struct<{
576
- readonly section: Schema.String;
577
- readonly removedKeys: Schema.$Array<Schema.String>;
578
- }>;
579
- }>]>;
580
- readonly failure: Schema.Union<readonly [typeof WorkspaceNotFound, typeof InvalidArgument, typeof EngineError, typeof BiomeUnavailable, typeof BiomeFailed]>;
581
- readonly failureMode: "error";
582
- }, Repos.ReposManager | WorkspaceRoot>;
551
+ }>;
552
+ }>, Schema.Struct<{
553
+ readonly action: Schema.Literal<"rename">;
554
+ readonly result: Schema.Struct<{
555
+ readonly oldName: Schema.String;
556
+ readonly newName: Schema.String;
557
+ readonly path: Schema.String;
558
+ readonly commitMessage: Schema.String;
559
+ }>;
560
+ }>, Schema.Struct<{
561
+ readonly action: Schema.Literal<"restore">;
562
+ readonly result: Schema.Struct<{
563
+ readonly restored: Schema.$Array<Schema.Struct<{
564
+ readonly name: Schema.String;
565
+ readonly commit: Schema.String;
566
+ }>>;
567
+ readonly skippedClean: Schema.$Array<Schema.String>;
568
+ readonly stillDirty: Schema.$Array<Schema.String>;
569
+ }>;
570
+ }>, Schema.Struct<{
571
+ readonly action: Schema.Literal<"deregister">;
572
+ readonly result: Schema.Struct<{
573
+ readonly section: Schema.String;
574
+ readonly removedKeys: Schema.$Array<Schema.String>;
575
+ }>;
576
+ }>]>, Schema.Union<readonly [typeof WorkspaceNotFound, typeof EngineError, typeof BiomeFailed, typeof ToolRefusal]>, Repos.ReposManager | WorkspaceRoot>;
583
577
  //#endregion
584
578
  //#region src/tools/turbo-inspect.d.ts
585
579
  /** The `turbo_inspect` tool value. */
@@ -641,7 +635,7 @@ declare const turboInspectTool: Tool.Tool<"turbo_inspect", {
641
635
  readonly dependents: Schema.$Array<Schema.String>;
642
636
  }>;
643
637
  }>]>;
644
- readonly failure: Schema.Union<readonly [typeof WorkspaceNotFound, typeof InvalidArgument, typeof EngineError, typeof BiomeUnavailable, typeof BiomeFailed]>;
638
+ readonly failure: Schema.Union<readonly [typeof WorkspaceNotFound, typeof EngineError, typeof BiomeFailed, typeof import("@effected/mcp").ToolRefusal]>;
645
639
  readonly failureMode: "error";
646
640
  }, Turbo.TurboInspector | WorkspaceRoot>;
647
641
  //#endregion
@@ -678,7 +672,7 @@ declare const workspaceInfoTool: Tool.Tool<"workspace_info", {
678
672
  readonly fixed: Schema.$Array<Schema.String>;
679
673
  }>>;
680
674
  }>;
681
- readonly failure: Schema.Union<readonly [typeof WorkspaceNotFound, typeof InvalidArgument, typeof EngineError, typeof BiomeUnavailable, typeof BiomeFailed]>;
675
+ readonly failure: Schema.Union<readonly [typeof WorkspaceNotFound, typeof EngineError, typeof BiomeFailed, typeof import("@effected/mcp").ToolRefusal]>;
682
676
  readonly failureMode: "error";
683
677
  }, SilkWorkspaceAnalyzer | WorkspaceRoot>;
684
678
  //#endregion
package/index.js CHANGED
@@ -1,7 +1,7 @@
1
- import { BiomeFailed, BiomeUnavailable, EngineError, InvalidArgument, McpToolError, WorkspaceNotFound } from "./errors.js";
1
+ import { BiomeFailed, EngineError, McpToolError, WorkspaceNotFound } from "./errors.js";
2
2
  import { makeSilkRuntimeLayer } from "./runtime.js";
3
3
  import { SilkToolkit, ToolsLayer } from "./toolkit.js";
4
4
  import { CURRENT_MCP_VERSION } from "./version.js";
5
5
  import { ServerLayer } from "./server.js";
6
6
 
7
- export { BiomeFailed, BiomeUnavailable, CURRENT_MCP_VERSION, EngineError, InvalidArgument, McpToolError, ServerLayer, SilkToolkit, ToolsLayer, WorkspaceNotFound, makeSilkRuntimeLayer };
7
+ export { BiomeFailed, CURRENT_MCP_VERSION, EngineError, McpToolError, ServerLayer, SilkToolkit, ToolsLayer, WorkspaceNotFound, makeSilkRuntimeLayer };
package/main.d.ts CHANGED
@@ -21,7 +21,8 @@ export interface MainOptions {
21
21
  * `SAVVY_MCP_PROJECT_DIR`, then `CLAUDE_PROJECT_DIR`, then the working
22
22
  * directory — an empty value or an unsubstituted `${VAR}` placeholder is
23
23
  * skipped (`LaunchContext.projectDir`). NO static imports of the server
24
- * graph — see the module remarks.
24
+ * graph — see the module remarks. The guard policy is the kit's default:
25
+ * a stray exception or rejection exits 1, before or after connecting.
25
26
  *
26
27
  * @public
27
28
  */
package/main.js CHANGED
@@ -1,8 +1,6 @@
1
+ import { McpGuard } from "@effected/mcp/guard";
2
+
1
3
  //#region src/main.ts
2
- const fatal = (label, error) => {
3
- process.stderr.write(`savvy-mcp: ${label}: ${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
4
- process.exit(1);
5
- };
6
4
  /**
7
5
  * Run the savvy MCP server over stdio. Owns the process.
8
6
  *
@@ -11,26 +9,31 @@ const fatal = (label, error) => {
11
9
  * `SAVVY_MCP_PROJECT_DIR`, then `CLAUDE_PROJECT_DIR`, then the working
12
10
  * directory — an empty value or an unsubstituted `${VAR}` placeholder is
13
11
  * skipped (`LaunchContext.projectDir`). NO static imports of the server
14
- * graph — see the module remarks.
12
+ * graph — see the module remarks. The guard policy is the kit's default:
13
+ * a stray exception or rejection exits 1, before or after connecting.
15
14
  *
16
15
  * @public
17
16
  */
18
- const main = async (options = {}) => {
19
- process.on("uncaughtException", (e) => fatal("uncaught exception", e));
20
- process.on("unhandledRejection", (r) => fatal("unhandled rejection", r));
21
- const { NodeRuntime, NodeServices } = await import("@effect/platform-node");
22
- const { Layer } = await import("effect");
23
- const { LaunchContext } = await import("@effected/engine");
24
- const { McpStdio } = await import("@effected/mcp");
25
- const { ServerLayer } = await import("./server.js");
26
- const cwd = LaunchContext.projectDir({
27
- argv: process.argv.slice(2, 3),
28
- env: process.env,
29
- keys: ["SAVVY_MCP_PROJECT_DIR", "CLAUDE_PROJECT_DIR"],
30
- cwd: process.cwd()
31
- });
32
- NodeRuntime.runMain(McpStdio.launch(ServerLayer(cwd, options).pipe(Layer.provide(NodeServices.layer))), { teardown: McpStdio.teardown });
33
- };
17
+ const main = (options = {}) => McpGuard.run({
18
+ label: "savvy-mcp",
19
+ host: process,
20
+ load: async () => {
21
+ const NodeRuntime = await import("@effect/platform-node/NodeRuntime");
22
+ const NodeServices = await import("@effect/platform-node/NodeServices");
23
+ const { Layer } = await import("effect");
24
+ const { LaunchContext } = await import("@effected/engine");
25
+ const { ServerLayer } = await import("./server.js");
26
+ return {
27
+ layer: ServerLayer(LaunchContext.projectDir({
28
+ argv: process.argv.slice(2, 3),
29
+ env: process.env,
30
+ keys: ["SAVVY_MCP_PROJECT_DIR", "CLAUDE_PROJECT_DIR"],
31
+ cwd: process.cwd()
32
+ }), options).pipe(Layer.provide(NodeServices.layer)),
33
+ runMain: NodeRuntime.runMain
34
+ };
35
+ }
36
+ });
34
37
  /* v8 ignore stop */
35
38
 
36
39
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savvy-web/mcp",
3
- "version": "3.3.0",
3
+ "version": "3.4.1",
4
4
  "private": false,
5
5
  "description": "The savvy MCP server — Silk Suite tooling and library knowledge for coding agents",
6
6
  "homepage": "https://github.com/savvy-web/systems/tree/main/packages/mcp",
@@ -40,10 +40,10 @@
40
40
  "@effected/commands": "^0.9.0",
41
41
  "@effected/engine": "^0.1.0",
42
42
  "@effected/git": "^0.17.0",
43
- "@effected/mcp": "^0.1.1",
43
+ "@effected/mcp": "^0.2.0",
44
44
  "@effected/templates": "^0.8.0",
45
- "@effected/workspaces": "^0.26.0",
46
- "@savvy-web/silk-effects": "9.1.3",
45
+ "@effected/workspaces": "^0.28.0",
46
+ "@savvy-web/silk-effects": "9.2.0",
47
47
  "effect": "4.0.0-rc.117"
48
48
  }
49
49
  }
package/toolkit.js CHANGED
@@ -36,7 +36,7 @@ const ToolsLayer = (cwd) => SilkToolkit.toLayer({
36
36
  changeset_preview: (params) => handleChangesetPreview(cwd, params),
37
37
  changeset_deps_regen: (params) => handleChangesetDepsRegen(cwd, params),
38
38
  repos_inspect: (params) => handleReposInspect(cwd, params),
39
- repos_manage: (params) => handleReposManage(cwd, params),
39
+ repos_manage: handleReposManage(cwd),
40
40
  biome_check: (params) => handleBiomeCheck(cwd, params)
41
41
  });
42
42
 
@@ -1,5 +1,5 @@
1
- import { BiomeFailed, BiomeUnavailable, InvalidArgument, McpToolError, invalidArgument } from "../errors.js";
2
- import { ToolFailure } from "@effected/mcp";
1
+ import { BiomeFailed, McpToolError } from "../errors.js";
2
+ import { ToolFailure, ToolRefusal } from "@effected/mcp";
3
3
  import { Effect, Schema } from "effect";
4
4
  import { Lint } from "@savvy-web/silk-effects";
5
5
  import { Tool } from "effect/unstable/ai";
@@ -210,30 +210,27 @@ const biomeFailed = (raw, exitCode) => new BiomeFailed({
210
210
  *
211
211
  * @remarks Resolves the Biome binary via {@link Lint.Biome.findBiome} (global
212
212
  * first, then the project's package manager). Throws a typed
213
- * {@link McpToolError} member — {@link InvalidArgument} for a cwd/path outside
214
- * the containment tree, {@link BiomeUnavailable} when no binary is found,
215
- * {@link BiomeFailed} when Biome exits with status > 1 (Biome itself failed,
216
- * vs. status 1 = lint issues found) — which {@link handleBiomeCheck} lifts
217
- * into the Effect error channel unchanged.
213
+ * {@link McpToolError} member — a `ToolRefusal` for a cwd/path outside the
214
+ * containment tree or when no binary is found, {@link BiomeFailed} when Biome
215
+ * exits with status > 1 (Biome itself failed, vs. status 1 = lint issues
216
+ * found) — which {@link handleBiomeCheck} lifts into the Effect error channel
217
+ * unchanged.
218
218
  */
219
219
  const runBiomeCheck = async (args, fallbackCwd) => {
220
220
  const mode = args.mode ?? "check";
221
221
  const root = canonicalize(fallbackCwd);
222
222
  const cwd = canonicalize(args.cwd ?? fallbackCwd);
223
223
  const containmentRoot = resolveContainmentRoot(root, cwd);
224
- if (containmentRoot === null) throw invalidArgument("cwd", `cwd escapes the workspace root: ${ToolFailure.truncate(args.cwd ?? fallbackCwd)}.`, CONTAINMENT_REMEDIATION);
224
+ if (containmentRoot === null) throw ToolRefusal.refuse(`cwd escapes the workspace root: ${ToolFailure.truncate(args.cwd ?? fallbackCwd)}.`, CONTAINMENT_REMEDIATION);
225
225
  const within = (abs) => abs === containmentRoot || abs.startsWith(`${containmentRoot}${sep}`);
226
226
  const paths = (args.paths && args.paths.length > 0 ? args.paths : ["."]).map((p) => {
227
227
  const lexical = resolve(cwd, p);
228
- if (!within(canonicalize(lexical))) throw invalidArgument("paths", `path escapes the workspace root: ${ToolFailure.truncate(p)}.`, CONTAINMENT_REMEDIATION);
228
+ if (!within(canonicalize(lexical))) throw ToolRefusal.refuse(`path escapes the workspace root: ${ToolFailure.truncate(p)}.`, CONTAINMENT_REMEDIATION);
229
229
  return relative(cwd, lexical) || ".";
230
230
  });
231
231
  const doWrite = Boolean(args.write || args.unsafe);
232
232
  const biomeCmd = Lint.Biome.findBiome();
233
- if (!biomeCmd) throw new BiomeUnavailable({
234
- message: ToolFailure.message("Biome not found.", BIOME_REMEDIATION),
235
- remediation: BIOME_REMEDIATION
236
- });
233
+ if (!biomeCmd) throw ToolRefusal.refuse("Biome not found.", BIOME_REMEDIATION);
237
234
  const parts = biomeCmd.split(" ");
238
235
  const bin = parts[0];
239
236
  const prefix = parts.slice(1);
@@ -303,12 +300,18 @@ const biomeCheckTool = Tool.make("biome_check", {
303
300
  success: BiomeCheckResult,
304
301
  failure: McpToolError
305
302
  }).annotate(Tool.Title, "Biome check").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, false).annotate(Tool.OpenWorld, false);
306
- const isMcpToolError = (u) => u instanceof InvalidArgument || u instanceof BiomeUnavailable || u instanceof BiomeFailed;
303
+ /**
304
+ * Every declared failure, recognized from the one union the tool declares, so
305
+ * a member added to {@link McpToolError} is passed through here without a
306
+ * second list to keep in step.
307
+ */
308
+ const isMcpToolError = Schema.is(McpToolError);
307
309
  /**
308
310
  * Wire handler: {@link runBiomeCheck} lifted into Effect. The typed members it
309
311
  * throws pass through unchanged; anything else (a defect in the parser, say)
310
312
  * is reported as {@link BiomeFailed} so the error channel stays closed over
311
- * {@link McpToolError}.
313
+ * {@link McpToolError} — nothing undeclared reaches core, which would scrub
314
+ * it to a generic internal-error text.
312
315
  */
313
316
  const handleBiomeCheck = (fallbackCwd, params) => Effect.tryPromise({
314
317
  try: () => runBiomeCheck(params, fallbackCwd),
@@ -68,7 +68,7 @@ const ChangesetDepsDetectParams = Schema.Struct({
68
68
  cwd: Schema.optionalKey(Schema.String.annotate({ description: "Directory to resolve the workspace root from." }))
69
69
  });
70
70
  const REMEDIATION = {
71
- hint: "The dependency diff could not be planned; check that the base branch exists locally and that every named package is a workspace member.",
71
+ hint: "The dependency diff could not be planned; check that the base branch exists locally, that every named package is a workspace member, and that every config dependency the base side declares is installed or in the pnpm store (a bumped config dependency's old version usually is not).",
72
72
  suggestedTool: "workspace_info"
73
73
  };
74
74
  /** The `changeset_deps_detect` tool value. */
@@ -77,7 +77,7 @@ const ChangesetDepsRegenParams = Schema.Struct({
77
77
  cwd: Schema.optionalKey(Schema.String.annotate({ description: "Directory to resolve the workspace root from." }))
78
78
  });
79
79
  const REMEDIATION = {
80
- hint: "The dependency changesets could not be regenerated; preview the plan with dryRun=true, and check that the base branch exists locally and that every named package is a workspace member.",
80
+ hint: "The dependency changesets could not be regenerated; preview the plan with dryRun=true, and check that the base branch exists locally, that every named package is a workspace member, and that every config dependency the base side declares is installed or in the pnpm store (a bumped config dependency's old version usually is not).",
81
81
  suggestedTool: "changeset_deps_detect"
82
82
  };
83
83
  /** The `changeset_deps_regen` tool value. Mutating: not read-only, not idempotent. */
@@ -1,10 +1,18 @@
1
1
  import { McpToolError, mapEngineError } from "../errors.js";
2
+ import { ToolOutputSchema } from "@effected/mcp";
2
3
  import { Effect, Schema } from "effect";
3
4
  import { WorkspaceRoot } from "@effected/workspaces";
4
5
  import { Changesets } from "@savvy-web/silk-effects";
5
6
  import { Tool } from "effect/unstable/ai";
6
7
 
7
8
  //#region src/tools/changeset-inspect.ts
9
+ /**
10
+ * The `changeset_inspect` MCP tool: a discriminated-union result keyed by `mode`
11
+ * (branch | config), each variant embedding the corresponding resolved-output
12
+ * schema from silk-effects' Changesets namespace. Read-only.
13
+ *
14
+ * @packageDocumentation
15
+ */
8
16
  /** Branch-analysis variant. */
9
17
  const ChangesetBranchResult = Schema.Struct({
10
18
  mode: Schema.Literal("branch"),
@@ -20,12 +28,15 @@ const ChangesetClassifyResult = Schema.Struct({
20
28
  mode: Schema.Literal("classify"),
21
29
  result: Schema.Array(Changesets.ClassificationSchema)
22
30
  }).annotate({ identifier: "ChangesetClassifyResult" });
23
- /** The `changeset_inspect` tool result — a discriminated union keyed by `mode`. */
24
- const ChangesetInspectResult = Schema.Union([
31
+ /**
32
+ * The `changeset_inspect` tool result — a discriminated union keyed by `mode`,
33
+ * object-rooted so its `outputSchema` is served.
34
+ */
35
+ const ChangesetInspectResult = ToolOutputSchema.objectRooted(Schema.Union([
25
36
  ChangesetBranchResult,
26
37
  ChangesetConfigResult,
27
38
  ChangesetClassifyResult
28
- ]).annotate({
39
+ ])).annotate({
29
40
  identifier: "ChangesetInspectResult",
30
41
  title: "changeset_inspect result",
31
42
  description: "Read-only changeset analysis grouped by mode (branch | config | classify)."
@@ -1,5 +1,5 @@
1
- import { McpToolError, invalidArgument, mapEngineError } from "../errors.js";
2
- import { ToolFailure } from "@effected/mcp";
1
+ import { McpToolError, mapEngineError } from "../errors.js";
2
+ import { ToolFailure, ToolRefusal } from "@effected/mcp";
3
3
  import { Data, Effect, Schema } from "effect";
4
4
  import { WorkspaceRoot } from "@effected/workspaces";
5
5
  import { Changesets } from "@savvy-web/silk-effects";
@@ -77,10 +77,10 @@ const changesetValidateTool = Tool.make("changeset_validate", {
77
77
  * Wire handler: {@link changesetValidate} with its error channel mapped onto
78
78
  * {@link McpToolError}. The typed {@link ChangesetValidateError} (a thrown
79
79
  * validate — in practice a missing directory) is an argument problem, so it
80
- * becomes {@link InvalidArgument} naming `dir`; the echoed directory is
80
+ * becomes a `ToolRefusal` naming the directory; the echoed directory is
81
81
  * truncated.
82
82
  */
83
- const handleChangesetValidate = (fallbackCwd, params) => changesetValidate(params, fallbackCwd).pipe(Effect.mapError((error) => error._tag === "ChangesetValidateError" ? invalidArgument("dir", `Changeset directory "${ToolFailure.truncate(error.dir)}" could not be validated: ${describeCause(error.cause)}`, DIR_REMEDIATION) : mapEngineError(params.cwd ?? fallbackCwd, DIR_REMEDIATION)(error)));
83
+ const handleChangesetValidate = (fallbackCwd, params) => changesetValidate(params, fallbackCwd).pipe(Effect.mapError((error) => error._tag === "ChangesetValidateError" ? ToolRefusal.refuse(`Changeset directory "${ToolFailure.truncate(error.dir)}" could not be validated: ${describeCause(error.cause)}`, DIR_REMEDIATION) : mapEngineError(params.cwd ?? fallbackCwd, DIR_REMEDIATION)(error)));
84
84
  const describeCause = (cause) => cause instanceof Error ? ToolFailure.truncate(cause.message) : ToolFailure.truncate(String(cause));
85
85
 
86
86
  //#endregion
@@ -1,4 +1,5 @@
1
1
  import { McpToolError, mapEngineError } from "../errors.js";
2
+ import { ToolOutputSchema } from "@effected/mcp";
2
3
  import { Effect, FileSystem, Option, Path, Result, Schema } from "effect";
3
4
  import { WorkspaceRoot } from "@effected/workspaces";
4
5
  import { Repos } from "@savvy-web/silk-effects";
@@ -50,13 +51,16 @@ const ReposGitmodulesResult = Schema.Struct({
50
51
  entries: Schema.Array(GitmodulesEntrySchema),
51
52
  parseError: Schema.optionalKey(Schema.String)
52
53
  }).annotate({ identifier: "ReposGitmodulesResult" });
53
- /** The `repos_inspect` tool result — a discriminated union keyed by `mode`. */
54
- const ReposInspectResult = Schema.Union([
54
+ /**
55
+ * The `repos_inspect` tool result — a discriminated union keyed by `mode`,
56
+ * object-rooted so its `outputSchema` is served.
57
+ */
58
+ const ReposInspectResult = ToolOutputSchema.objectRooted(Schema.Union([
55
59
  ReposStatusResult,
56
60
  ReposConfigResult,
57
61
  ReposDriftResult,
58
62
  ReposGitmodulesResult
59
- ]).annotate({
63
+ ])).annotate({
60
64
  identifier: "ReposInspectResult",
61
65
  title: "repos_inspect result",
62
66
  description: "Drift report (status), the parsed manifest (config), the five-authority reconciliation report (drift), or the decoded .gitmodules sections (gitmodules)."
@@ -1,6 +1,6 @@
1
- import { McpToolError, invalidArgument, mapEngineError } from "../errors.js";
2
- import { ToolFailure } from "@effected/mcp";
3
- import { Effect, Schema } from "effect";
1
+ import { McpToolError, mapEngineError } from "../errors.js";
2
+ import { McpToolkit, ToolOutputSchema, ToolRefusal } from "@effected/mcp";
3
+ import { Effect, Result, Schema } from "effect";
4
4
  import { WorkspaceRoot } from "@effected/workspaces";
5
5
  import { Repos } from "@savvy-web/silk-effects";
6
6
  import { Tool } from "effect/unstable/ai";
@@ -9,70 +9,117 @@ import { Tool } from "effect/unstable/ai";
9
9
  /**
10
10
  * The `repos_manage` MCP tool: one action-discriminated mutating tool
11
11
  * covering `sync`, `pin`, `add`, `note`, `remove`, `rename`, `restore`, and
12
- * `deregister` against the vendored `.repos/` submodules. The wire schema is flat (no
13
- * `oneOf`); the handler maps it into an internal `Schema.TaggedStruct`
14
- * request union that names the missing field per action on decode failure.
15
- * Mutating — no `readOnlyHint`.
12
+ * `deregister` against the vendored `.repos/` submodules. Its parameters are
13
+ * a `Schema.Union` of one struct per action, served by `@effected/mcp`'s
14
+ * `McpToolkit.unionTool` as a strict `oneOf` keyed by `action`; the handler
15
+ * receives the decoded member. Mutating — no `readOnlyHint`.
16
16
  *
17
17
  * @packageDocumentation
18
18
  */
19
- /** `sync` has no extra fields. */
20
- const SyncRequest = Schema.TaggedStruct("sync", {});
19
+ /** The optional `cwd` every action takes. */
20
+ const cwd = Schema.optionalKey(Schema.String.annotate({ description: "Directory to resolve the workspace root from." }));
21
+ /** `sync` has no fields of its own. */
22
+ const SyncRequest = Schema.Struct({
23
+ action: Schema.Literal("sync"),
24
+ cwd
25
+ }).annotate({ description: "Initialize/reconcile submodules per the manifest." });
21
26
  /** `pin` requires both `name` and `ref`. */
22
- const PinRequest = Schema.TaggedStruct("pin", {
23
- name: Schema.String,
24
- ref: Schema.String
25
- });
27
+ const PinRequest = Schema.Struct({
28
+ action: Schema.Literal("pin"),
29
+ name: Schema.String.annotate({ description: "Repo name to re-pin." }),
30
+ ref: Schema.String.annotate({ description: "Git ref to pin to." }),
31
+ cwd
32
+ }).annotate({ description: "Re-pin a repo to a new ref." });
26
33
  /**
27
34
  * `add` requires `url`/`ref`/`purpose`; `name`, `sparse` and `orientation`
28
35
  * are optional. `orientation` is what makes a re-vendor lossless — pass back
29
36
  * the block a preceding `remove` reported.
30
37
  */
31
- const AddRequest = Schema.TaggedStruct("add", {
32
- url: Schema.String,
33
- ref: Schema.String,
34
- purpose: Schema.String,
35
- name: Schema.optional(Schema.String),
36
- sparse: Schema.optional(Schema.Array(Schema.String)),
37
- orientation: Schema.optional(Repos.RepoOrientation)
38
- });
38
+ const AddRequest = Schema.Struct({
39
+ action: Schema.Literal("add"),
40
+ url: Schema.String.annotate({ description: "Repo URL to vendor." }),
41
+ ref: Schema.String.annotate({ description: "Git ref to vendor." }),
42
+ purpose: Schema.String.annotate({ description: "One-line purpose for the manifest." }),
43
+ name: Schema.optionalKey(Schema.String.annotate({ description: "Override the repo name." })),
44
+ sparse: Schema.optionalKey(Schema.Array(Schema.String).annotate({ description: "Sparse-checkout patterns." })),
45
+ orientation: Schema.optionalKey(Repos.RepoOrientation.annotate({ description: "Orientation block to write. Pass back what a preceding remove reported as removedEntry.orientation — add does NOT restore it on its own, so a re-vendor loses it otherwise." })),
46
+ cwd
47
+ }).annotate({ description: "Vendor a new repo." });
48
+ /**
49
+ * The note operation a `note` request names, or the reason it is incomplete.
50
+ * The fields each `op` needs are enforced here rather than as three members
51
+ * sharing `action: "note"`, so the served `oneOf` keeps one member per
52
+ * action; the same function is the decode-time filter and the handler's
53
+ * narrowing, so the two cannot disagree.
54
+ */
55
+ const noteOperation = (request) => {
56
+ switch (request.op) {
57
+ case "add": return request.note === void 0 ? Result.fail("note op \"add\" requires `note`") : Result.succeed({
58
+ op: "add",
59
+ note: request.note
60
+ });
61
+ case "remove": return request.id === void 0 ? Result.fail("note op \"remove\" requires `id`") : Result.succeed({
62
+ op: "remove",
63
+ id: request.id
64
+ });
65
+ case "promote": return request.id === void 0 || request.into === void 0 ? Result.fail("note op \"promote\" requires both `id` and `into`") : Result.succeed({
66
+ op: "promote",
67
+ id: request.id,
68
+ into: request.into
69
+ });
70
+ }
71
+ };
39
72
  /**
40
73
  * `note` requires `name` and `op`; the fields required beyond that depend on
41
74
  * `op` — enforced by the trailing filter so the decode error names exactly
42
75
  * what's missing for the chosen op.
43
76
  */
44
- const NoteRequest = Schema.TaggedStruct("note", {
45
- name: Schema.String,
77
+ const NoteRequest = Schema.Struct({
78
+ action: Schema.Literal("note"),
79
+ name: Schema.String.annotate({ description: "Repo name the note belongs to." }),
46
80
  op: Schema.Literals([
47
81
  "add",
48
82
  "remove",
49
83
  "promote"
50
- ]),
51
- note: Schema.optional(Schema.String),
52
- id: Schema.optional(Schema.String),
53
- into: Schema.optional(Schema.Literals(["layout", "startHere"]))
54
- }).check(Schema.makeFilter((request) => {
55
- if (request.op === "add" && request.note === void 0) return "note op \"add\" requires `note`";
56
- if (request.op === "remove" && request.id === void 0) return "note op \"remove\" requires `id`";
57
- if (request.op === "promote" && (request.id === void 0 || request.into === void 0)) return "note op \"promote\" requires both `id` and `into`";
58
- return true;
59
- }));
84
+ ]).annotate({ description: "Note operation." }),
85
+ note: Schema.optionalKey(Schema.String.annotate({ description: "Note text (op=add)." })),
86
+ id: Schema.optionalKey(Schema.String.annotate({ description: "Note id (op=remove|promote)." })),
87
+ into: Schema.optionalKey(Schema.Literals(["layout", "startHere"]).annotate({ description: "Orientation target (op=promote)." })),
88
+ cwd
89
+ }).check(Schema.makeFilter((request) => Result.match(noteOperation(request), {
90
+ onFailure: (reason) => reason,
91
+ onSuccess: () => true
92
+ }))).annotate({ description: "Add, remove or promote an agent note." });
60
93
  /** `remove` requires only `name`. */
61
- const RemoveRequest = Schema.TaggedStruct("remove", { name: Schema.String });
94
+ const RemoveRequest = Schema.Struct({
95
+ action: Schema.Literal("remove"),
96
+ name: Schema.String.annotate({ description: "Repo name to unvendor." }),
97
+ cwd
98
+ }).annotate({ description: "Unvendor a repo." });
62
99
  /** `rename` requires `name` (the old name) and `newName`. */
63
- const RenameRequest = Schema.TaggedStruct("rename", {
64
- name: Schema.String,
65
- newName: Schema.String
66
- });
100
+ const RenameRequest = Schema.Struct({
101
+ action: Schema.Literal("rename"),
102
+ name: Schema.String.annotate({ description: "The repo's current name." }),
103
+ newName: Schema.String.annotate({ description: "New repo name." }),
104
+ cwd
105
+ }).annotate({ description: "Rename a vendored repo's manifest key and worktree." });
67
106
  /** `restore`'s `names` is optional and repeatable, mirroring `add`'s `sparse`; omitted means "every dirty entry". */
68
- const RestoreRequest = Schema.TaggedStruct("restore", { names: Schema.optional(Schema.Array(Schema.String)) });
107
+ const RestoreRequest = Schema.Struct({
108
+ action: Schema.Literal("restore"),
109
+ names: Schema.optionalKey(Schema.Array(Schema.String).annotate({ description: "Repo names to restore; omitted restores every dirty repo." })),
110
+ cwd
111
+ }).annotate({ description: "Hard-reset worktrees back to their pinned gitlink commits." });
69
112
  /**
70
113
  * `deregister` requires `section` — the stale registration name exactly as the
71
114
  * drift report states it (e.g. `.repos/old-name`); the `submodule.` prefix is
72
115
  * implied, never passed.
73
116
  */
74
- const DeregisterRequest = Schema.TaggedStruct("deregister", { section: Schema.String });
75
- /** Internal tagged-union request the flat wire args decode into. */
117
+ const DeregisterRequest = Schema.Struct({
118
+ action: Schema.Literal("deregister"),
119
+ section: Schema.String.annotate({ description: "Stale registration name to clear, exactly as the drift report states it (e.g. .repos/old-name); the submodule. prefix is implied." }),
120
+ cwd
121
+ }).annotate({ description: "Clear a stale submodule registration from local git config." });
122
+ /** The `repos_manage` parameters: one struct per action, discriminated by `action`. */
76
123
  const ReposManageRequest = Schema.Union([
77
124
  SyncRequest,
78
125
  PinRequest,
@@ -123,8 +170,11 @@ const ReposManageDeregisterResult = Schema.Struct({
123
170
  action: Schema.Literal("deregister"),
124
171
  result: Repos.ReposDeregisterResult
125
172
  }).annotate({ identifier: "ReposManageDeregisterResult" });
126
- /** The `repos_manage` tool result — a discriminated union keyed by `action`. */
127
- const ReposManageResult = Schema.Union([
173
+ /**
174
+ * The `repos_manage` tool result — a discriminated union keyed by `action`,
175
+ * object-rooted so its `outputSchema` is served (every member is a struct).
176
+ */
177
+ const ReposManageResult = ToolOutputSchema.objectRooted(Schema.Union([
128
178
  ReposManageSyncResult,
129
179
  ReposManagePinResult,
130
180
  ReposManageAddResult,
@@ -133,25 +183,24 @@ const ReposManageResult = Schema.Union([
133
183
  ReposManageRenameResult,
134
184
  ReposManageRestoreResult,
135
185
  ReposManageDeregisterResult
136
- ]).annotate({
186
+ ])).annotate({
137
187
  identifier: "ReposManageResult",
138
188
  title: "repos_manage result",
139
189
  description: "Result of a mutating repos action: sync, pin, add, note, remove, rename, restore, or deregister."
140
190
  });
191
+ const REMEDIATION = {
192
+ hint: "The mutation did not complete; inspect the vendored-repo state before retrying.",
193
+ suggestedTool: "repos_inspect"
194
+ };
195
+ const REQUEST_REMEDIATION = { hint: "Pass the fields the chosen note op needs: note (op=add), id (op=remove), or id and into (op=promote)." };
141
196
  /**
142
- * Effect handler: resolve the workspace root, decode the flat wire args into
143
- * the internal per-action request (naming the missing field on failure), then
144
- * dispatch to the matching `ReposManager` method.
197
+ * Effect handler: resolve the workspace root, then dispatch the decoded
198
+ * request to the matching `ReposManager` method.
145
199
  */
146
- const reposManage = (args, fallbackCwd) => Effect.gen(function* () {
147
- const root = yield* (yield* WorkspaceRoot).find(args.cwd ?? fallbackCwd);
200
+ const reposManage = (request, fallbackCwd) => Effect.gen(function* () {
201
+ const root = yield* (yield* WorkspaceRoot).find(request.cwd ?? fallbackCwd);
148
202
  const manager = yield* Repos.ReposManager;
149
- const { action, cwd: _cwd, ...rest } = args;
150
- const request = yield* Schema.decodeUnknownEffect(ReposManageRequest)({
151
- _tag: action,
152
- ...rest
153
- });
154
- switch (request._tag) {
203
+ switch (request.action) {
155
204
  case "sync": return {
156
205
  action: "sync",
157
206
  result: yield* manager.sync(root)
@@ -172,20 +221,11 @@ const reposManage = (args, fallbackCwd) => Effect.gen(function* () {
172
221
  })
173
222
  };
174
223
  case "note": {
175
- const noteOp = request.op === "add" ? {
176
- op: "add",
177
- note: request.note
178
- } : request.op === "remove" ? {
179
- op: "remove",
180
- id: request.id
181
- } : {
182
- op: "promote",
183
- id: request.id,
184
- into: request.into
185
- };
224
+ const operation = noteOperation(request);
225
+ if (Result.isFailure(operation)) return yield* ToolRefusal.refuse(`repos_manage note: ${operation.failure}.`, REQUEST_REMEDIATION);
186
226
  return {
187
227
  action: "note",
188
- result: yield* manager.note(root, request.name, noteOp)
228
+ result: yield* manager.note(root, request.name, operation.success)
189
229
  };
190
230
  }
191
231
  case "remove": return {
@@ -206,59 +246,27 @@ const reposManage = (args, fallbackCwd) => Effect.gen(function* () {
206
246
  };
207
247
  }
208
248
  });
209
- /** Wire parameters for `repos_manage`: flat (no `oneOf`); the handler decodes them per action. */
210
- const ReposManageParams = Schema.Struct({
211
- action: Schema.Literals([
212
- "sync",
213
- "pin",
214
- "add",
215
- "note",
216
- "remove",
217
- "rename",
218
- "restore",
219
- "deregister"
220
- ]).annotate({ description: "Which mutation to perform." }),
221
- name: Schema.optionalKey(Schema.String.annotate({ description: "Repo name (pin, note, remove, rename; optional override for add)." })),
222
- newName: Schema.optionalKey(Schema.String.annotate({ description: "New repo name (rename)." })),
223
- ref: Schema.optionalKey(Schema.String.annotate({ description: "Git ref to pin/vendor to (pin, add)." })),
224
- url: Schema.optionalKey(Schema.String.annotate({ description: "Repo URL to vendor (add)." })),
225
- purpose: Schema.optionalKey(Schema.String.annotate({ description: "One-line purpose for the manifest (add)." })),
226
- sparse: Schema.optionalKey(Schema.Array(Schema.String).annotate({ description: "Sparse-checkout patterns (add)." })),
227
- orientation: Schema.optionalKey(Repos.RepoOrientation.annotate({ description: "Orientation block to write (add). Pass back what a preceding remove reported as removedEntry.orientation — add does NOT restore it on its own, so a re-vendor loses it otherwise." })),
228
- op: Schema.optionalKey(Schema.Literals([
229
- "add",
230
- "remove",
231
- "promote"
232
- ]).annotate({ description: "Note operation (note)." })),
233
- note: Schema.optionalKey(Schema.String.annotate({ description: "Note text (note, op=add)." })),
234
- id: Schema.optionalKey(Schema.String.annotate({ description: "Note id (note, op=remove|promote)." })),
235
- into: Schema.optionalKey(Schema.Literals(["layout", "startHere"]).annotate({ description: "Orientation target (note, op=promote)." })),
236
- names: Schema.optionalKey(Schema.Array(Schema.String).annotate({ description: "Repo names to restore (restore); omitted restores every dirty repo." })),
237
- section: Schema.optionalKey(Schema.String.annotate({ description: "Stale registration name to clear (deregister), exactly as the drift report states it (e.g. .repos/old-name); the submodule. prefix is implied." })),
238
- cwd: Schema.optionalKey(Schema.String.annotate({ description: "Directory to resolve the workspace root from." }))
239
- });
240
- const REMEDIATION = {
241
- hint: "The mutation did not complete; inspect the vendored-repo state before retrying.",
242
- suggestedTool: "repos_inspect"
243
- };
244
- const REQUEST_REMEDIATION = { hint: "Pass the fields the chosen action needs (pin: name+ref; add: url+ref+purpose; note: name+op plus note/id/into; remove: name; rename: name+newName; deregister: section)." };
245
- /** The `repos_manage` tool value. Mutating: not read-only, not idempotent. */
246
- const reposManageTool = Tool.make("repos_manage", {
247
- description: "Mutating: sync (initialize/reconcile submodules per the manifest), pin (re-pin a repo to a new ref), add (vendor a new repo), note (add/remove/promote an agent note), remove (unvendor a repo), rename (rename a vendored repo's manifest key and worktree), restore (hard-reset a repo's worktree back to its pinned gitlink commit and re-apply sparse paths — DESTRUCTIVE to uncommitted worktree edits; never run implicitly), or deregister (clear a STALE submodule.<section> registration from the superproject's local git config — the phantom entry repos_inspect drift reports as localRegistrationDivergence with no matching manifest entry; refuses a section outside .repos/ and any section still backing a live manifest entry — canonically named or gitdir-diverged — and touches local config only, so nothing is staged). Pass action plus the fields that action needs: pin needs name+ref; add needs url+ref+purpose (name/sparse/orientation optional — pass orientation back from a preceding remove's removedEntry to make a re-vendor lossless); note needs name+op, plus note (op=add), id (op=remove), or id+into (op=promote); remove needs name; rename needs name (the old name) + newName; restore takes an optional names list — omitted, it restores every dirty repo and reports the clean ones as skipped; given, it restores exactly those repos even if already clean; deregister needs section (the registration name exactly as the drift report states it, e.g. .repos/old-name — no submodule. prefix). A decode failure names the missing field. The pin result surfaces commitMessage and staleNoteIds — review and commit after pinning. The remove result surfaces commitMessage, removedNotes and the removed entry's orientation block — promote any durable notes elsewhere, keep the orientation if you intend to re-vendor, then review and commit. The rename result surfaces commitMessage — review and commit after renaming. The restore result names exactly what was discarded. The deregister result lists the config keys the removed section carried — nothing to commit afterwards. Returns a typed object in structuredContent (content[] carries the same object as JSON).",
248
- parameters: ReposManageParams,
249
+ /**
250
+ * The `repos_manage` tool value: a `McpToolkit.unionTool`, so the served input
251
+ * schema is the request union's strict `oneOf` keyed by `action`, and a bad
252
+ * call is rejected as invalid parameters before the handler runs. Mutating:
253
+ * not read-only, not idempotent.
254
+ */
255
+ const reposManageTool = McpToolkit.unionTool("repos_manage", {
256
+ description: "Mutating: sync (initialize/reconcile submodules per the manifest), pin (re-pin a repo to a new ref), add (vendor a new repo), note (add/remove/promote an agent note), remove (unvendor a repo), rename (rename a vendored repo's manifest key and worktree), restore (hard-reset a repo's worktree back to its pinned gitlink commit and re-apply sparse paths — DESTRUCTIVE to uncommitted worktree edits; never run implicitly), or deregister (clear a STALE submodule.<section> registration from the superproject's local git config — the phantom entry repos_inspect drift reports as localRegistrationDivergence with no matching manifest entry; refuses a section outside .repos/ and any section still backing a live manifest entry — canonically named or gitdir-diverged — and touches local config only, so nothing is staged). Pass action plus the fields that action needs: pin needs name+ref; add needs url+ref+purpose (name/sparse/orientation optional — pass orientation back from a preceding remove's removedEntry to make a re-vendor lossless); note needs name+op, plus note (op=add), id (op=remove), or id+into (op=promote); remove needs name; rename needs name (the old name) + newName; restore takes an optional names list — omitted, it restores every dirty repo and reports the clean ones as skipped; given, it restores exactly those repos even if already clean; deregister needs section (the registration name exactly as the drift report states it, e.g. .repos/old-name — no submodule. prefix). Each action's fields are served as one member of a strict oneOf keyed by action; a missing field or a key the chosen action does not take is rejected as invalid parameters naming it. The pin result surfaces commitMessage and staleNoteIds — review and commit after pinning. The remove result surfaces commitMessage, removedNotes and the removed entry's orientation block — promote any durable notes elsewhere, keep the orientation if you intend to re-vendor, then review and commit. The rename result surfaces commitMessage — review and commit after renaming. The restore result names exactly what was discarded. The deregister result lists the config keys the removed section carried — nothing to commit afterwards. Returns a typed object in structuredContent (content[] carries the same object as JSON).",
257
+ parameters: ReposManageRequest,
249
258
  success: ReposManageResult,
250
259
  failure: McpToolError,
251
260
  dependencies: [Repos.ReposManager, WorkspaceRoot]
252
261
  }).annotate(Tool.Title, "Manage vendored repos").annotate(Tool.Readonly, false).annotate(Tool.Destructive, true).annotate(Tool.Idempotent, false).annotate(Tool.OpenWorld, false);
253
262
  /**
254
- * Wire handler: {@link reposManage} with its error channel mapped onto
255
- * {@link McpToolError}. The per-action request decode failure (`SchemaError`,
256
- * which names the missing field, and echoes the decoded value) is an argument
257
- * problem and becomes {@link InvalidArgument} keyed on `action`, its text
258
- * truncated at `ToolFailure.ENGINE_ECHO_LIMIT`; the engine's own errors go
259
- * through {@link mapEngineError}.
263
+ * Wire handler: `McpToolkit.unionHandler` decodes the raw payload into one
264
+ * {@link ReposManageRequest} member (an unknown key or a missing field fails
265
+ * `InvalidParams`, as a `Tool.make` decode failure does), then
266
+ * {@link reposManage} runs with its engine errors mapped onto
267
+ * {@link McpToolError} through {@link mapEngineError}.
260
268
  */
261
- const handleReposManage = (fallbackCwd, params) => reposManage(params, fallbackCwd).pipe(Effect.mapError((error) => error._tag === "SchemaError" ? invalidArgument("action", `repos_manage ${params.action}: ${ToolFailure.truncate(error.message, ToolFailure.ENGINE_ECHO_LIMIT)}`, REQUEST_REMEDIATION) : mapEngineError(params.cwd ?? fallbackCwd, REMEDIATION)(error)));
269
+ const handleReposManage = (fallbackCwd) => McpToolkit.unionHandler(reposManageTool, (request) => reposManage(request, fallbackCwd).pipe(Effect.mapError((error) => error._tag === "ToolRefusal" ? error : mapEngineError(request.cwd ?? fallbackCwd, REMEDIATION)(error))));
262
270
 
263
271
  //#endregion
264
- export { ReposManageAddResult, ReposManageDeregisterResult, ReposManageNoteResult, ReposManageParams, ReposManagePinResult, ReposManageRemoveResult, ReposManageRenameResult, ReposManageRestoreResult, ReposManageResult, ReposManageSyncResult, handleReposManage, reposManage, reposManageTool };
272
+ export { ReposManageAddResult, ReposManageDeregisterResult, ReposManageNoteResult, ReposManagePinResult, ReposManageRemoveResult, ReposManageRenameResult, ReposManageRequest, ReposManageRestoreResult, ReposManageResult, ReposManageSyncResult, handleReposManage, reposManage, reposManageTool };
@@ -1,10 +1,18 @@
1
1
  import { McpToolError, mapEngineError } from "../errors.js";
2
+ import { ToolOutputSchema } from "@effected/mcp";
2
3
  import { Effect, Schema } from "effect";
3
4
  import { WorkspaceRoot } from "@effected/workspaces";
4
5
  import { Turbo } from "@savvy-web/silk-effects";
5
6
  import { Tool } from "effect/unstable/ai";
6
7
 
7
8
  //#region src/tools/turbo-inspect.ts
9
+ /**
10
+ * The `turbo_inspect` MCP tool: a discriminated-union result schema keyed by
11
+ * `mode` (cache | graph | affected), each variant embedding the corresponding
12
+ * `Turbo` result schema from silk-effects. Read-only.
13
+ *
14
+ * @packageDocumentation
15
+ */
8
16
  /** Cache-diagnosis variant of the `turbo_inspect` result. */
9
17
  const TurboCacheResult = Schema.Struct({
10
18
  mode: Schema.Literal("cache"),
@@ -20,12 +28,15 @@ const TurboAffectedResult = Schema.Struct({
20
28
  mode: Schema.Literal("affected"),
21
29
  result: Turbo.AffectedResult
22
30
  }).annotate({ identifier: "TurboAffectedResult" });
23
- /** The `turbo_inspect` tool result — a discriminated union keyed by `mode`. */
24
- const TurboInspectResult = Schema.Union([
31
+ /**
32
+ * The `turbo_inspect` tool result — a discriminated union keyed by `mode`,
33
+ * object-rooted so its `outputSchema` is served.
34
+ */
35
+ const TurboInspectResult = ToolOutputSchema.objectRooted(Schema.Union([
25
36
  TurboCacheResult,
26
37
  TurboGraphResult,
27
38
  TurboAffectedResult
28
- ]).annotate({
39
+ ])).annotate({
29
40
  identifier: "TurboInspectResult",
30
41
  title: "turbo_inspect result",
31
42
  description: "Read-only Turborepo inspection grouped by mode (cache | graph | affected)."
package/version.js CHANGED
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * @packageDocumentation
8
8
  */
9
- const CURRENT_MCP_VERSION = "3.3.0";
9
+ const CURRENT_MCP_VERSION = "3.4.1";
10
10
 
11
11
  //#endregion
12
12
  export { CURRENT_MCP_VERSION };