@savvy-web/mcp 3.4.17 → 3.4.18

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/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { Distribution } from "@effected/engine";
2
2
  import { ToolRefusal } from "@effected/mcp";
3
3
  import { FileSystem, Layer, Path, Schema, Stdio } from "effect";
4
+ import { ToolDiscovery } from "@effected/commands";
5
+ import { Git } from "@effected/git";
4
6
  import { WorkspaceRoot } from "@effected/workspaces";
5
7
  import { Changesets, Repos, SilkWorkspaceAnalyzer, Turbo } from "@savvy-web/silk-effects";
6
8
  import { ChildProcessSpawner } from "effect/process";
@@ -71,11 +73,13 @@ export type McpToolError = typeof McpToolError.Type;
71
73
  * {@link makeSilkRuntimeLayer} passes both through onto its own output via an
72
74
  * `Effect.context` identity layer, so they survive `main.ts`'s
73
75
  * `Layer.provide(NodeServices.layer)` instead of being fully discharged by
74
- * it.
76
+ * it. `ChildProcessSpawner` is passed through the same way, and `Git` (from the
77
+ * kit graph) plus the shared `ToolDiscovery` are exposed, because
78
+ * `biome_check` yields all three directly.
75
79
  *
76
80
  * @public
77
81
  */
78
- type McpServices = SilkWorkspaceAnalyzer | WorkspaceRoot | Turbo.TurboInspector | Changesets.BranchAnalyzer | Changesets.ConfigInspector | Changesets.ReleasePlanner | Changesets.DepsRegen | Repos.ReposManager | Repos.ReposConfigStore | Repos.ReposDrift | FileSystem.FileSystem | Path.Path;
82
+ type McpServices = SilkWorkspaceAnalyzer | WorkspaceRoot | Turbo.TurboInspector | Changesets.BranchAnalyzer | Changesets.ConfigInspector | Changesets.ReleasePlanner | Changesets.DepsRegen | Repos.ReposManager | Repos.ReposConfigStore | Repos.ReposDrift | Git | ToolDiscovery | ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Path.Path;
79
83
  /**
80
84
  * Build the MCP runtime layer for a workspace root. Provides
81
85
  * `SilkWorkspaceAnalyzer`, `WorkspaceRoot`, `Turbo.TurboInspector`,
@@ -137,8 +141,9 @@ interface ServerOptions {
137
141
  export declare const ServerLayer: (cwd: string, options?: ServerOptions) => Layer.Layer<never, never, PlatformServices>;
138
142
  /**
139
143
  * The `biome_check` tool value. Mutating when `write`/`unsafe` is set, so it is
140
- * annotated non-read-only and non-idempotent; `dependencies` is empty because
141
- * the handler shells out directly and yields no service.
144
+ * annotated non-read-only and non-idempotent. `dependencies` lists what the
145
+ * handler yields: `Git` (worktree containment), `ToolDiscovery` (the Biome
146
+ * binary) and core's `ChildProcessSpawner` (`Run.collect`).
142
147
  */
143
148
  declare const biomeCheckTool: Tool.Tool<"biome_check", {
144
149
  readonly parameters: Schema.Struct<{
@@ -170,7 +175,7 @@ declare const biomeCheckTool: Tool.Tool<"biome_check", {
170
175
  }>;
171
176
  readonly failure: Schema.Union<readonly [typeof WorkspaceNotFound, typeof EngineError, typeof BiomeFailed, typeof ToolRefusal]>;
172
177
  readonly failureMode: "error";
173
- }, never>;
178
+ }, ChildProcessSpawner.ChildProcessSpawner | Git | ToolDiscovery>;
174
179
  /** The `changeset_deps_detect` tool value. */
175
180
  declare const changesetDepsDetectTool: Tool.Tool<"changeset_deps_detect", {
176
181
  readonly parameters: Schema.Struct<{
@@ -672,7 +677,9 @@ export declare const SilkToolkit: Toolkit.Toolkit<SilkTools>;
672
677
  * The handler layer. `cwd` — the project directory `main.ts` resolved once at
673
678
  * startup — is closed over as every handler's fallback when a call omits its
674
679
  * own `cwd`; the handlers' declared service dependencies are discharged by
675
- * `makeSilkRuntimeLayer(cwd)` in `server.ts`.
680
+ * `makeSilkRuntimeLayer(cwd)` in `server.ts`. Building the layer mints the
681
+ * server's ONE `Exclusive` write lane, so concurrent `biome_check` write passes
682
+ * are serialized per server.
676
683
  *
677
684
  * @public
678
685
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savvy-web/mcp",
3
- "version": "3.4.17",
3
+ "version": "3.4.18",
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",
@@ -37,13 +37,13 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "@effect/platform-node": "^4.0.6",
40
- "@effected/commands": "^0.16.0",
41
- "@effected/engine": "^0.8.0",
42
- "@effected/git": "^0.25.1",
43
- "@effected/mcp": "^0.9.0",
40
+ "@effected/commands": "^0.17.0",
41
+ "@effected/engine": "^0.9.0",
42
+ "@effected/git": "^0.26.0",
43
+ "@effected/mcp": "^0.10.0",
44
44
  "@effected/templates": "^0.14.1",
45
- "@effected/workspaces": "^0.39.0",
46
- "@savvy-web/silk-effects": "^10.1.0",
45
+ "@effected/workspaces": "^0.39.1",
46
+ "@savvy-web/silk-effects": "^10.2.0",
47
47
  "effect": "^4.0.6"
48
48
  }
49
49
  }
package/runtime.js CHANGED
@@ -73,7 +73,7 @@ const makeSilkRuntimeLayer = (cwd) => {
73
73
  */
74
74
  const repos = Layer.mergeAll(Repos.ReposConfigStore.layer, Repos.ReposManager.layer.pipe(Layer.provide(Repos.ReposConfigStore.layer), Layer.provide(Repos.ReposLockdown.layer)), Repos.ReposDrift.layer.pipe(Layer.provide(Repos.ReposConfigStore.layer)));
75
75
  const platformPassthrough = Layer.effectContext(Effect.context());
76
- return Layer.mergeAll(SilkWorkspaceAnalyzer.layer.pipe(Layer.provide(analyzerDeps)), Turbo.TurboInspector.layer.pipe(Layer.provide(toolDiscovery)), inspectorAndAnalyzer, depsRegen, repos, platformPassthrough).pipe(Layer.provideMerge(kitGraph));
76
+ return Layer.mergeAll(SilkWorkspaceAnalyzer.layer.pipe(Layer.provide(analyzerDeps)), Turbo.TurboInspector.layer.pipe(Layer.provide(toolDiscovery)), toolDiscovery, inspectorAndAnalyzer, depsRegen, repos, platformPassthrough).pipe(Layer.provideMerge(kitGraph));
77
77
  };
78
78
 
79
79
  export { makeSilkRuntimeLayer };
package/toolkit.js CHANGED
@@ -8,10 +8,19 @@ import { handleReposInspect, reposInspectTool } from "./tools/repos-inspect.js";
8
8
  import { handleReposManage, reposManageTool } from "./tools/repos-manage.js";
9
9
  import { handleTurboInspect, turboInspectTool } from "./tools/turbo-inspect.js";
10
10
  import { handleWorkspaceInfo, workspaceInfoTool } from "./tools/workspace-info.js";
11
+ import { Effect } from "effect";
12
+ import { Exclusive } from "@effected/engine";
11
13
  import { Toolkit } from "effect/ai";
12
14
 
13
15
  // src/toolkit.ts
14
16
  /**
17
+ * Where the ten tool values and their handlers meet: {@link SilkToolkit} is
18
+ * the served toolkit a structural test can import with no bin and no platform
19
+ * layer; {@link ToolsLayer} binds every handler to one project directory.
20
+ *
21
+ * @packageDocumentation
22
+ */
23
+ /**
15
24
  * The ten savvy-mcp tools — seven read-only, three mutating (`biome_check`
16
25
  * with `write`/`unsafe`, `changeset_deps_regen`, `repos_manage`) — in the
17
26
  * order `tools/list` serves them.
@@ -23,11 +32,13 @@ const SilkToolkit = Toolkit.make(workspaceInfoTool, turboInspectTool, changesetI
23
32
  * The handler layer. `cwd` — the project directory `main.ts` resolved once at
24
33
  * startup — is closed over as every handler's fallback when a call omits its
25
34
  * own `cwd`; the handlers' declared service dependencies are discharged by
26
- * `makeSilkRuntimeLayer(cwd)` in `server.ts`.
35
+ * `makeSilkRuntimeLayer(cwd)` in `server.ts`. Building the layer mints the
36
+ * server's ONE `Exclusive` write lane, so concurrent `biome_check` write passes
37
+ * are serialized per server.
27
38
  *
28
39
  * @public
29
40
  */
30
- const ToolsLayer = (cwd) => SilkToolkit.toLayer({
41
+ const ToolsLayer = (cwd) => SilkToolkit.toLayer(Effect.map(Exclusive.make, (biomeWriteLane) => ({
31
42
  workspace_info: (params) => handleWorkspaceInfo(cwd, params),
32
43
  turbo_inspect: (params) => handleTurboInspect(cwd, params),
33
44
  changeset_inspect: (params) => handleChangesetInspect(cwd, params),
@@ -37,7 +48,7 @@ const ToolsLayer = (cwd) => SilkToolkit.toLayer({
37
48
  changeset_deps_regen: (params) => handleChangesetDepsRegen(cwd, params),
38
49
  repos_inspect: (params) => handleReposInspect(cwd, params),
39
50
  repos_manage: handleReposManage(cwd),
40
- biome_check: (params) => handleBiomeCheck(cwd, params)
41
- });
51
+ biome_check: (params) => handleBiomeCheck(cwd, params, biomeWriteLane)
52
+ })));
42
53
 
43
54
  export { SilkToolkit, ToolsLayer };
@@ -1,11 +1,12 @@
1
1
  import { BiomeFailed, McpToolError } from "../errors.js";
2
2
  import { ToolFailure, ToolRefusal } from "@effected/mcp";
3
- import { Effect, Schema } from "effect";
4
- import { Lint } from "@savvy-web/silk-effects";
5
- import { Tool } from "effect/ai";
6
- import { spawnSync } from "node:child_process";
3
+ import { Effect, Option, Schema } from "effect";
4
+ import { Run, Tool, ToolDiscovery } from "@effected/commands";
5
+ import { Tool as Tool$1 } from "effect/ai";
7
6
  import { realpathSync } from "node:fs";
8
7
  import { relative, resolve, sep } from "node:path";
8
+ import { Git, TreeIdentity } from "@effected/git";
9
+ import { ChildProcess, ChildProcessSpawner } from "effect/process";
9
10
 
10
11
  // src/tools/biome-check.ts
11
12
  /**
@@ -136,8 +137,10 @@ const buildBiomeResult = (params) => {
136
137
  };
137
138
  };
138
139
  /**
139
- * Canonicalize a path, falling back to lexical resolution when it does not exist
140
- * (a non-existent target cannot be a symlink pointing out of tree).
140
+ * Canonicalize a caller-supplied path, falling back to lexical resolution when
141
+ * it does not exist (a non-existent target cannot be a symlink pointing out of
142
+ * tree). Also applied to a sibling worktree's `TreeIdentity.toplevel`, so the
143
+ * containment root is spelled the same way as the realpath'd caller paths.
141
144
  */
142
145
  const canonicalize = (p) => {
143
146
  try {
@@ -146,52 +149,43 @@ const canonicalize = (p) => {
146
149
  return resolve(p);
147
150
  }
148
151
  };
149
- /** Real git probe. One `rev-parse` yielding both paths already absolute. */
150
- const gitWorktreeProbe = (dir) => {
151
- const res = spawnSync("git", [
152
- "-C",
153
- dir,
154
- "rev-parse",
155
- "--path-format=absolute",
156
- "--git-common-dir",
157
- "--show-toplevel"
158
- ], {
159
- encoding: "utf8",
160
- timeout: 1e4
161
- });
162
- if (res.error || (res.status ?? 1) !== 0) return null;
163
- const [commonDir, topLevel] = (res.stdout ?? "").trim().split("\n");
164
- if (!commonDir || !topLevel) return null;
165
- return {
166
- commonDir: canonicalize(commonDir.trim()),
167
- topLevel: canonicalize(topLevel.trim())
168
- };
169
- };
152
+ /**
153
+ * The working-tree identity of `dir`, or `Option.none` when it is in no
154
+ * working tree. A git failure (git missing, too old for `--path-format`) also
155
+ * answers `none`: the caller then rejects the cwd, which is what an unknown
156
+ * identity must mean for a mutating tool.
157
+ */
158
+ const treeIdentity = (git, dir) => git.treeIdentity(dir).pipe(Effect.orElseSucceed(() => Option.none()));
170
159
  /**
171
160
  * Decide which directory tree this run may touch.
172
161
  *
173
162
  * @remarks A cwd inside the server's root keeps that root. A cwd OUTSIDE it is
174
- * accepted only when it belongs to a DIFFERENT worktree of the same repository —
175
- * a shared git common dir but a different top level — and containment then follows
176
- * that worktree rather than the server's start directory (systems#482). Binding to the start directory
177
- * meant a call from a sibling worktree silently mutated the main checkout, which in
178
- * a parallel multi-agent session is another agent's tree. Returns null to reject.
163
+ * accepted only when it belongs to a DIFFERENT worktree of the same repository
164
+ * (`TreeIdentity.relation` answers `"sibling-worktree"`), and containment then
165
+ * follows that worktree's toplevel rather than the server's start directory
166
+ * (systems#482). Binding to the start directory meant a call from a sibling
167
+ * worktree silently mutated the main checkout, which in a parallel multi-agent
168
+ * session is another agent's tree.
169
+ *
170
+ * `"same-tree"` is rejected: `root` can be a strict subdirectory of its own
171
+ * worktree (dev tooling launches the server from packages/mcp/), and a cwd
172
+ * elsewhere in that same checkout must not widen a --write pass to the whole
173
+ * repo, including the .repos/** vendored trees. `"different-repo"`, or either
174
+ * directory being in no working tree, is rejected too.
179
175
  *
180
176
  * @param root - the server's workspace root, already canonicalized
181
177
  * @param cwd - the requested working directory, already canonicalized
182
- * @param probe - git identity probe; injectable for tests
183
- * @returns the directory tree to contain to, or null if the cwd must be rejected
178
+ * @returns the directory tree to contain to, or `Option.none` to reject
184
179
  */
185
- const resolveContainmentRoot = (root, cwd, probe = gitWorktreeProbe) => {
186
- if (cwd === root || cwd.startsWith(`${root}${sep}`)) return root;
187
- const from = probe(cwd);
188
- if (!from) return null;
189
- const home = probe(root);
190
- if (!home) return null;
191
- if (from.commonDir !== home.commonDir) return null;
192
- if (from.topLevel === home.topLevel) return null;
193
- return from.topLevel;
194
- };
180
+ const resolveContainmentRoot = Effect.fn("biome_check.resolveContainmentRoot")(function* (root, cwd) {
181
+ if (cwd === root || cwd.startsWith(`${root}${sep}`)) return Option.some(root);
182
+ const git = yield* Git;
183
+ const from = yield* treeIdentity(git, cwd);
184
+ if (Option.isNone(from)) return Option.none();
185
+ const home = yield* treeIdentity(git, root);
186
+ if (Option.isNone(home)) return Option.none();
187
+ return TreeIdentity.relation(from.value, home.value) === "sibling-worktree" ? Option.some(canonicalize(from.value.toplevel)) : Option.none();
188
+ });
195
189
  /** Remediation for a path or cwd that escapes the containment tree. */
196
190
  const CONTAINMENT_REMEDIATION = { hint: "Keep cwd and every path inside the server's workspace root, or inside a git worktree of the same repository." };
197
191
  /** Remediation when no Biome binary can be located. */
@@ -205,81 +199,100 @@ const biomeFailed = (raw, exitCode) => new BiomeFailed({
205
199
  remediation: BIOME_FAILED_REMEDIATION
206
200
  });
207
201
  /**
202
+ * The Biome tool, global copy first — the same lookup order as silk-effects'
203
+ * `Lint.Biome.findBiome` (`Command.findTool` resolves
204
+ * `Tool.named(name, { prefer: "global" })`). One module-level value, so every
205
+ * call hits the same `ToolDiscovery` probe-cache entry.
206
+ */
207
+ const BIOME_TOOL = Tool.named("biome", { prefer: "global" });
208
+ /** Per-stream captured-output ceiling for one Biome run. */
209
+ const MAX_OUTPUT_BYTES = 67108864;
210
+ /**
211
+ * Hard cap so a hung Biome process (password prompt, deadlock, stalled
212
+ * watcher) can't block the MCP server. Expiry fails the run as a
213
+ * `CommandFailedError` of kind `"timeout"`.
214
+ */
215
+ const BIOME_TIMEOUT = "120 seconds";
216
+ /**
217
+ * Backstop on the write lane, a little above {@link BIOME_TIMEOUT} so the run's
218
+ * own typed timeout fires first; the lane only bounds how long one write may
219
+ * hold the permit, never the wait for it.
220
+ */
221
+ const WRITE_LANE_TIMEOUT = "125 seconds";
222
+ /**
223
+ * Re-scope a resolved Biome command to the request's `cwd` (a local
224
+ * resolution is scoped to the server's root by its `ExecContext`; paths are
225
+ * relative to `cwd`) and kill it with `SIGKILL` on timeout — core's default
226
+ * `SIGTERM` with no `forceKillAfter` would wait on a child that ignores it.
227
+ */
228
+ const scoped = (command, cwd) => command._tag === "StandardCommand" ? ChildProcess.make(command.command, command.args, {
229
+ ...command.options,
230
+ cwd,
231
+ killSignal: "SIGKILL"
232
+ }) : ChildProcess.setCwd(command, cwd);
233
+ /**
208
234
  * Run Biome and return structured diagnostics. When `write`/`unsafe` is set,
209
235
  * runs a fix pass first, then a read-only gitlab pass to report what remains.
210
236
  *
211
- * @remarks Resolves the Biome binary via {@link Lint.Biome.findBiome} (global
212
- * first, then the project's package manager). Throws a typed
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.
237
+ * @remarks Resolves the Biome binary through `ToolDiscovery` (global first,
238
+ * then the project's package manager — {@link BIOME_TOOL}) and runs it with
239
+ * `Run.collect`. Fails with a {@link McpToolError} member: a `ToolRefusal` for
240
+ * a cwd/path outside the containment tree or when no binary resolves,
241
+ * {@link BiomeFailed} when Biome exits with status > 1 (Biome itself failed,
242
+ * vs. status 1 = lint issues found), cannot spawn, times out, or overflows the
243
+ * output cap. The write pass runs on `lane`, one permit per server, so two
244
+ * concurrent write calls never interleave on a tree.
245
+ *
246
+ * @param args - the decoded tool parameters
247
+ * @param fallbackCwd - the server's project directory
248
+ * @param lane - the server's write lane
218
249
  */
219
- const runBiomeCheck = async (args, fallbackCwd) => {
250
+ const runBiomeCheck = Effect.fn("biome_check.run")(function* (args, fallbackCwd, lane) {
220
251
  const mode = args.mode ?? "check";
221
252
  const root = canonicalize(fallbackCwd);
222
253
  const cwd = canonicalize(args.cwd ?? fallbackCwd);
223
- const containmentRoot = resolveContainmentRoot(root, cwd);
224
- if (containmentRoot === null) throw ToolRefusal.refuse(`cwd escapes the workspace root: ${ToolFailure.truncate(args.cwd ?? fallbackCwd)}.`, CONTAINMENT_REMEDIATION);
254
+ const contained = yield* resolveContainmentRoot(root, cwd);
255
+ if (Option.isNone(contained)) return yield* ToolRefusal.refuse(`cwd escapes the workspace root: ${ToolFailure.truncate(args.cwd ?? fallbackCwd)}.`, CONTAINMENT_REMEDIATION);
256
+ const containmentRoot = contained.value;
225
257
  const within = (abs) => abs === containmentRoot || abs.startsWith(`${containmentRoot}${sep}`);
226
- const paths = (args.paths && args.paths.length > 0 ? args.paths : ["."]).map((p) => {
258
+ const rawPaths = args.paths && args.paths.length > 0 ? args.paths : ["."];
259
+ const paths = [];
260
+ for (const p of rawPaths) {
227
261
  const lexical = resolve(cwd, p);
228
- if (!within(canonicalize(lexical))) throw ToolRefusal.refuse(`path escapes the workspace root: ${ToolFailure.truncate(p)}.`, CONTAINMENT_REMEDIATION);
229
- return relative(cwd, lexical) || ".";
230
- });
262
+ if (!within(canonicalize(lexical))) return yield* ToolRefusal.refuse(`path escapes the workspace root: ${ToolFailure.truncate(p)}.`, CONTAINMENT_REMEDIATION);
263
+ paths.push(relative(cwd, lexical) || ".");
264
+ }
231
265
  const doWrite = Boolean(args.write || args.unsafe);
232
- const biomeCmd = Lint.Biome.findBiome();
233
- if (!biomeCmd) throw ToolRefusal.refuse("Biome not found.", BIOME_REMEDIATION);
234
- const parts = biomeCmd.split(" ");
235
- const bin = parts[0];
236
- const prefix = parts.slice(1);
237
- const maxBuffer = 67108864;
238
- const timeout = 12e4;
239
- const killSignal = "SIGKILL";
266
+ const biome = yield* (yield* ToolDiscovery).resolve(BIOME_TOOL).pipe(Effect.mapError((error) => error._tag === "ToolNotFoundError" ? ToolRefusal.refuse("Biome not found.", BIOME_REMEDIATION) : ToolRefusal.refuse(`Biome could not be resolved: ${error.message}`, BIOME_REMEDIATION)));
267
+ /** One Biome pass; exit 0/1 is a result, anything else a {@link BiomeFailed}. */
268
+ const pass = (label, argv) => Run.collect(scoped(biome.command(...argv), cwd), {
269
+ timeout: BIOME_TIMEOUT,
270
+ maxOutputBytes: MAX_OUTPUT_BYTES
271
+ }).pipe(Effect.mapError((error) => biomeFailed(`${label} could not run: ${error.message}`, error.exitCode)), Effect.flatMap((output) => output.exitCode > 1 ? Effect.fail(biomeFailed(`${label} failed (exit ${output.exitCode}): ${output.stderr.trim() || "unknown error"}`, output.exitCode)) : Effect.succeed(output)));
240
272
  let wrote = false;
241
273
  if (doWrite) {
242
274
  const writeArgs = [
243
- ...prefix,
244
275
  mode,
245
276
  "--write",
246
277
  ...args.unsafe ? ["--unsafe"] : [],
247
278
  "--no-errors-on-unmatched",
248
279
  ...paths
249
280
  ];
250
- const fix = spawnSync(bin, writeArgs, {
251
- cwd,
252
- encoding: "utf8",
253
- maxBuffer,
254
- timeout,
255
- killSignal
256
- });
257
- if (fix.error) throw biomeFailed(`Biome --write could not run: ${fix.error.message}`);
258
- if ((fix.status ?? 0) > 1) throw biomeFailed(`Biome --write failed (exit ${fix.status}): ${(fix.stderr ?? "").trim() || "unknown error"}`, fix.status ?? void 0);
281
+ yield* lane.run(pass("Biome --write", writeArgs), { timeout: WRITE_LANE_TIMEOUT }).pipe(Effect.catchTag("ExclusiveTimeout", (timeout) => Effect.fail(biomeFailed(`Biome --write did not finish: ${timeout.message}`))));
259
282
  wrote = true;
260
283
  }
261
- const readArgs = [
262
- ...prefix,
284
+ const read = yield* pass("Biome", [
263
285
  mode,
264
286
  "--reporter=gitlab",
265
287
  "--no-errors-on-unmatched",
266
288
  ...paths
267
- ];
268
- const read = spawnSync(bin, readArgs, {
269
- cwd,
270
- encoding: "utf8",
271
- maxBuffer,
272
- timeout,
273
- killSignal
274
- });
275
- if (read.error) throw biomeFailed(`Biome could not run: ${read.error.message}`);
276
- if ((read.status ?? 0) > 1) throw biomeFailed(`Biome failed (exit ${read.status}): ${(read.stderr ?? "").trim() || "unknown error"}`, read.status ?? void 0);
289
+ ]);
277
290
  return buildBiomeResult({
278
- diagnostics: parseBiomeGitlab(read.stdout ?? ""),
291
+ diagnostics: parseBiomeGitlab(read.stdout),
279
292
  wrote,
280
293
  ...args.strict !== void 0 ? { strict: args.strict } : {}
281
294
  });
282
- };
295
+ });
283
296
  /** Wire parameters for `biome_check`. */
284
297
  const BiomeCheckParams = Schema.Struct({
285
298
  paths: Schema.optionalKey(Schema.Array(Schema.String).annotate({ description: "Paths to check. Defaults to the whole workspace." })),
@@ -291,31 +304,30 @@ const BiomeCheckParams = Schema.Struct({
291
304
  });
292
305
  /**
293
306
  * The `biome_check` tool value. Mutating when `write`/`unsafe` is set, so it is
294
- * annotated non-read-only and non-idempotent; `dependencies` is empty because
295
- * the handler shells out directly and yields no service.
307
+ * annotated non-read-only and non-idempotent. `dependencies` lists what the
308
+ * handler yields: `Git` (worktree containment), `ToolDiscovery` (the Biome
309
+ * binary) and core's `ChildProcessSpawner` (`Run.collect`).
296
310
  */
297
- const biomeCheckTool = Tool.make("biome_check", {
311
+ const biomeCheckTool = Tool$1.make("biome_check", {
298
312
  description: "Run Biome over a path and get structured diagnostics back. mode=check (default; lint + format + organize-imports) or mode=lint. Set write=true to apply safe fixes (--write), unsafe=true for unsafe fixes (--write --unsafe). Severities match the project's Biome config (what `biome check` reports); set strict=true to surface project warnings as errors, each marked with its originalSeverity. Prefer this over shelling out to biome; the LSP already covers files you've edited. Returns a typed object in structuredContent (content[] carries the same object as JSON). NOTE: with write/unsafe this tool MUTATES files (git-reversible).",
299
313
  parameters: BiomeCheckParams,
300
314
  success: BiomeCheckResult,
301
- failure: McpToolError
302
- }).annotate(Tool.Title, "Biome check").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, false).annotate(Tool.OpenWorld, false);
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);
315
+ failure: McpToolError,
316
+ dependencies: [
317
+ Git,
318
+ ToolDiscovery,
319
+ ChildProcessSpawner.ChildProcessSpawner
320
+ ]
321
+ }).annotate(Tool$1.Title, "Biome check").annotate(Tool$1.Readonly, false).annotate(Tool$1.Destructive, false).annotate(Tool$1.Idempotent, false).annotate(Tool$1.OpenWorld, false);
309
322
  /**
310
- * Wire handler: {@link runBiomeCheck} lifted into Effect. The typed members it
311
- * throws pass through unchanged; anything else (a defect in the parser, say)
312
- * is reported as {@link BiomeFailed} so the error channel stays closed over
313
- * {@link McpToolError} — nothing undeclared reaches core, which would scrub
314
- * it to a generic internal-error text.
323
+ * Wire handler: {@link runBiomeCheck} with its error channel closed over
324
+ * {@link McpToolError}. A defect (a bug in the parser, say) is reported as
325
+ * {@link BiomeFailed} rather than reaching core, which would scrub it to a
326
+ * generic internal-error text.
327
+ *
328
+ * @param fallbackCwd - the server's project directory
329
+ * @param lane - the server's one write lane, built once in `ToolsLayer`
315
330
  */
316
- const handleBiomeCheck = (fallbackCwd, params) => Effect.tryPromise({
317
- try: () => runBiomeCheck(params, fallbackCwd),
318
- catch: (cause) => isMcpToolError(cause) ? cause : biomeFailed(`Biome check failed: ${cause instanceof Error ? cause.message : String(cause)}`)
319
- });
331
+ const handleBiomeCheck = (fallbackCwd, params, lane) => runBiomeCheck(params, fallbackCwd, lane).pipe(Effect.catchDefect((defect) => Effect.fail(biomeFailed(`Biome check failed: ${defect instanceof Error ? defect.message : String(defect)}`))));
320
332
 
321
333
  export { BiomeCheckParams, BiomeCheckResult, BiomeDiagnostic, BiomeSeverity, biomeCheckTool, buildBiomeResult, handleBiomeCheck, parseBiomeGitlab, resolveContainmentRoot, runBiomeCheck };
package/version.js CHANGED
@@ -6,6 +6,6 @@
6
6
  *
7
7
  * @packageDocumentation
8
8
  */
9
- const CURRENT_MCP_VERSION = "3.4.17";
9
+ const CURRENT_MCP_VERSION = "3.4.18";
10
10
 
11
11
  export { CURRENT_MCP_VERSION };