@savvy-web/mcp 1.8.1 → 2.0.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/bin/savvy-mcp.js CHANGED
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import { SilkRuntimeLive } from "../runtime.js";
2
+ import { makeSilkRuntimeLayer } from "../runtime.js";
3
3
  import { startMcpServer } from "../server.js";
4
4
  import { Layer, ManagedRuntime } from "effect";
5
- import { NodeContext } from "@effect/platform-node";
5
+ import { NodeServices } from "@effect/platform-node";
6
6
 
7
7
  //#region src/bin.ts
8
8
  /**
9
9
  * Binary entrypoint for the `savvy-mcp` server.
10
10
  *
11
- * Resolves the project working directory, builds the long-lived runtime, and
12
- * starts the MCP server over stdio.
11
+ * Resolves the project working directory, builds the long-lived runtime
12
+ * (root-bound to that directory), and starts the MCP server over stdio.
13
13
  *
14
14
  * @internal
15
15
  */
@@ -20,7 +20,7 @@ function resolveProjectDir() {
20
20
  }
21
21
  async function main() {
22
22
  const cwd = resolveProjectDir();
23
- const appLayer = SilkRuntimeLive.pipe(Layer.provide(NodeContext.layer));
23
+ const appLayer = makeSilkRuntimeLayer(cwd).pipe(Layer.provide(NodeServices.layer));
24
24
  const ctx = {
25
25
  runtime: ManagedRuntime.make(appLayer),
26
26
  cwd
package/index.d.ts CHANGED
@@ -1,29 +1,41 @@
1
+ import { WorkspaceRoot } from "@effected/workspaces";
1
2
  import { Changesets, Repos, SilkWorkspaceAnalyzer, Turbo } from "@savvy-web/silk-effects";
2
- import { Layer, ManagedRuntime } from "effect";
3
- import { WorkspaceDiscoveryError, WorkspaceRoot } from "workspaces-effect";
3
+ import { FileSystem, Layer, ManagedRuntime, Path } from "effect";
4
+ import { ChildProcessSpawner } from "effect/unstable/process";
4
5
  import "@modelcontextprotocol/sdk/server/mcp.js";
5
6
  //#region src/context.d.ts
7
+ /** Every service the MCP runtime provides to the tool handlers. */
8
+ type McpServices = SilkWorkspaceAnalyzer | WorkspaceRoot | Turbo.TurboInspector | Changesets.BranchAnalyzer | Changesets.ConfigInspector | Changesets.ReleasePlanner | Changesets.DepsRegen | Repos.ReposManager | Repos.ReposConfigStore;
6
9
  /** The long-lived runtime and the project working directory. */
7
10
  interface McpContext {
8
- readonly runtime: ManagedRuntime.ManagedRuntime<SilkWorkspaceAnalyzer | WorkspaceRoot | Turbo.TurboInspector | Changesets.BranchAnalyzer | Changesets.ConfigInspector | Changesets.ReleasePlanner | Changesets.DepsRegen | Repos.ReposManager | Repos.ReposConfigStore, WorkspaceDiscoveryError>;
11
+ readonly runtime: ManagedRuntime.ManagedRuntime<McpServices, never>;
9
12
  readonly cwd: string;
10
13
  }
11
14
  //#endregion
12
15
  //#region src/runtime.d.ts
13
16
  /**
14
- * The MCP runtime layer. Provides `SilkWorkspaceAnalyzer`, `WorkspaceRoot`,
15
- * `Turbo.TurboInspector`, `Changesets.BranchAnalyzer`,
16
- * `Changesets.ConfigInspector`, `Changesets.ReleasePlanner`,
17
- * `Changesets.DepsRegen`, `Repos.ReposManager`, and `Repos.ReposConfigStore`;
18
- * requires `CommandExecutor` + `FileSystem` + `Path` from the host's platform
19
- * layer (`NodeContext.layer` in bin.ts).
17
+ * Build the MCP runtime layer for a workspace root. Provides
18
+ * `SilkWorkspaceAnalyzer`, `WorkspaceRoot`, `Turbo.TurboInspector`,
19
+ * `Changesets.BranchAnalyzer`, `Changesets.ConfigInspector`,
20
+ * `Changesets.ReleasePlanner`, `Changesets.DepsRegen`, `Repos.ReposManager`,
21
+ * and `Repos.ReposConfigStore`; requires `ChildProcessSpawner` + `FileSystem`
22
+ * + `Path` from the host's platform layer (`NodeServices.layer` in bin.ts).
20
23
  *
21
- * `TurboInspectorLive` is fed its own `ToolDiscoveryLive`, whose
22
- * `PackageManagerDetector` + `WorkspaceRoot` requirements are satisfied by
23
- * {@link DepsLive}; the leftover `CommandExecutor` + `FileSystem` flow up to the
24
- * host platform layer.
24
+ * @remarks
25
+ * The kit graph (`Workspaces.layerWithGit`) mints a fresh layer reference per
26
+ * call, so it is bound to a `const` and provided ONCE via `Layer.provideMerge`
27
+ * layer memoization by reference then constructs each kit service exactly
28
+ * once and exposes `WorkspaceRoot` on the runtime. The same discipline gives
29
+ * the whole runtime a SINGLE `ConfigInspector` (the MCP server holds one for
30
+ * its whole process lifetime, #229 — `changeset_inspect` refreshes it before
31
+ * every call, and `BranchAnalyzer`, `ReleasePlanner`, and `DepsRegen` all read
32
+ * through that shared instance). `DepsRegen` is gated by silk's adaptive
33
+ * publishability detector — provided closer than the kit graph, so it wins
34
+ * over the kit's npm-semantics default — mirroring
35
+ * `Changesets.makeDepsRegenDefault`'s composition ("versionable minus
36
+ * ignored", identical to the savvy CLI).
25
37
  */
26
- declare const SilkRuntimeLive: Layer.Layer<Changesets.BranchAnalyzer | Changesets.ConfigInspector | Changesets.DepsRegen | Changesets.ReleasePlanner | Repos.ReposConfigStore | Repos.ReposManager | import("@savvy-web/silk-effects").SilkWorkspaceAnalyzer | Turbo.TurboInspector | import("workspaces-effect").WorkspaceRoot, import("workspaces-effect").WorkspaceDiscoveryError, import("@effect/platform/CommandExecutor").CommandExecutor | import("@effect/platform/FileSystem").FileSystem | import("@effect/platform/Path").Path>;
38
+ declare const makeSilkRuntimeLayer: (cwd: string) => Layer.Layer<McpServices, never, FileSystem.FileSystem | Path.Path | ChildProcessSpawner.ChildProcessSpawner>;
27
39
  //#endregion
28
40
  //#region src/server.d.ts
29
41
  /** Build the server and connect it over stdio. */
@@ -39,5 +51,5 @@ declare function startMcpServer(ctx: McpContext): Promise<void>;
39
51
  */
40
52
  declare const CURRENT_MCP_VERSION = "0.0.0";
41
53
  //#endregion
42
- export { CURRENT_MCP_VERSION, type McpContext, SilkRuntimeLive, startMcpServer };
54
+ export { CURRENT_MCP_VERSION, type McpContext, type McpServices, makeSilkRuntimeLayer, startMcpServer };
43
55
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { SilkRuntimeLive } from "./runtime.js";
1
+ import { makeSilkRuntimeLayer } from "./runtime.js";
2
2
  import { CURRENT_MCP_VERSION } from "./version.js";
3
3
  import { startMcpServer } from "./server.js";
4
4
 
5
- export { CURRENT_MCP_VERSION, SilkRuntimeLive, startMcpServer };
5
+ export { CURRENT_MCP_VERSION, makeSilkRuntimeLayer, startMcpServer };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savvy-web/mcp",
3
- "version": "1.8.1",
3
+ "version": "2.0.0",
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",
@@ -22,7 +22,8 @@
22
22
  "exports": {
23
23
  ".": {
24
24
  "types": "./index.d.ts",
25
- "import": "./index.js"
25
+ "import": "./index.js",
26
+ "default": "./index.js"
26
27
  },
27
28
  "./package.json": "./package.json"
28
29
  },
@@ -30,17 +31,11 @@
30
31
  "savvy-mcp": "bin/savvy-mcp.js"
31
32
  },
32
33
  "dependencies": {
33
- "@effect/cluster": "^0.59.0",
34
- "@effect/experimental": "^0.60.0",
35
- "@effect/platform": "^0.96.2",
36
- "@effect/platform-node": "^0.107.0",
37
- "@effect/rpc": "^0.75.1",
38
- "@effect/sql": "^0.51.1",
39
- "@effect/workflow": "^0.18.2",
34
+ "@effect/platform-node": "4.0.0-beta.98",
35
+ "@effected/workspaces": "^0.3.0",
40
36
  "@modelcontextprotocol/sdk": "^1.29.0",
41
- "@savvy-web/silk-effects": "3.3.1",
42
- "effect": "^3.21.4",
43
- "workspaces-effect": "^2.1.0",
37
+ "@savvy-web/silk-effects": "4.0.0",
38
+ "effect": "4.0.0-beta.98",
44
39
  "zod": "^4.4.3"
45
40
  }
46
41
  }
package/runtime.js CHANGED
@@ -1,71 +1,62 @@
1
- import { ChangesetConfigLive, ChangesetConfigReaderLive, Changesets, Repos, SilkWorkspaceAnalyzerLive, TagStrategyLive, ToolDiscoveryLive, Turbo, VersioningStrategyLive } from "@savvy-web/silk-effects";
1
+ import { Workspaces } from "@effected/workspaces";
2
+ import { ChangesetConfigLive, ChangesetConfigReaderLive, Changesets, PublishabilityDetectorAdaptiveLive, Repos, SilkWorkspaceAnalyzerLive, TagStrategyLive, ToolDiscoveryLive, Turbo, VersioningStrategyLive } from "@savvy-web/silk-effects";
2
3
  import { Layer } from "effect";
3
- import { PointInTimeWorkspaceLive, WorkspaceRootLive, WorkspacesLive } from "workspaces-effect";
4
4
 
5
5
  //#region src/runtime.ts
6
6
  /**
7
7
  * Composes the long-lived Effect runtime layer for the MCP server.
8
8
  *
9
- * `SilkRuntimeLive` provides {@link SilkWorkspaceAnalyzer} and {@link WorkspaceRoot}.
10
- * `WorkspaceRoot` lets the server resolve the workspace root by walking up from
11
- * its launch directory. The layer still requires `FileSystem` + `Path`; the host
12
- * (bin.ts) supplies them via `NodeContext.layer`.
9
+ * {@link makeSilkRuntimeLayer} builds the full service graph for ONE workspace
10
+ * root: the `@effected/workspaces` kit layers are root-bound at layer build
11
+ * (single-root by design), so the server resolves its project directory once
12
+ * at startup (bin.ts) and builds the layer with that root. The layer still
13
+ * requires the platform services (`FileSystem` + `Path` +
14
+ * `ChildProcessSpawner`); the host supplies them via `NodeServices.layer`.
13
15
  *
14
16
  * @packageDocumentation
15
17
  */
16
18
  /**
17
- * The silk-effects dependency set fed to {@link SilkWorkspaceAnalyzerLive}.
19
+ * Build the MCP runtime layer for a workspace root. Provides
20
+ * `SilkWorkspaceAnalyzer`, `WorkspaceRoot`, `Turbo.TurboInspector`,
21
+ * `Changesets.BranchAnalyzer`, `Changesets.ConfigInspector`,
22
+ * `Changesets.ReleasePlanner`, `Changesets.DepsRegen`, `Repos.ReposManager`,
23
+ * and `Repos.ReposConfigStore`; requires `ChildProcessSpawner` + `FileSystem`
24
+ * + `Path` from the host's platform layer (`NodeServices.layer` in bin.ts).
18
25
  *
19
- * `WorkspacesLive` supplies the workspace trio plus `DependencyGraph` and
20
- * `TopologicalSorter` (all required by the analyzer). `VersioningStrategyLive`
21
- * is provided its own `ChangesetConfigReader` because `Layer.mergeAll` does not
22
- * cross-feed sibling layers.
26
+ * @remarks
27
+ * The kit graph (`Workspaces.layerWithGit`) mints a fresh layer reference per
28
+ * call, so it is bound to a `const` and provided ONCE via `Layer.provideMerge`
29
+ * layer memoization by reference then constructs each kit service exactly
30
+ * once and exposes `WorkspaceRoot` on the runtime. The same discipline gives
31
+ * the whole runtime a SINGLE `ConfigInspector` (the MCP server holds one for
32
+ * its whole process lifetime, #229 — `changeset_inspect` refreshes it before
33
+ * every call, and `BranchAnalyzer`, `ReleasePlanner`, and `DepsRegen` all read
34
+ * through that shared instance). `DepsRegen` is gated by silk's adaptive
35
+ * publishability detector — provided closer than the kit graph, so it wins
36
+ * over the kit's npm-semantics default — mirroring
37
+ * `Changesets.makeDepsRegenDefault`'s composition ("versionable minus
38
+ * ignored", identical to the savvy CLI).
23
39
  */
24
- const DepsLive = Layer.mergeAll(WorkspacesLive, ChangesetConfigReaderLive, TagStrategyLive, VersioningStrategyLive.pipe(Layer.provide(ChangesetConfigReaderLive)));
25
- const InspectorAndAnalyzerLive = Changesets.BranchAnalyzerLive.pipe(Layer.provideMerge(Changesets.ReleasePlannerLive), Layer.provideMerge(Changesets.ConfigInspectorLive));
26
- /**
27
- * `Changesets.DepsRegen` (the `changeset_deps_detect` / `changeset_deps_regen`
28
- * orchestration service), fully composed except for the services supplied by
29
- * {@link DepsLive} / the host platform layer.
30
- *
31
- * `DepsRegenLive` requires `PointInTimeWorkspace | ConfigInspector |
32
- * WorkspaceDiscovery | PublishabilityDetector | ChangesetConfig`. Here
33
- * `ConfigInspector` is provided via the shared {@link InspectorAndAnalyzerLive}
34
- * reference (so Effect memoizes the single `ConfigInspector` instance already
35
- * merged into the runtime), and `PointInTimeWorkspace` via
36
- * `PointInTimeWorkspaceLive` (whose `WorkspaceRoot`/`WorkspaceDiscovery` come
37
- * from `WorkspacesLive` inside {@link DepsLive}). `ChangesetConfig` is provided
38
- * its own `ChangesetConfigReaderLive` (already present in {@link DepsLive}, but
39
- * `Layer.mergeAll` does not cross-feed sibling layers). The remaining two —
40
- * `WorkspaceDiscovery`, `PublishabilityDetector` — are left open and satisfied
41
- * by `WorkspacesLive` inside {@link DepsLive}. `FileSystem` / `Path` /
42
- * `CommandExecutor` flow up to the host `NodeContext.layer`.
43
- */
44
- const DepsRegenGroupLive = Changesets.DepsRegenLive.pipe(Layer.provide(InspectorAndAnalyzerLive), Layer.provide(PointInTimeWorkspaceLive), Layer.provide(ChangesetConfigLive.pipe(Layer.provide(ChangesetConfigReaderLive))));
45
- /**
46
- * `Repos.ReposManager` + `Repos.ReposConfigStore`, both exposed on the
47
- * runtime. `ReposManagerLive` requires `ReposConfigStore`, so it is given its
48
- * own `ReposConfigStoreLive` reference here (`Layer.mergeAll` does not
49
- * cross-feed sibling layers); `ReposConfigStore` is ALSO merged in directly so
50
- * `repos_inspect`'s config mode can resolve it on its own. The remaining
51
- * `CommandExecutor` + `FileSystem` + `Path` requirements flow up to the host
52
- * platform layer (`NodeContext.layer` in bin.ts).
53
- */
54
- const ReposGroupLive = Layer.mergeAll(Repos.ReposConfigStoreLive, Repos.ReposManagerLive.pipe(Layer.provide(Repos.ReposConfigStoreLive)));
55
- /**
56
- * The MCP runtime layer. Provides `SilkWorkspaceAnalyzer`, `WorkspaceRoot`,
57
- * `Turbo.TurboInspector`, `Changesets.BranchAnalyzer`,
58
- * `Changesets.ConfigInspector`, `Changesets.ReleasePlanner`,
59
- * `Changesets.DepsRegen`, `Repos.ReposManager`, and `Repos.ReposConfigStore`;
60
- * requires `CommandExecutor` + `FileSystem` + `Path` from the host's platform
61
- * layer (`NodeContext.layer` in bin.ts).
62
- *
63
- * `TurboInspectorLive` is fed its own `ToolDiscoveryLive`, whose
64
- * `PackageManagerDetector` + `WorkspaceRoot` requirements are satisfied by
65
- * {@link DepsLive}; the leftover `CommandExecutor` + `FileSystem` flow up to the
66
- * host platform layer.
67
- */
68
- const SilkRuntimeLive = Layer.mergeAll(SilkWorkspaceAnalyzerLive, WorkspaceRootLive, Turbo.TurboInspectorLive.pipe(Layer.provide(ToolDiscoveryLive)), InspectorAndAnalyzerLive, DepsRegenGroupLive, ReposGroupLive).pipe(Layer.provide(DepsLive));
40
+ const makeSilkRuntimeLayer = (cwd) => {
41
+ const kitGraph = Workspaces.layerWithGit({ cwd });
42
+ const analyzerDeps = Layer.mergeAll(ChangesetConfigReaderLive, TagStrategyLive, VersioningStrategyLive.pipe(Layer.provide(ChangesetConfigReaderLive)));
43
+ /**
44
+ * `BranchAnalyzer` + `ReleasePlanner` + the ONE shared `ConfigInspector`,
45
+ * merged so all three land on the runtime and every internal consumer
46
+ * memoizes onto the same inspector reference.
47
+ */
48
+ const inspectorAndAnalyzer = Changesets.BranchAnalyzerLive.pipe(Layer.provideMerge(Changesets.ReleasePlannerLive), Layer.provideMerge(Changesets.ConfigInspectorLive.pipe(Layer.provide(ChangesetConfigReaderLive))));
49
+ const changesetConfig = ChangesetConfigLive.pipe(Layer.provide(ChangesetConfigReaderLive));
50
+ const depsRegen = Changesets.DepsRegenLive.pipe(Layer.provide(inspectorAndAnalyzer), Layer.provide(PublishabilityDetectorAdaptiveLive.pipe(Layer.provide(changesetConfig))), Layer.provide(changesetConfig));
51
+ /**
52
+ * `ReposManagerLive` requires `ReposConfigStore`, so it is given its own
53
+ * `ReposConfigStoreLive` reference; the store is ALSO merged in directly so
54
+ * `repos_inspect`'s config mode can resolve it on its own. Same reference —
55
+ * one store instance.
56
+ */
57
+ const repos = Layer.mergeAll(Repos.ReposConfigStoreLive, Repos.ReposManagerLive.pipe(Layer.provide(Repos.ReposConfigStoreLive)));
58
+ return Layer.mergeAll(SilkWorkspaceAnalyzerLive.pipe(Layer.provide(analyzerDeps)), Turbo.TurboInspectorLive.pipe(Layer.provide(ToolDiscoveryLive)), inspectorAndAnalyzer, depsRegen, repos).pipe(Layer.provideMerge(kitGraph));
59
+ };
69
60
 
70
61
  //#endregion
71
- export { SilkRuntimeLive };
62
+ export { makeSilkRuntimeLayer };
@@ -1,54 +1,205 @@
1
- import { JSONSchema } from "effect";
1
+ import { Schema } from "effect";
2
2
  import { z } from "zod";
3
3
 
4
4
  //#region src/schema/effect-to-zod.ts
5
5
  /**
6
- * Convert an Effect `Schema.Schema<A, I, never>` to a zod schema:
7
- * `JSONSchema.make`, then inline every `#/$defs/*` `$ref`, then `z.fromJSONSchema`.
6
+ * Bridge an Effect Schema to a zod schema by routing through JSON Schema, so a
7
+ * tool keeps Effect Schema as the canonical source of truth while the MCP SDK
8
+ * receives the zod instance its `registerTool` API requires.
9
+ *
10
+ * @packageDocumentation
11
+ */
12
+ /**
13
+ * Convert an Effect `Schema.Codec<A, I>` to a zod schema:
14
+ * `Schema.toJsonSchemaDocument`, then inline every `#/$defs/*` `$ref` and
15
+ * normalize back to the wire contract, then `z.fromJSONSchema`.
8
16
  *
9
17
  * @remarks
10
18
  * - Effect-only refinements (custom predicates, brands) erase during the
11
19
  * round-trip; declare zod directly if boundary enforcement matters.
12
20
  * - The source schema must not use `Schema.suspend` (recursive `$ref`s would
13
- * make `inlineAllRefs` non-terminating). MCP tool output schemas in this
21
+ * make the inlining pass non-terminating). MCP tool output schemas in this
14
22
  * package are non-recursive projections by construction.
15
23
  * - The MCP SDK normalises `outputSchema` to an object; non-object results
16
24
  * (e.g. a bare union) are wrapped in a permissive object so the SDK accepts
17
25
  * them.
18
26
  */
19
27
  const effectToZodSchema = (schema) => {
20
- const jsonSchema = JSONSchema.make(schema);
21
- const inlined = inlineAllRefs(jsonSchema);
28
+ const inlined = effectSchemaToInlinedJsonSchema(schema);
22
29
  const zodSchema = z.fromJSONSchema(inlined);
23
30
  if (isObjectLike(zodSchema)) return zodSchema;
24
31
  return z.object({}).catchall(z.unknown());
25
32
  };
26
33
  const isObjectLike = (schema) => schema instanceof z.ZodObject;
34
+ /**
35
+ * Produce the inlined, normalized JSON Schema object handed to
36
+ * `z.fromJSONSchema`. Exposed separately so schema-snapshot tooling can
37
+ * serialize exactly what the bridge feeds zod.
38
+ *
39
+ * @remarks
40
+ * v4's `Schema.toJsonSchemaDocument` returns `{ dialect, schema, definitions }`
41
+ * with every identifier-annotated subschema hoisted into `definitions` and
42
+ * referenced as `#/$defs/<name>` — including the root itself. Two v4 encoding
43
+ * changes are normalized back to the v3-era wire contract here (in the bridge,
44
+ * never in the public schemas):
45
+ *
46
+ * - `Schema.Number` now encodes non-finite values as strings, emitting
47
+ * `anyOf: [number, "NaN", "Infinity", "-Infinity"]`. Tool results never
48
+ * carry non-finite numbers, so this collapses to `{ type: "number" }`.
49
+ * - `Schema.optional(S)` now admits `undefined`, emitting
50
+ * `anyOf: [S, { type: "null" }]` on the (already non-required) key. The
51
+ * handlers build results with conditional spreads and never emit
52
+ * `undefined`/`null` for optional keys, so the null arm is dropped.
53
+ * Deliberate `Schema.NullOr` fields are all on required keys and keep
54
+ * their null arm.
55
+ * - Filter checks (`isMinLength`, `isPattern`, …) now emit as
56
+ * `allOf: [{ minLength: 1 }]` instead of inline keywords. Bare-constraint
57
+ * `allOf` members are folded back into the parent node.
58
+ */
59
+ const effectSchemaToInlinedJsonSchema = (schema) => {
60
+ const doc = Schema.toJsonSchemaDocument(schema);
61
+ return inlineAllRefs(doc.schema, doc.definitions);
62
+ };
27
63
  const REF_PREFIX = "#/$defs/";
64
+ /** A JSON-schema node matching `{ "type": "null" }` exactly. */
65
+ const isNullSchema = (value) => {
66
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
67
+ const obj = value;
68
+ return obj.type === "null" && Object.keys(obj).length === 1;
69
+ };
70
+ /** A JSON-schema node matching `{ "type": "string", "enum": ["NaN" | "Infinity" | "-Infinity"] }`. */
71
+ const isNonFiniteArm = (value) => {
72
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
73
+ const obj = value;
74
+ if (obj.type !== "string" || !Array.isArray(obj.enum) || obj.enum.length !== 1) return false;
75
+ const literal = obj.enum[0];
76
+ return literal === "NaN" || literal === "Infinity" || literal === "-Infinity";
77
+ };
78
+ /** A JSON-schema node matching `{ "type": "number" }` exactly. */
79
+ const isPlainNumberArm = (value) => {
80
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
81
+ const obj = value;
82
+ return obj.type === "number" && Object.keys(obj).length === 1;
83
+ };
84
+ /** JSON-schema keywords that are pure value constraints (no structural meaning). */
85
+ const CONSTRAINT_KEYS = /* @__PURE__ */ new Set([
86
+ "minLength",
87
+ "maxLength",
88
+ "pattern",
89
+ "minimum",
90
+ "maximum",
91
+ "exclusiveMinimum",
92
+ "exclusiveMaximum",
93
+ "multipleOf",
94
+ "minItems",
95
+ "maxItems",
96
+ "format"
97
+ ]);
98
+ /** A node whose every key is a pure value-constraint keyword. */
99
+ const isBareConstraint = (value) => {
100
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
101
+ const keys = Object.keys(value);
102
+ return keys.length > 0 && keys.every((k) => CONSTRAINT_KEYS.has(k));
103
+ };
104
+ /**
105
+ * Fold `allOf` members that are bare constraint objects — v4's encoding of
106
+ * filter checks — back into the parent node (v3 inlined them). Keys already
107
+ * present on the parent are left inside `allOf` untouched.
108
+ */
109
+ const flattenConstraintAllOf = (node) => {
110
+ const allOf = node.allOf;
111
+ if (!Array.isArray(allOf)) return node;
112
+ const remaining = [];
113
+ const folded = {};
114
+ for (const member of allOf) {
115
+ if (!isBareConstraint(member) || Object.keys(member).some((k) => k in node || k in folded)) {
116
+ remaining.push(member);
117
+ continue;
118
+ }
119
+ Object.assign(folded, member);
120
+ }
121
+ if (Object.keys(folded).length === 0) return node;
122
+ const { allOf: _dropped, ...rest } = node;
123
+ return remaining.length > 0 ? {
124
+ ...rest,
125
+ ...folded,
126
+ allOf: remaining
127
+ } : {
128
+ ...rest,
129
+ ...folded
130
+ };
131
+ };
132
+ /**
133
+ * Collapse v4's non-finite number encoding — `anyOf: [{ type: "number" },
134
+ * "NaN", "Infinity", "-Infinity"]` — to `{ type: "number" }`, keeping any
135
+ * sibling annotations (description, title) on the node.
136
+ */
137
+ const collapseNonFiniteNumber = (node) => {
138
+ const anyOf = node.anyOf;
139
+ if (!Array.isArray(anyOf) || anyOf.length !== 4) return node;
140
+ const numberArms = anyOf.filter(isPlainNumberArm);
141
+ const nonFiniteArms = anyOf.filter(isNonFiniteArm);
142
+ if (numberArms.length !== 1 || nonFiniteArms.length !== 3) return node;
143
+ const { anyOf: _dropped, ...siblings } = node;
144
+ return {
145
+ type: "number",
146
+ ...siblings
147
+ };
148
+ };
149
+ /**
150
+ * Drop the `{ type: "null" }` arm that v4's `Schema.optional` adds for its
151
+ * `undefined` case. Applied only to non-required properties, so deliberate
152
+ * `Schema.NullOr` fields (all on required keys) keep their null arm.
153
+ */
154
+ const dropUndefinedArm = (node) => {
155
+ if (node === null || typeof node !== "object" || Array.isArray(node)) return node;
156
+ const obj = node;
157
+ if (!Array.isArray(obj.anyOf)) return obj;
158
+ const arms = obj.anyOf.filter((arm) => !isNullSchema(arm));
159
+ if (arms.length === obj.anyOf.length) return obj;
160
+ const { anyOf: _dropped, ...siblings } = obj;
161
+ if (arms.length === 1 && arms[0] !== null && typeof arms[0] === "object" && !Array.isArray(arms[0])) return collapseNonFiniteNumber({
162
+ ...arms[0],
163
+ ...siblings
164
+ });
165
+ return collapseNonFiniteNumber({
166
+ anyOf: arms,
167
+ ...siblings
168
+ });
169
+ };
28
170
  /**
29
- * Replace every `$ref: "#/$defs/X"` node with the contents of `$defs.X`,
30
- * recursively, and drop the `$defs` table. Assumes acyclic refs.
171
+ * Replace every `$ref: "#/$defs/X"` node with the contents of
172
+ * `definitions.X`, recursively, and normalize the v4 encoding deltas back to
173
+ * the wire contract. Assumes acyclic refs.
31
174
  */
32
- const inlineAllRefs = (root) => {
33
- const defs = root.$defs ?? {};
175
+ const inlineAllRefs = (root, defs) => {
34
176
  const visit = (value) => {
35
177
  if (Array.isArray(value)) return value.map(visit);
36
178
  if (value === null || typeof value !== "object") return value;
37
179
  const obj = value;
38
180
  if (typeof obj.$ref === "string" && obj.$ref.startsWith(REF_PREFIX)) {
39
- const defName = obj.$ref.slice(8);
40
- const target = defs[defName];
181
+ const target = defs[obj.$ref.slice(8)];
41
182
  if (target !== void 0) return visit(target);
42
183
  }
184
+ const required = Array.isArray(obj.required) ? obj.required : [];
43
185
  const out = {};
44
186
  for (const [k, v] of Object.entries(obj)) {
45
187
  if (k === "$defs") continue;
188
+ if (k === "properties" && v !== null && typeof v === "object" && !Array.isArray(v)) {
189
+ const props = {};
190
+ for (const [propKey, propValue] of Object.entries(v)) {
191
+ const visited = visit(propValue);
192
+ props[propKey] = required.includes(propKey) ? visited : dropUndefinedArm(visited);
193
+ }
194
+ out[k] = props;
195
+ continue;
196
+ }
46
197
  out[k] = visit(v);
47
198
  }
48
- return out;
199
+ return collapseNonFiniteNumber(flattenConstraintAllOf(out));
49
200
  };
50
201
  return visit(root);
51
202
  };
52
203
 
53
204
  //#endregion
54
- export { effectToZodSchema };
205
+ export { effectSchemaToInlinedJsonSchema, effectToZodSchema };
package/server.js CHANGED
@@ -43,7 +43,7 @@ function buildServer(ctx) {
43
43
  }, async (args) => {
44
44
  const root = args.cwd ?? ctx.cwd;
45
45
  const data = await ctx.runtime.runPromise(workspaceInfo(root));
46
- const text = Schema.decodeSync(WorkspaceInfoAsMarkdown)(data);
46
+ const text = Schema.decodeUnknownSync(WorkspaceInfoAsMarkdown)(data);
47
47
  return structuredResult(text, data);
48
48
  });
49
49
  server.registerTool("turbo_inspect", {
@@ -62,7 +62,7 @@ function buildServer(ctx) {
62
62
  annotations: { readOnlyHint: true }
63
63
  }, async (args) => {
64
64
  const data = await ctx.runtime.runPromise(turboInspect(args, ctx.cwd));
65
- const text = Schema.decodeSync(TurboInspectAsMarkdown)(data);
65
+ const text = Schema.decodeUnknownSync(TurboInspectAsMarkdown)(data);
66
66
  return structuredResult(text, data);
67
67
  });
68
68
  server.registerTool("changeset_inspect", {
@@ -81,7 +81,7 @@ function buildServer(ctx) {
81
81
  annotations: { readOnlyHint: true }
82
82
  }, async (args) => {
83
83
  const data = await ctx.runtime.runPromise(changesetInspect(args, ctx.cwd));
84
- const text = Schema.decodeSync(ChangesetInspectAsMarkdown)(data);
84
+ const text = Schema.decodeUnknownSync(ChangesetInspectAsMarkdown)(data);
85
85
  return structuredResult(text, data);
86
86
  });
87
87
  server.registerTool("changeset_validate", {
@@ -94,7 +94,7 @@ function buildServer(ctx) {
94
94
  annotations: { readOnlyHint: true }
95
95
  }, async (args) => {
96
96
  const data = await ctx.runtime.runPromise(changesetValidate(args, ctx.cwd));
97
- const text = Schema.decodeSync(ChangesetValidateAsMarkdown)(data);
97
+ const text = Schema.decodeUnknownSync(ChangesetValidateAsMarkdown)(data);
98
98
  return structuredResult(text, data);
99
99
  });
100
100
  server.registerTool("changeset_deps_detect", {
@@ -110,7 +110,7 @@ function buildServer(ctx) {
110
110
  annotations: { readOnlyHint: true }
111
111
  }, async (args) => {
112
112
  const data = await ctx.runtime.runPromise(changesetDepsDetect(args, ctx.cwd));
113
- const text = Schema.decodeSync(ChangesetDepsDetectAsMarkdown)(data);
113
+ const text = Schema.decodeUnknownSync(ChangesetDepsDetectAsMarkdown)(data);
114
114
  return structuredResult(text, data);
115
115
  });
116
116
  server.registerTool("changeset_preview", {
@@ -120,7 +120,7 @@ function buildServer(ctx) {
120
120
  annotations: { readOnlyHint: true }
121
121
  }, async (args) => {
122
122
  const data = await ctx.runtime.runPromise(changesetPreview(args, ctx.cwd));
123
- const text = Schema.decodeSync(ChangesetPreviewAsMarkdown)(data);
123
+ const text = Schema.decodeUnknownSync(ChangesetPreviewAsMarkdown)(data);
124
124
  return structuredResult(text, data);
125
125
  });
126
126
  server.registerTool("changeset_deps_regen", {
@@ -140,7 +140,7 @@ function buildServer(ctx) {
140
140
  }
141
141
  }, async (args) => {
142
142
  const data = await ctx.runtime.runPromise(changesetDepsRegen(args, ctx.cwd));
143
- const text = Schema.decodeSync(ChangesetDepsRegenAsMarkdown)(data);
143
+ const text = Schema.decodeUnknownSync(ChangesetDepsRegenAsMarkdown)(data);
144
144
  return structuredResult(text, data);
145
145
  });
146
146
  server.registerTool("repos_inspect", {
@@ -154,7 +154,7 @@ function buildServer(ctx) {
154
154
  annotations: { readOnlyHint: true }
155
155
  }, async (args) => {
156
156
  const data = await ctx.runtime.runPromise(reposInspect(args, ctx.cwd));
157
- const text = Schema.decodeSync(ReposInspectAsMarkdown)(data);
157
+ const text = Schema.decodeUnknownSync(ReposInspectAsMarkdown)(data);
158
158
  return structuredResult(text, data);
159
159
  });
160
160
  server.registerTool("repos_manage", {
@@ -189,7 +189,7 @@ function buildServer(ctx) {
189
189
  }
190
190
  }, async (args) => {
191
191
  const data = await ctx.runtime.runPromise(reposManage(args, ctx.cwd));
192
- const text = Schema.decodeSync(ReposManageAsMarkdown)(data);
192
+ const text = Schema.decodeUnknownSync(ReposManageAsMarkdown)(data);
193
193
  return structuredResult(text, data);
194
194
  });
195
195
  server.registerTool("biome_check", {
@@ -205,7 +205,7 @@ function buildServer(ctx) {
205
205
  outputSchema: effectToZodSchema(BiomeCheckResult)
206
206
  }, async (args) => {
207
207
  const data = await runBiomeCheck(args, ctx.cwd);
208
- const text = Schema.decodeSync(BiomeCheckAsMarkdown)(data);
208
+ const text = Schema.decodeUnknownSync(BiomeCheckAsMarkdown)(data);
209
209
  return structuredResult(text, data);
210
210
  });
211
211
  return server;
@@ -1,5 +1,5 @@
1
1
  import { Lint } from "@savvy-web/silk-effects";
2
- import { ParseResult, Schema } from "effect";
2
+ import { Schema, SchemaGetter } from "effect";
3
3
  import { spawnSync } from "node:child_process";
4
4
  import { realpathSync } from "node:fs";
5
5
  import { relative, resolve, sep } from "node:path";
@@ -14,7 +14,11 @@ import { relative, resolve, sep } from "node:path";
14
14
  * @packageDocumentation
15
15
  */
16
16
  /** Normalized diagnostic severity. */
17
- const BiomeSeverity = Schema.Literal("error", "warning", "info");
17
+ const BiomeSeverity = Schema.Literals([
18
+ "error",
19
+ "warning",
20
+ "info"
21
+ ]);
18
22
  /** A single normalized Biome diagnostic. */
19
23
  const BiomeDiagnostic = Schema.Struct({
20
24
  file: Schema.String,
@@ -24,7 +28,7 @@ const BiomeDiagnostic = Schema.Struct({
24
28
  message: Schema.String,
25
29
  /** Present only when `strict` upgraded this diagnostic; holds the project-configured severity. */
26
30
  originalSeverity: Schema.optional(BiomeSeverity)
27
- }).annotations({ identifier: "BiomeDiagnostic" });
31
+ }).annotate({ identifier: "BiomeDiagnostic" });
28
32
  /** The `biome_check` tool result. */
29
33
  const BiomeCheckResult = Schema.Struct({
30
34
  summary: Schema.Struct({
@@ -36,7 +40,7 @@ const BiomeCheckResult = Schema.Struct({
36
40
  diagnostics: Schema.Array(BiomeDiagnostic),
37
41
  wrote: Schema.Boolean,
38
42
  guidance: Schema.String
39
- }).annotations({
43
+ }).annotate({
40
44
  identifier: "BiomeCheckResult",
41
45
  title: "biome_check result",
42
46
  description: "Structured Biome diagnostics, with a flag for whether a --write pass ran."
@@ -51,7 +55,13 @@ const GUIDANCE_STRICT_NOTE = "Diagnostics marked with originalSeverity are proje
51
55
  const GitlabDiagnostic = Schema.Struct({
52
56
  description: Schema.String,
53
57
  check_name: Schema.String,
54
- severity: Schema.Literal("info", "minor", "major", "critical", "blocker"),
58
+ severity: Schema.Literals([
59
+ "info",
60
+ "minor",
61
+ "major",
62
+ "critical",
63
+ "blocker"
64
+ ]),
55
65
  location: Schema.Struct({
56
66
  path: Schema.String,
57
67
  lines: Schema.Struct({ begin: Schema.Number })
@@ -128,11 +138,10 @@ const renderMarkdown = (data) => {
128
138
  return lines.join("\n");
129
139
  };
130
140
  /** One-way transform: result to markdown. Encoding back is forbidden. */
131
- const BiomeCheckAsMarkdown = Schema.transformOrFail(BiomeCheckResult, Schema.String, {
132
- strict: true,
133
- decode: (data) => ParseResult.succeed(renderMarkdown(data)),
134
- encode: (text, _options, ast) => ParseResult.fail(new ParseResult.Forbidden(ast, text, "BiomeCheckAsMarkdown is one-way: markdown cannot be parsed back."))
135
- });
141
+ const BiomeCheckAsMarkdown = BiomeCheckResult.pipe(Schema.decodeTo(Schema.String, {
142
+ decode: SchemaGetter.transform(renderMarkdown),
143
+ encode: SchemaGetter.forbidden(() => "BiomeCheckAsMarkdown is one-way: markdown cannot be parsed back.")
144
+ }));
136
145
  /**
137
146
  * Run Biome and return structured diagnostics. When `write`/`unsafe` is set,
138
147
  * runs a fix pass first, then a read-only gitlab pass to report what remains.
@@ -1,29 +1,19 @@
1
+ import { WorkspaceRoot } from "@effected/workspaces";
1
2
  import { Changesets } from "@savvy-web/silk-effects";
2
- import { Effect, ParseResult, Schema } from "effect";
3
- import { WorkspaceRoot } from "workspaces-effect";
3
+ import { Effect, Schema, SchemaGetter } from "effect";
4
4
 
5
5
  //#region src/tools/changeset-deps-detect.ts
6
- /**
7
- * The `changeset_deps_detect` MCP tool: a read-only preview of the cumulative
8
- * dependency diff (merge-base → working tree) over silk-effects'
9
- * `Changesets.DepsRegen.plan`. Returns one entry per affected workspace package
10
- * — its resolved dependency-table rows (devDependencies retained) — plus a
11
- * one-way markdown transform. Read-only: no changeset file is written or
12
- * deleted.
13
- *
14
- * @packageDocumentation
15
- */
16
6
  /** One affected workspace package's resolved dependency diff. */
17
7
  const ChangesetDepsDetectPackage = Schema.Struct({
18
8
  package: Schema.String,
19
9
  relativePath: Schema.String,
20
10
  rows: Schema.Array(Changesets.DependencyTableRowSchema)
21
- }).annotations({ identifier: "ChangesetDepsDetectPackage" });
11
+ }).annotate({ identifier: "ChangesetDepsDetectPackage" });
22
12
  /** The `changeset_deps_detect` tool result. */
23
13
  const ChangesetDepsDetectResult = Schema.Struct({
24
14
  root: Schema.String,
25
15
  packages: Schema.Array(ChangesetDepsDetectPackage)
26
- }).annotations({
16
+ }).annotate({
27
17
  identifier: "ChangesetDepsDetectResult",
28
18
  title: "changeset_deps_detect result",
29
19
  description: "Read-only per-package dependency diff (devDependencies retained). No files are written."
@@ -56,11 +46,10 @@ const renderMarkdown = (data) => {
56
46
  return lines.join("\n").trimEnd();
57
47
  };
58
48
  /** One-way transform: result to markdown. Encoding back is forbidden. */
59
- const ChangesetDepsDetectAsMarkdown = Schema.transformOrFail(ChangesetDepsDetectResult, Schema.String, {
60
- strict: true,
61
- decode: (data) => ParseResult.succeed(renderMarkdown(data)),
62
- encode: (text, _options, ast) => ParseResult.fail(new ParseResult.Forbidden(ast, text, "ChangesetDepsDetectAsMarkdown is one-way: markdown cannot be parsed back."))
63
- });
49
+ const ChangesetDepsDetectAsMarkdown = ChangesetDepsDetectResult.pipe(Schema.decodeTo(Schema.String, {
50
+ decode: SchemaGetter.transform(renderMarkdown),
51
+ encode: SchemaGetter.forbidden(() => "ChangesetDepsDetectAsMarkdown is one-way: markdown cannot be parsed back.")
52
+ }));
64
53
  /**
65
54
  * Effect handler: resolve the workspace root, then compute the cumulative
66
55
  * dependency diff via {@link Changesets.DepsRegen.plan} with `includeDevDeps`
@@ -1,17 +1,8 @@
1
+ import { WorkspaceRoot } from "@effected/workspaces";
1
2
  import { Changesets } from "@savvy-web/silk-effects";
2
- import { Effect, ParseResult, Schema } from "effect";
3
- import { WorkspaceRoot } from "workspaces-effect";
3
+ import { Effect, Schema, SchemaGetter } from "effect";
4
4
 
5
5
  //#region src/tools/changeset-deps-regen.ts
6
- /**
7
- * The `changeset_deps_regen` MCP tool: delete stale pure-dependency changesets
8
- * and write fresh single-package, patch-bump changesets from the cumulative
9
- * dependency diff, over silk-effects' `Changesets.DepsRegen`. Mutating (writes
10
- * and deletes `.changeset/*.md`) unless `dryRun` is set. The second mutating
11
- * tool after `biome_check`; no `readOnlyHint`.
12
- *
13
- * @packageDocumentation
14
- */
15
6
  /** The `changeset_deps_regen` tool result. */
16
7
  const ChangesetDepsRegenResult = Schema.Struct({
17
8
  root: Schema.String,
@@ -19,7 +10,7 @@ const ChangesetDepsRegenResult = Schema.Struct({
19
10
  written: Schema.Array(Schema.String),
20
11
  skippedMixed: Schema.Array(Schema.String),
21
12
  dryRun: Schema.Boolean
22
- }).annotations({
13
+ }).annotate({
23
14
  identifier: "ChangesetDepsRegenResult",
24
15
  title: "changeset_deps_regen result",
25
16
  description: "Regenerated pure-dependency changesets. Mutates .changeset/*.md unless dryRun is set."
@@ -60,11 +51,10 @@ const renderMarkdown = (data) => {
60
51
  return lines.join("\n").trimEnd();
61
52
  };
62
53
  /** One-way transform: result to markdown. Encoding back is forbidden. */
63
- const ChangesetDepsRegenAsMarkdown = Schema.transformOrFail(ChangesetDepsRegenResult, Schema.String, {
64
- strict: true,
65
- decode: (data) => ParseResult.succeed(renderMarkdown(data)),
66
- encode: (text, _options, ast) => ParseResult.fail(new ParseResult.Forbidden(ast, text, "ChangesetDepsRegenAsMarkdown is one-way: markdown cannot be parsed back."))
67
- });
54
+ const ChangesetDepsRegenAsMarkdown = ChangesetDepsRegenResult.pipe(Schema.decodeTo(Schema.String, {
55
+ decode: SchemaGetter.transform(renderMarkdown),
56
+ encode: SchemaGetter.forbidden(() => "ChangesetDepsRegenAsMarkdown is one-way: markdown cannot be parsed back.")
57
+ }));
68
58
  /**
69
59
  * Effect handler: resolve the workspace root, compute a {@link Changesets.RegenPlan}
70
60
  * via {@link Changesets.DepsRegen.plan}, then — unless `dryRun` — apply it via
@@ -1,33 +1,29 @@
1
+ import { WorkspaceRoot } from "@effected/workspaces";
1
2
  import { Changesets } from "@savvy-web/silk-effects";
2
- import { Effect, ParseResult, Schema } from "effect";
3
- import { WorkspaceRoot } from "workspaces-effect";
3
+ import { Effect, Schema, SchemaGetter } from "effect";
4
4
 
5
5
  //#region src/tools/changeset-inspect.ts
6
- /**
7
- * The `changeset_inspect` MCP tool: a discriminated-union result keyed by `mode`
8
- * (branch | config), each variant embedding the corresponding resolved-output
9
- * schema from silk-effects' Changesets namespace, plus a one-way markdown
10
- * transform. Read-only.
11
- *
12
- * @packageDocumentation
13
- */
14
6
  /** Branch-analysis variant. */
15
7
  const ChangesetBranchResult = Schema.Struct({
16
8
  mode: Schema.Literal("branch"),
17
9
  result: Changesets.BranchAnalysisSchema
18
- }).annotations({ identifier: "ChangesetBranchResult" });
10
+ }).annotate({ identifier: "ChangesetBranchResult" });
19
11
  /** Config-inspection variant. */
20
12
  const ChangesetConfigResult = Schema.Struct({
21
13
  mode: Schema.Literal("config"),
22
14
  result: Changesets.InspectedConfigSchema
23
- }).annotations({ identifier: "ChangesetConfigResult" });
15
+ }).annotate({ identifier: "ChangesetConfigResult" });
24
16
  /** Classify variant — arbitrary paths to owning package. */
25
17
  const ChangesetClassifyResult = Schema.Struct({
26
18
  mode: Schema.Literal("classify"),
27
19
  result: Schema.Array(Changesets.ClassificationSchema)
28
- }).annotations({ identifier: "ChangesetClassifyResult" });
20
+ }).annotate({ identifier: "ChangesetClassifyResult" });
29
21
  /** The `changeset_inspect` tool result — a discriminated union keyed by `mode`. */
30
- const ChangesetInspectResult = Schema.Union(ChangesetBranchResult, ChangesetConfigResult, ChangesetClassifyResult).annotations({
22
+ const ChangesetInspectResult = Schema.Union([
23
+ ChangesetBranchResult,
24
+ ChangesetConfigResult,
25
+ ChangesetClassifyResult
26
+ ]).annotate({
31
27
  identifier: "ChangesetInspectResult",
32
28
  title: "changeset_inspect result",
33
29
  description: "Read-only changeset analysis grouped by mode (branch | config | classify)."
@@ -95,11 +91,10 @@ const renderMarkdown = (data) => {
95
91
  }
96
92
  };
97
93
  /** One-way transform: result to markdown. Encoding back is forbidden. */
98
- const ChangesetInspectAsMarkdown = Schema.transformOrFail(ChangesetInspectResult, Schema.String, {
99
- strict: true,
100
- decode: (data) => ParseResult.succeed(renderMarkdown(data)),
101
- encode: (text, _options, ast) => ParseResult.fail(new ParseResult.Forbidden(ast, text, "ChangesetInspectAsMarkdown is one-way: markdown cannot be parsed back."))
102
- });
94
+ const ChangesetInspectAsMarkdown = ChangesetInspectResult.pipe(Schema.decodeTo(Schema.String, {
95
+ decode: SchemaGetter.transform(renderMarkdown),
96
+ encode: SchemaGetter.forbidden(() => "ChangesetInspectAsMarkdown is one-way: markdown cannot be parsed back.")
97
+ }));
103
98
  /**
104
99
  * Effect handler: resolve the workspace root, then dispatch to the matching
105
100
  * Changesets service keyed by `mode`. Mirrors `turboInspect`.
@@ -1,17 +1,10 @@
1
+ import { WorkspaceRoot } from "@effected/workspaces";
1
2
  import { Changesets } from "@savvy-web/silk-effects";
2
- import { Effect, ParseResult, Schema } from "effect";
3
- import { WorkspaceRoot } from "workspaces-effect";
3
+ import { Effect, Schema, SchemaGetter } from "effect";
4
4
 
5
5
  //#region src/tools/changeset-preview.ts
6
- /**
7
- * The `changeset_preview` MCP tool: a read-only preview of the next release's
8
- * CHANGELOG, produced by the genuine changesets engine via silk-effects'
9
- * ReleasePlanner. Structured result + one-way markdown transform. Read-only.
10
- *
11
- * @packageDocumentation
12
- */
13
6
  /** The `changeset_preview` result — the silk-effects preview shape. */
14
- const ChangesetPreviewResult = Changesets.ChangesetPreviewSchema.annotations({
7
+ const ChangesetPreviewResult = Changesets.ChangesetPreviewSchema.annotate({
15
8
  identifier: "ChangesetPreviewResult",
16
9
  title: "changeset_preview result",
17
10
  description: "Read-only preview of the next release: version bumps + rendered CHANGELOG blocks."
@@ -34,11 +27,10 @@ const renderMarkdown = (data) => {
34
27
  return lines.join("\n").trimEnd();
35
28
  };
36
29
  /** One-way transform: result to markdown. Encoding back is forbidden. */
37
- const ChangesetPreviewAsMarkdown = Schema.transformOrFail(ChangesetPreviewResult, Schema.String, {
38
- strict: true,
39
- decode: (data) => ParseResult.succeed(renderMarkdown(data)),
40
- encode: (text, _options, ast) => ParseResult.fail(new ParseResult.Forbidden(ast, text, "ChangesetPreviewAsMarkdown is one-way: markdown cannot be parsed back."))
41
- });
30
+ const ChangesetPreviewAsMarkdown = ChangesetPreviewResult.pipe(Schema.decodeTo(Schema.String, {
31
+ decode: SchemaGetter.transform(renderMarkdown),
32
+ encode: SchemaGetter.forbidden(() => "ChangesetPreviewAsMarkdown is one-way: markdown cannot be parsed back.")
33
+ }));
42
34
  /**
43
35
  * Effect handler: resolve the workspace root, then render the preview via
44
36
  * ReleasePlanner. Mirrors `changesetInspect`.
@@ -1,6 +1,6 @@
1
+ import { WorkspaceRoot } from "@effected/workspaces";
1
2
  import { Changesets } from "@savvy-web/silk-effects";
2
- import { Data, Effect, ParseResult, Schema } from "effect";
3
- import { WorkspaceRoot } from "workspaces-effect";
3
+ import { Data, Effect, Schema, SchemaGetter } from "effect";
4
4
  import { resolve } from "node:path";
5
5
 
6
6
  //#region src/tools/changeset-validate.ts
@@ -20,14 +20,14 @@ const ChangesetLintMessage = Schema.Struct({
20
20
  line: Schema.Number,
21
21
  column: Schema.Number,
22
22
  message: Schema.String
23
- }).annotations({ identifier: "ChangesetLintMessage" });
23
+ }).annotate({ identifier: "ChangesetLintMessage" });
24
24
  /** The `changeset_validate` tool result. */
25
25
  const ChangesetValidateResult = Schema.Struct({
26
26
  dir: Schema.String,
27
27
  ok: Schema.Boolean,
28
28
  errorCount: Schema.Number,
29
29
  messages: Schema.Array(ChangesetLintMessage)
30
- }).annotations({
30
+ }).annotate({
31
31
  identifier: "ChangesetValidateResult",
32
32
  title: "changeset_validate result",
33
33
  description: "Read-only validation of changeset files against the section-aware rules."
@@ -51,11 +51,10 @@ const renderMarkdown = (data) => {
51
51
  return lines.join("\n");
52
52
  };
53
53
  /** One-way transform: result to markdown. Encoding back is forbidden. */
54
- const ChangesetValidateAsMarkdown = Schema.transformOrFail(ChangesetValidateResult, Schema.String, {
55
- strict: true,
56
- decode: (data) => ParseResult.succeed(renderMarkdown(data)),
57
- encode: (text, _options, ast) => ParseResult.fail(new ParseResult.Forbidden(ast, text, "ChangesetValidateAsMarkdown is one-way: markdown cannot be parsed back."))
58
- });
54
+ const ChangesetValidateAsMarkdown = ChangesetValidateResult.pipe(Schema.decodeTo(Schema.String, {
55
+ decode: SchemaGetter.transform(renderMarkdown),
56
+ encode: SchemaGetter.forbidden(() => "ChangesetValidateAsMarkdown is one-way: markdown cannot be parsed back.")
57
+ }));
59
58
  /**
60
59
  * Effect handler: resolve the workspace root, then validate the changeset
61
60
  * directory via the pure {@link Changesets.ChangesetLinter.validate}. The
@@ -1,29 +1,21 @@
1
1
  import { mdInline } from "./md-inline.js";
2
+ import { WorkspaceRoot } from "@effected/workspaces";
2
3
  import { Repos } from "@savvy-web/silk-effects";
3
- import { Effect, ParseResult, Schema } from "effect";
4
- import { WorkspaceRoot } from "workspaces-effect";
4
+ import { Effect, Schema, SchemaGetter } from "effect";
5
5
 
6
6
  //#region src/tools/repos-inspect.ts
7
- /**
8
- * The `repos_inspect` MCP tool: a discriminated-union result keyed by `mode`
9
- * (status | config), each variant embedding the corresponding resolved-output
10
- * schema from silk-effects' Repos namespace, plus a one-way markdown
11
- * transform. Read-only.
12
- *
13
- * @packageDocumentation
14
- */
15
7
  /** Status-report variant. */
16
8
  const ReposStatusResult = Schema.Struct({
17
9
  mode: Schema.Literal("status"),
18
10
  result: Repos.ReposStatusReport
19
- }).annotations({ identifier: "ReposStatusResult" });
11
+ }).annotate({ identifier: "ReposStatusResult" });
20
12
  /** Manifest-config variant. */
21
13
  const ReposConfigResult = Schema.Struct({
22
14
  mode: Schema.Literal("config"),
23
15
  result: Repos.ReposManifestFile
24
- }).annotations({ identifier: "ReposConfigResult" });
16
+ }).annotate({ identifier: "ReposConfigResult" });
25
17
  /** The `repos_inspect` tool result — a discriminated union keyed by `mode`. */
26
- const ReposInspectResult = Schema.Union(ReposStatusResult, ReposConfigResult).annotations({
18
+ const ReposInspectResult = Schema.Union([ReposStatusResult, ReposConfigResult]).annotate({
27
19
  identifier: "ReposInspectResult",
28
20
  title: "repos_inspect result",
29
21
  description: "Drift report (status) or the parsed manifest with purposes, orientation, and notes (config)."
@@ -77,11 +69,10 @@ const renderMarkdown = (data) => {
77
69
  }
78
70
  };
79
71
  /** One-way transform: result to markdown. Encoding back is forbidden. */
80
- const ReposInspectAsMarkdown = Schema.transformOrFail(ReposInspectResult, Schema.String, {
81
- strict: true,
82
- decode: (data) => ParseResult.succeed(renderMarkdown(data)),
83
- encode: (text, _options, ast) => ParseResult.fail(new ParseResult.Forbidden(ast, text, "ReposInspectAsMarkdown is one-way: markdown cannot be parsed back."))
84
- });
72
+ const ReposInspectAsMarkdown = ReposInspectResult.pipe(Schema.decodeTo(Schema.String, {
73
+ decode: SchemaGetter.transform(renderMarkdown),
74
+ encode: SchemaGetter.forbidden(() => "ReposInspectAsMarkdown is one-way: markdown cannot be parsed back.")
75
+ }));
85
76
  /**
86
77
  * Effect handler: resolve the workspace root, then dispatch to the matching
87
78
  * Repos service keyed by `mode`. Mirrors `changesetInspect`.
@@ -1,18 +1,9 @@
1
1
  import { mdInline } from "./md-inline.js";
2
+ import { WorkspaceRoot } from "@effected/workspaces";
2
3
  import { Repos } from "@savvy-web/silk-effects";
3
- import { Effect, ParseResult, Schema } from "effect";
4
- import { WorkspaceRoot } from "workspaces-effect";
4
+ import { Effect, Schema, SchemaGetter } from "effect";
5
5
 
6
6
  //#region src/tools/repos-manage.ts
7
- /**
8
- * The `repos_manage` MCP tool: one action-discriminated mutating tool
9
- * covering `sync`, `pin`, `add`, and `note` against the vendored `.repos/`
10
- * submodules. The wire schema is flat (no `oneOf`); the handler maps it into
11
- * an internal `Schema.TaggedStruct` request union that names the missing
12
- * field per action on decode failure. Mutating — no `readOnlyHint`.
13
- *
14
- * @packageDocumentation
15
- */
16
7
  /** `sync` has no extra fields. */
17
8
  const SyncRequest = Schema.TaggedStruct("sync", {});
18
9
  /** `pin` requires both `name` and `ref`. */
@@ -35,40 +26,54 @@ const AddRequest = Schema.TaggedStruct("add", {
35
26
  */
36
27
  const NoteRequest = Schema.TaggedStruct("note", {
37
28
  name: Schema.String,
38
- op: Schema.Literal("add", "remove", "promote"),
29
+ op: Schema.Literals([
30
+ "add",
31
+ "remove",
32
+ "promote"
33
+ ]),
39
34
  note: Schema.optional(Schema.String),
40
35
  id: Schema.optional(Schema.String),
41
- into: Schema.optional(Schema.Literal("layout", "startHere"))
42
- }).pipe(Schema.filter((request) => {
36
+ into: Schema.optional(Schema.Literals(["layout", "startHere"]))
37
+ }).check(Schema.makeFilter((request) => {
43
38
  if (request.op === "add" && request.note === void 0) return "note op \"add\" requires `note`";
44
39
  if (request.op === "remove" && request.id === void 0) return "note op \"remove\" requires `id`";
45
40
  if (request.op === "promote" && (request.id === void 0 || request.into === void 0)) return "note op \"promote\" requires both `id` and `into`";
46
41
  return true;
47
42
  }));
48
43
  /** Internal tagged-union request the flat wire args decode into. */
49
- const ReposManageRequest = Schema.Union(SyncRequest, PinRequest, AddRequest, NoteRequest);
44
+ const ReposManageRequest = Schema.Union([
45
+ SyncRequest,
46
+ PinRequest,
47
+ AddRequest,
48
+ NoteRequest
49
+ ]);
50
50
  /** `sync` result variant. */
51
51
  const ReposManageSyncResult = Schema.Struct({
52
52
  action: Schema.Literal("sync"),
53
53
  result: Repos.ReposSyncReport
54
- }).annotations({ identifier: "ReposManageSyncResult" });
54
+ }).annotate({ identifier: "ReposManageSyncResult" });
55
55
  /** `pin` result variant. */
56
56
  const ReposManagePinResult = Schema.Struct({
57
57
  action: Schema.Literal("pin"),
58
58
  result: Repos.ReposPinResult
59
- }).annotations({ identifier: "ReposManagePinResult" });
59
+ }).annotate({ identifier: "ReposManagePinResult" });
60
60
  /** `add` result variant. */
61
61
  const ReposManageAddResult = Schema.Struct({
62
62
  action: Schema.Literal("add"),
63
63
  result: Repos.ReposAddResult
64
- }).annotations({ identifier: "ReposManageAddResult" });
64
+ }).annotate({ identifier: "ReposManageAddResult" });
65
65
  /** `note` result variant. */
66
66
  const ReposManageNoteResult = Schema.Struct({
67
67
  action: Schema.Literal("note"),
68
68
  result: Repos.ReposNoteResult
69
- }).annotations({ identifier: "ReposManageNoteResult" });
69
+ }).annotate({ identifier: "ReposManageNoteResult" });
70
70
  /** The `repos_manage` tool result — a discriminated union keyed by `action`. */
71
- const ReposManageResult = Schema.Union(ReposManageSyncResult, ReposManagePinResult, ReposManageAddResult, ReposManageNoteResult).annotations({
71
+ const ReposManageResult = Schema.Union([
72
+ ReposManageSyncResult,
73
+ ReposManagePinResult,
74
+ ReposManageAddResult,
75
+ ReposManageNoteResult
76
+ ]).annotate({
72
77
  identifier: "ReposManageResult",
73
78
  title: "repos_manage result",
74
79
  description: "Result of a mutating repos action: sync, pin, add, or note."
@@ -134,11 +139,10 @@ const renderMarkdown = (data) => {
134
139
  }
135
140
  };
136
141
  /** One-way transform: result to markdown. Encoding back is forbidden. */
137
- const ReposManageAsMarkdown = Schema.transformOrFail(ReposManageResult, Schema.String, {
138
- strict: true,
139
- decode: (data) => ParseResult.succeed(renderMarkdown(data)),
140
- encode: (text, _options, ast) => ParseResult.fail(new ParseResult.Forbidden(ast, text, "ReposManageAsMarkdown is one-way: markdown cannot be parsed back."))
141
- });
142
+ const ReposManageAsMarkdown = ReposManageResult.pipe(Schema.decodeTo(Schema.String, {
143
+ decode: SchemaGetter.transform(renderMarkdown),
144
+ encode: SchemaGetter.forbidden(() => "ReposManageAsMarkdown is one-way: markdown cannot be parsed back.")
145
+ }));
142
146
  /**
143
147
  * Effect handler: resolve the workspace root, decode the flat wire args into
144
148
  * the internal per-action request (naming the missing field on failure), then
@@ -148,7 +152,7 @@ const reposManage = (args, fallbackCwd) => Effect.gen(function* () {
148
152
  const root = yield* (yield* WorkspaceRoot).find(args.cwd ?? fallbackCwd);
149
153
  const manager = yield* Repos.ReposManager;
150
154
  const { action, cwd: _cwd, ...rest } = args;
151
- const request = yield* Schema.decodeUnknown(ReposManageRequest)({
155
+ const request = yield* Schema.decodeUnknownEffect(ReposManageRequest)({
152
156
  _tag: action,
153
157
  ...rest
154
158
  });
@@ -1,32 +1,29 @@
1
+ import { WorkspaceRoot } from "@effected/workspaces";
1
2
  import { Turbo } from "@savvy-web/silk-effects";
2
- import { Effect, ParseResult, Schema } from "effect";
3
- import { WorkspaceRoot } from "workspaces-effect";
3
+ import { Effect, Schema, SchemaGetter } from "effect";
4
4
 
5
5
  //#region src/tools/turbo-inspect.ts
6
- /**
7
- * The `turbo_inspect` MCP tool: a discriminated-union result schema keyed by
8
- * `mode` (cache | graph | affected), each variant embedding the corresponding
9
- * `Turbo` result schema from silk-effects, plus a one-way markdown transform.
10
- *
11
- * @packageDocumentation
12
- */
13
6
  /** Cache-diagnosis variant of the `turbo_inspect` result. */
14
7
  const TurboCacheResult = Schema.Struct({
15
8
  mode: Schema.Literal("cache"),
16
9
  result: Turbo.CacheDiagnosis
17
- }).annotations({ identifier: "TurboCacheResult" });
10
+ }).annotate({ identifier: "TurboCacheResult" });
18
11
  /** Task-graph variant of the `turbo_inspect` result. */
19
12
  const TurboGraphResult = Schema.Struct({
20
13
  mode: Schema.Literal("graph"),
21
14
  result: Turbo.TaskGraphResult
22
- }).annotations({ identifier: "TurboGraphResult" });
15
+ }).annotate({ identifier: "TurboGraphResult" });
23
16
  /** Affected-packages variant of the `turbo_inspect` result. */
24
17
  const TurboAffectedResult = Schema.Struct({
25
18
  mode: Schema.Literal("affected"),
26
19
  result: Turbo.AffectedResult
27
- }).annotations({ identifier: "TurboAffectedResult" });
20
+ }).annotate({ identifier: "TurboAffectedResult" });
28
21
  /** The `turbo_inspect` tool result — a discriminated union keyed by `mode`. */
29
- const TurboInspectResult = Schema.Union(TurboCacheResult, TurboGraphResult, TurboAffectedResult).annotations({
22
+ const TurboInspectResult = Schema.Union([
23
+ TurboCacheResult,
24
+ TurboGraphResult,
25
+ TurboAffectedResult
26
+ ]).annotate({
30
27
  identifier: "TurboInspectResult",
31
28
  title: "turbo_inspect result",
32
29
  description: "Read-only Turborepo inspection grouped by mode (cache | graph | affected)."
@@ -80,11 +77,10 @@ const renderMarkdown = (data) => {
80
77
  }
81
78
  };
82
79
  /** One-way transform: result to markdown. Encoding back is forbidden. */
83
- const TurboInspectAsMarkdown = Schema.transformOrFail(TurboInspectResult, Schema.String, {
84
- strict: true,
85
- decode: (data) => ParseResult.succeed(renderMarkdown(data)),
86
- encode: (text, _options, ast) => ParseResult.fail(new ParseResult.Forbidden(ast, text, "TurboInspectAsMarkdown is one-way: markdown cannot be parsed back."))
87
- });
80
+ const TurboInspectAsMarkdown = TurboInspectResult.pipe(Schema.decodeTo(Schema.String, {
81
+ decode: SchemaGetter.transform(renderMarkdown),
82
+ encode: SchemaGetter.forbidden(() => "TurboInspectAsMarkdown is one-way: markdown cannot be parsed back.")
83
+ }));
88
84
  /**
89
85
  * Effect handler: resolve the workspace root by walking up from the requested
90
86
  * directory, then dispatch to the matching {@link Turbo.TurboInspector} method
@@ -1,6 +1,6 @@
1
+ import { WorkspaceRoot } from "@effected/workspaces";
1
2
  import { SilkWorkspaceAnalyzer } from "@savvy-web/silk-effects";
2
- import { Effect, ParseResult, Schema } from "effect";
3
- import { WorkspaceRoot } from "workspaces-effect";
3
+ import { Effect, Schema, SchemaGetter } from "effect";
4
4
 
5
5
  //#region src/tools/workspace-info.ts
6
6
  /** A flattened, non-recursive summary of one analyzed workspace. */
@@ -10,24 +10,29 @@ const WorkspaceSummary = Schema.Struct({
10
10
  path: Schema.String,
11
11
  root: Schema.Boolean,
12
12
  publishable: Schema.Boolean,
13
- targets: Schema.Array(Schema.String).annotations({ description: "Publish registry URLs." }),
13
+ targets: Schema.Array(Schema.String).annotate({ description: "Publish registry URLs." }),
14
14
  versioned: Schema.Boolean,
15
15
  tagged: Schema.Boolean,
16
16
  released: Schema.Boolean,
17
- linked: Schema.Array(Schema.String).annotations({ description: "Names of linked workspaces." }),
18
- fixed: Schema.Array(Schema.String).annotations({ description: "Names of fixed-group siblings." })
19
- }).annotations({ identifier: "WorkspaceSummary" });
17
+ linked: Schema.Array(Schema.String).annotate({ description: "Names of linked workspaces." }),
18
+ fixed: Schema.Array(Schema.String).annotate({ description: "Names of fixed-group siblings." })
19
+ }).annotate({ identifier: "WorkspaceSummary" });
20
20
  /** The `workspace_info` tool result — a projection of `WorkspaceAnalysis`. */
21
21
  const WorkspaceInfoResult = Schema.Struct({
22
22
  root: Schema.String,
23
- runtime: Schema.Literal("node", "bun"),
23
+ runtime: Schema.Literals(["node", "bun"]),
24
24
  packageManager: Schema.Struct({
25
- type: Schema.Literal("npm", "pnpm", "yarn", "bun"),
25
+ type: Schema.Literals([
26
+ "npm",
27
+ "pnpm",
28
+ "yarn",
29
+ "bun"
30
+ ]),
26
31
  version: Schema.optional(Schema.String)
27
32
  }),
28
33
  workspaceCount: Schema.Number,
29
34
  workspaces: Schema.Array(WorkspaceSummary)
30
- }).annotations({
35
+ }).annotate({
31
36
  identifier: "WorkspaceInfoResult",
32
37
  title: "workspace_info result",
33
38
  description: "Structured snapshot of the Silk workspace: runtime, package manager, and a per-workspace summary (publishability, versioning, tag/release state, linked/fixed relations)."
@@ -73,11 +78,10 @@ const formatWorkspaceInfoMarkdown = (data) => {
73
78
  return lines.join("\n");
74
79
  };
75
80
  /** One-way transform: result to markdown. Encoding back is forbidden. */
76
- const WorkspaceInfoAsMarkdown = Schema.transformOrFail(WorkspaceInfoResult, Schema.String, {
77
- strict: true,
78
- decode: (data) => ParseResult.succeed(formatWorkspaceInfoMarkdown(data)),
79
- encode: (text, _options, ast) => ParseResult.fail(new ParseResult.Forbidden(ast, text, "WorkspaceInfoAsMarkdown is one-way: markdown cannot be parsed back."))
80
- });
81
+ const WorkspaceInfoAsMarkdown = WorkspaceInfoResult.pipe(Schema.decodeTo(Schema.String, {
82
+ decode: SchemaGetter.transform(formatWorkspaceInfoMarkdown),
83
+ encode: SchemaGetter.forbidden(() => "WorkspaceInfoAsMarkdown is one-way: markdown cannot be parsed back.")
84
+ }));
81
85
  /**
82
86
  * Effect handler: resolve the workspace root by walking up from `base`, analyze
83
87
  * that root, and project to the tool result. Fails with `WorkspaceRootNotFoundError`