@savvy-web/mcp 3.2.3 → 3.3.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/server.js CHANGED
@@ -1,11 +1,9 @@
1
- import { SilkMarkdown } from "./markdown.js";
2
1
  import { makeSilkRuntimeLayer } from "./runtime.js";
3
2
  import { SilkToolkit, ToolsLayer } from "./toolkit.js";
4
3
  import { CURRENT_MCP_VERSION } from "./version.js";
5
- import { Cause, Context, Effect, ErrorReporter, Layer, Option, Result, Schema, Stream } from "effect";
6
- import { CurrentLogLevel } from "effect/References";
7
- import { AiError, McpProtocol, McpSchema, McpServer, Tool, Toolkit } from "effect/unstable/ai";
8
- import { HttpServerRequest } from "effect/unstable/http";
4
+ import { McpStdio, McpToolkit } from "@effected/mcp";
5
+ import { Layer, Option } from "effect";
6
+ import { distributionSuffix } from "@effected/engine";
9
7
 
10
8
  //#region src/server.ts
11
9
  /**
@@ -25,187 +23,33 @@ const SERVER_INSTRUCTIONS = [
25
23
  "changeset_inspect, changeset_validate, changeset_preview: the branch diff by owning package, typed CSH001-CSH005 diagnostics, and the CHANGELOG the pending changesets would produce.",
26
24
  "changeset_deps_detect (reads) and changeset_deps_regen (deletes and recreates) dependency changesets.",
27
25
  "repos_inspect (reads) and repos_manage (mutates) the vendored reference repos under .repos/; repos_manage restore discards uncommitted worktree edits.",
28
- "Every successful result carries a markdown transcript in content[0].text and the typed object in structuredContent; read structuredContent when you need fields. A failure is an isError result whose content[0].text carries the message and remediation, with no structuredContent. Every tool takes an optional cwd; omit it to use the server's project directory."
26
+ "Every successful result is a typed object in structuredContent (content[0].text carries the same object as JSON). A failure is an isError result whose content[0].text carries the message and remediation, with no structuredContent. Every tool takes an optional cwd; omit it to use the server's project directory."
29
27
  ].join("\n");
30
- const INTERNAL_TOOL_ERROR_MESSAGE = "Tool execution failed due to an internal server error.";
31
- const toolErrorResult = (message) => new McpSchema.CallToolResult({
32
- isError: true,
33
- content: [{
34
- type: "text",
35
- text: message
36
- }]
37
- });
38
- const toolResultContent = (encoded) => encoded === void 0 ? [] : [{
39
- type: "text",
40
- text: JSON.stringify(encoded)
41
- }];
42
- const omitRequestServices = Context.omit(McpSchema.McpRequestContext, McpSchema.McpServerClient, HttpServerRequest.HttpServerRequest, CurrentLogLevel);
43
- const isParameterValidationError = (error) => AiError.isAiError(error) && error.reason._tag === "ToolParameterValidationError";
44
28
  /**
45
- * Hoist a `$ref`-rooted JSON Schema document onto its referenced definition.
46
- * `Schema.toJsonSchemaDocument` emits `{ $ref: "#/$defs/<id>", $defs }` for
47
- * any schema annotated with an `identifier` — every result schema here is —
48
- * and a strict MCP client needs a `type: "object"` root on `outputSchema`,
49
- * so without this no tool would serve one. The definitions stay attached
50
- * for nested refs. (rc.116's own `registerToolkit` inlines the top-level
51
- * reference of the INPUT schema through an internal helper and serves the
52
- * output document verbatim; this is the public-API equivalent for both.)
29
+ * The toolkit registration as a layer: `McpToolkit.layer` over core's
30
+ * module-level `McpServer.McpServer.layer` — the same reference
31
+ * `McpStdio.layer` serves, so layer memoization lands the registration on the
32
+ * served instance — with the handlers bound to `cwd`.
53
33
  */
54
- const hoistRootRef = (schema) => {
55
- const ref = schema.$ref;
56
- const defs = schema.$defs;
57
- if (typeof ref !== "string" || !ref.startsWith("#/$defs/") || typeof defs !== "object" || defs === null) return schema;
58
- const target = defs[ref.slice(8)];
59
- if (typeof target !== "object" || target === null) return schema;
60
- const { $ref: _ref, ...rest } = schema;
61
- return {
62
- ...rest,
63
- ...target,
64
- $defs: defs
65
- };
66
- };
67
- /**
68
- * The served input schema: MCP requires an object root, so a top-level `$ref`
69
- * is inlined, and a strict tool is emitted with `onExcessProperty: "error"`
70
- * so the client sees `additionalProperties: false`. Mirrors rc.116's
71
- * `toolInputJsonSchema` through public API (`Schema.toJsonSchemaDocument` +
72
- * {@link hoistRootRef} in place of the internal `resolveTopLevelReference`).
73
- */
74
- const toolInputJsonSchema = (schema, strict) => {
75
- const document = Schema.toJsonSchemaDocument(schema, { onExcessProperty: strict ? "error" : "ignore" });
76
- const withDefs = Object.keys(document.definitions).length === 0 ? document.schema : {
77
- ...document.schema,
78
- $defs: document.definitions
79
- };
80
- return hoistRootRef(withDefs);
81
- };
82
- /** MCP models `structuredContent` as a JSON object, so a `null` or array encoded result is omitted. */
83
- const toStructuredContent = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
84
- /**
85
- * Register a toolkit with the running `McpServer`, rendering each success as
86
- * the tool's markdown transcript in `content[0].text` and the encoded result
87
- * in `structuredContent`. A port of rc.116's `McpServer.registerToolkit`
88
- * that differs in the success branch and the `outputSchema` gate only (see
89
- * the module remarks).
90
- *
91
- * @remarks
92
- * `Effect.context<never>()` captures the registration-time services (the
93
- * handlers' declared dependencies, discharged by the runtime layer) so each
94
- * call runs against them; the per-request services (`McpRequestContext`,
95
- * the legacy `McpServerClient`, the HTTP request, the request log level) are
96
- * omitted from that capture because the framework provides them per call.
97
- *
98
- * @internal exported for the in-process suite, which registers fixture
99
- * toolkits (a strict tool, for one) through the same path the served
100
- * toolkit takes.
101
- */
102
- const registerSilkToolkit = (toolkit) => Effect.gen(function* () {
103
- const registry = yield* McpServer.McpServer;
104
- const built = yield* toolkit.pipe(Effect.updateContext((context) => {
105
- const services = new Map(context.mapUnsafe);
106
- for (const tool of Object.values(toolkit.tools)) {
107
- const handler = services.get(tool.id);
108
- if (handler !== void 0) services.set(tool.id, {
109
- ...handler,
110
- context: omitRequestServices(handler.context)
111
- });
112
- }
113
- return Context.makeUnsafe(services);
114
- }));
115
- const services = omitRequestServices(yield* Effect.context());
116
- const reportCause = (cause) => Effect.provideContext(ErrorReporter.report(cause), services);
117
- const internalToolError = (cause) => {
118
- const failure = Cause.findFail(cause);
119
- return Result.isFailure(failure) && !Cause.hasDies(cause) ? Effect.failCause(failure.failure) : Effect.logError(cause).pipe(Effect.andThen(reportCause(cause)), Effect.as(toolErrorResult(INTERNAL_TOOL_ERROR_MESSAGE)));
120
- };
121
- const registrations = [];
122
- for (const tool of Object.values(built.tools)) {
123
- const strict = Tool.getStrictMode(tool) === true;
124
- const rawJsonSchema = Tool.isDynamic(tool) ? tool.jsonSchema : void 0;
125
- if (strict && rawJsonSchema !== void 0) return yield* Effect.die(`McpServer cannot strictly validate the raw JSON Schema for tool '${tool.name}'; use an Effect Schema instead`);
126
- const decodeOptions = strict ? { onExcessProperty: "error" } : void 0;
127
- const annotations = tool.annotations;
128
- const renderMarkdown = Context.getOrUndefined(annotations, SilkMarkdown);
129
- const toolMeta = Context.getOrUndefined(annotations, Tool.Meta);
130
- const isDeclaredFailure = Schema.is(tool.failureSchema);
131
- const encodeFailure = Schema.encodeUnknownEffect(tool.failureSchema);
132
- const declaredFailureResult = (error) => error instanceof Error ? Effect.succeed(toolErrorResult(error.message)) : Effect.map(encodeFailure(error), (encoded) => new McpSchema.CallToolResult({
133
- isError: true,
134
- content: toolResultContent(encoded)
135
- }));
136
- const handleCause = (cause) => {
137
- const failure = Cause.findFail(cause);
138
- if (Result.isSuccess(failure)) {
139
- const error = failure.success.error;
140
- const origin = Context.get(Cause.reasonAnnotations(failure.success), Toolkit.FailureOrigin);
141
- if (origin === "parameters" && isParameterValidationError(error)) return Effect.fail(new McpSchema.InvalidParams({ message: error.reason.message }));
142
- if (origin === "handler" && isDeclaredFailure(error)) return Effect.logError(cause).pipe(Effect.andThen(Effect.catchCause(declaredFailureResult(error), internalToolError)));
143
- }
144
- return internalToolError(cause);
145
- };
146
- const outputJsonSchema = hoistRootRef(Tool.getJsonSchemaFromSchema(tool.successSchema));
147
- const outputSchema = outputJsonSchema.type === "object" ? yield* Schema.decodeUnknownEffect(McpSchema.ToolOutputJson)(outputJsonSchema).pipe(Effect.orDie) : void 0;
148
- const inputSchema = yield* Schema.decodeUnknownEffect(McpSchema.ToolJson)(rawJsonSchema ?? toolInputJsonSchema(tool.parametersSchema, strict)).pipe(Effect.orDie);
149
- const description = Tool.getDescription(tool);
150
- const mcpTool = new McpSchema.Tool({
151
- name: tool.name,
152
- ...description === void 0 ? {} : { description },
153
- inputSchema,
154
- ...outputSchema === void 0 ? {} : { outputSchema },
155
- annotations: {
156
- ...Context.getOption(annotations, Tool.Title).pipe(Option.map((title) => ({ title })), Option.getOrUndefined),
157
- readOnlyHint: Context.get(annotations, Tool.Readonly),
158
- destructiveHint: Context.get(annotations, Tool.Destructive),
159
- idempotentHint: Context.get(annotations, Tool.Idempotent),
160
- openWorldHint: Context.get(annotations, Tool.OpenWorld)
161
- },
162
- ...toolMeta === void 0 ? {} : { _meta: toolMeta }
163
- });
164
- registrations.push({
165
- tool: mcpTool,
166
- annotations,
167
- handle: (payload) => built.handle(tool.name, payload ?? {}, void 0, decodeOptions).pipe(Stream.unwrap, Stream.runLast, Effect.flatMap(Effect.fromOption), Effect.flatMap((result) => result.isFailure && result.failureOrigin !== "handler" ? Effect.failCause(Cause.annotate(Cause.fail(result.result), Context.make(Toolkit.FailureOrigin, result.failureOrigin ?? "result"))) : Effect.succeed(new McpSchema.CallToolResult({
168
- isError: result.isFailure,
169
- structuredContent: result.isFailure ? void 0 : toStructuredContent(result.encodedResult),
170
- content: result.isFailure || renderMarkdown === void 0 ? toolResultContent(result.encodedResult) : [{
171
- type: "text",
172
- text: renderMarkdown(result.result)
173
- }]
174
- }))), Effect.catchCause(handleCause), Effect.provideContext(services))
175
- });
176
- }
177
- for (const registration of registrations) yield* registry.addTool(registration);
178
- });
179
- /**
180
- * The toolkit registration as a layer: {@link registerSilkToolkit} over
181
- * `McpServer.layer` (the same reference `McpServer.layerStdio` merges, so
182
- * layer memoization lands the registration on the served instance), with the
183
- * handlers bound to `cwd`.
184
- */
185
- const SilkToolsLayer = (cwd) => Layer.effectDiscard(registerSilkToolkit(SilkToolkit)).pipe(Layer.provide(McpServer.McpServer.layer), Layer.provide(ToolsLayer(cwd)));
34
+ const SilkToolsLayer = (cwd) => McpToolkit.layer(SilkToolkit, { strict: "annotated" }).pipe(Layer.provide(ToolsLayer(cwd)));
186
35
  /**
187
36
  * The whole server as one layer: the ten-tool toolkit, the silk-effects
188
- * runtime discharging its dependencies, over `McpServer.layerStdio`.
37
+ * runtime discharging its dependencies, over `@effected/mcp`'s
38
+ * `McpStdio.layer`.
189
39
  *
190
- * `protocols` lists the stateless `2026-07-28` adapter first, then the two
191
- * newest stateful ones — see gotcha 2 in the module remarks.
192
- * `Cause.IllegalArgumentError` in `layerStdio`'s error channel is `orDie`d:
193
- * `protocols` is a static literal with exactly one stateless member, so a
194
- * failure there is an implementer-time defect, not a runtime condition.
40
+ * `McpStdio.layer` is `McpServer.layerStdio` with the kit's default
41
+ * `protocols` (the stateless `2026-07-28` adapter first, then the two newest
42
+ * stateful ones — see gotcha 2), `LogToStderr` merged into its output, a
43
+ * stdin guard answering a non-JSON line with `-32700`, and `Layer.orDie`.
195
44
  *
196
45
  * @public
197
46
  */
198
- const ServerLayer = (cwd) => SilkToolsLayer(cwd).pipe(Layer.provide(makeSilkRuntimeLayer(cwd)), Layer.provide(McpServer.layerStdio({
47
+ const ServerLayer = (cwd, options = {}) => SilkToolsLayer(cwd).pipe(Layer.provide(makeSilkRuntimeLayer(cwd)), Layer.provide(McpStdio.layer({
199
48
  name: "savvy-mcp",
200
- version: CURRENT_MCP_VERSION,
49
+ version: `${CURRENT_MCP_VERSION}${distributionSuffix(Option.fromNullishOr(options.distribution))}`,
201
50
  description: "Structured Silk Suite workspace tools: workspace, turbo, biome, changesets and vendored repos.",
202
- instructions: SERVER_INSTRUCTIONS,
203
- protocols: [
204
- McpProtocol.v2026_07_28,
205
- McpProtocol.v2025_11_25,
206
- McpProtocol.v2025_06_18
207
- ]
208
- })), Layer.orDie);
51
+ instructions: SERVER_INSTRUCTIONS
52
+ })));
209
53
 
210
54
  //#endregion
211
- export { SERVER_INSTRUCTIONS, ServerLayer, registerSilkToolkit };
55
+ export { SERVER_INSTRUCTIONS, ServerLayer };
@@ -1,6 +1,6 @@
1
- import { BiomeFailed, BiomeUnavailable, InvalidArgument, McpToolError, composeRemediatedMessage, invalidArgument, truncateEchoed } from "../errors.js";
2
- import { SilkMarkdown } from "../markdown.js";
3
- import { Effect, Schema, SchemaGetter } from "effect";
1
+ import { BiomeFailed, BiomeUnavailable, InvalidArgument, McpToolError, invalidArgument } from "../errors.js";
2
+ import { ToolFailure } from "@effected/mcp";
3
+ import { Effect, Schema } from "effect";
4
4
  import { Lint } from "@savvy-web/silk-effects";
5
5
  import { Tool } from "effect/unstable/ai";
6
6
  import { spawnSync } from "node:child_process";
@@ -135,25 +135,6 @@ const buildBiomeResult = (params) => {
135
135
  guidance: diagnostics.length === 0 ? GUIDANCE_ERRORS : guidance
136
136
  };
137
137
  };
138
- /** Render the structured result as markdown. */
139
- const renderMarkdown = (data) => {
140
- if (data.diagnostics.length === 0) return `# biome — clean\n\n✅ No remaining diagnostics.${data.wrote ? " A --write pass ran; check `git diff` for what changed." : ""}`;
141
- const upgraded = data.summary.upgradedWarnings ?? 0;
142
- const upgradedNote = upgraded > 0 ? ` (${upgraded} strict-upgraded from project warnings)` : "";
143
- const lines = [`# biome — ${data.summary.errors} error(s)${upgradedNote}, ${data.summary.warnings} warning(s)`, ``];
144
- if (data.wrote) lines.push(`A --write pass ran; the diagnostics below remain unfixed.`, ``);
145
- for (const d of data.diagnostics) {
146
- const severity = d.originalSeverity ? `${d.severity} (project ${d.originalSeverity}, strict)` : d.severity;
147
- lines.push(`- \`${d.file}:${d.line}\` **${severity}** ${d.rule} — ${d.message}`);
148
- }
149
- lines.push(``, `---`, data.guidance);
150
- return lines.join("\n");
151
- };
152
- /** One-way transform: result to markdown. Encoding back is forbidden. */
153
- const BiomeCheckAsMarkdown = BiomeCheckResult.pipe(Schema.decodeTo(Schema.String, {
154
- decode: SchemaGetter.transform(renderMarkdown),
155
- encode: SchemaGetter.forbidden(() => "BiomeCheckAsMarkdown is one-way: markdown cannot be parsed back.")
156
- }));
157
138
  /**
158
139
  * Canonicalize a path, falling back to lexical resolution when it does not exist
159
140
  * (a non-existent target cannot be a symlink pointing out of tree).
@@ -220,7 +201,7 @@ const BIOME_FAILED_REMEDIATION = { hint: "Biome did not complete; the message ca
220
201
  /** Build a {@link BiomeFailed} from a spawn error or a non-lint exit status. */
221
202
  const biomeFailed = (raw, exitCode) => new BiomeFailed({
222
203
  ...exitCode === void 0 ? {} : { exitCode },
223
- message: composeRemediatedMessage(raw, BIOME_FAILED_REMEDIATION),
204
+ message: ToolFailure.message(raw, BIOME_FAILED_REMEDIATION),
224
205
  remediation: BIOME_FAILED_REMEDIATION
225
206
  });
226
207
  /**
@@ -240,17 +221,17 @@ const runBiomeCheck = async (args, fallbackCwd) => {
240
221
  const root = canonicalize(fallbackCwd);
241
222
  const cwd = canonicalize(args.cwd ?? fallbackCwd);
242
223
  const containmentRoot = resolveContainmentRoot(root, cwd);
243
- if (containmentRoot === null) throw invalidArgument("cwd", `cwd escapes the workspace root: ${truncateEchoed(args.cwd ?? fallbackCwd)}.`, CONTAINMENT_REMEDIATION);
224
+ if (containmentRoot === null) throw invalidArgument("cwd", `cwd escapes the workspace root: ${ToolFailure.truncate(args.cwd ?? fallbackCwd)}.`, CONTAINMENT_REMEDIATION);
244
225
  const within = (abs) => abs === containmentRoot || abs.startsWith(`${containmentRoot}${sep}`);
245
226
  const paths = (args.paths && args.paths.length > 0 ? args.paths : ["."]).map((p) => {
246
227
  const lexical = resolve(cwd, p);
247
- if (!within(canonicalize(lexical))) throw invalidArgument("paths", `path escapes the workspace root: ${truncateEchoed(p)}.`, CONTAINMENT_REMEDIATION);
228
+ if (!within(canonicalize(lexical))) throw invalidArgument("paths", `path escapes the workspace root: ${ToolFailure.truncate(p)}.`, CONTAINMENT_REMEDIATION);
248
229
  return relative(cwd, lexical) || ".";
249
230
  });
250
231
  const doWrite = Boolean(args.write || args.unsafe);
251
232
  const biomeCmd = Lint.Biome.findBiome();
252
233
  if (!biomeCmd) throw new BiomeUnavailable({
253
- message: composeRemediatedMessage("Biome not found.", BIOME_REMEDIATION),
234
+ message: ToolFailure.message("Biome not found.", BIOME_REMEDIATION),
254
235
  remediation: BIOME_REMEDIATION
255
236
  });
256
237
  const parts = biomeCmd.split(" ");
@@ -317,11 +298,11 @@ const BiomeCheckParams = Schema.Struct({
317
298
  * the handler shells out directly and yields no service.
318
299
  */
319
300
  const biomeCheckTool = Tool.make("biome_check", {
320
- 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 markdown in content[] and a typed object in structuredContent. NOTE: with write/unsafe this tool MUTATES files (git-reversible).",
301
+ 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).",
321
302
  parameters: BiomeCheckParams,
322
303
  success: BiomeCheckResult,
323
304
  failure: McpToolError
324
- }).annotate(Tool.Title, "Biome check").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, false).annotate(Tool.OpenWorld, false).annotate(SilkMarkdown, Schema.decodeUnknownSync(BiomeCheckAsMarkdown));
305
+ }).annotate(Tool.Title, "Biome check").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, false).annotate(Tool.OpenWorld, false);
325
306
  const isMcpToolError = (u) => u instanceof InvalidArgument || u instanceof BiomeUnavailable || u instanceof BiomeFailed;
326
307
  /**
327
308
  * Wire handler: {@link runBiomeCheck} lifted into Effect. The typed members it
@@ -335,4 +316,4 @@ const handleBiomeCheck = (fallbackCwd, params) => Effect.tryPromise({
335
316
  });
336
317
 
337
318
  //#endregion
338
- export { BiomeCheckAsMarkdown, BiomeCheckParams, BiomeCheckResult, BiomeDiagnostic, BiomeSeverity, biomeCheckTool, buildBiomeResult, handleBiomeCheck, parseBiomeGitlab, resolveContainmentRoot, runBiomeCheck };
319
+ export { BiomeCheckParams, BiomeCheckResult, BiomeDiagnostic, BiomeSeverity, biomeCheckTool, buildBiomeResult, handleBiomeCheck, parseBiomeGitlab, resolveContainmentRoot, runBiomeCheck };
@@ -1,6 +1,5 @@
1
1
  import { McpToolError, mapEngineError } from "../errors.js";
2
- import { SilkMarkdown } from "../markdown.js";
3
- import { Effect, Schema, SchemaGetter } from "effect";
2
+ import { Effect, Schema } from "effect";
4
3
  import { WorkspaceRoot } from "@effected/workspaces";
5
4
  import { Changesets } from "@savvy-web/silk-effects";
6
5
  import { Tool } from "effect/unstable/ai";
@@ -32,49 +31,6 @@ const ChangesetDepsDetectResult = Schema.Struct({
32
31
  description: "Read-only per-package dependency diff (devDependencies retained). No files are written."
33
32
  });
34
33
  /**
35
- * Render a repo-derived value as an inert markdown code span so a crafted path,
36
- * package, or dependency name cannot inject markdown structure into the
37
- * transcript an agent reads. Control characters (which would break table rows
38
- * and headings) are flattened to spaces, table-cell pipes are escaped, and the
39
- * span is fenced with a backtick run longer than any in the value — CommonMark
40
- * forbids backslash-escaping a backtick inside a code span.
41
- */
42
- const mdInline = (value) => {
43
- const safe = value.replace(/\p{Cc}/gu, " ").replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
44
- const longest = safe.match(/`+/g)?.reduce((m, run) => Math.max(m, run.length), 0) ?? 0;
45
- const fence = "`".repeat(longest + 1);
46
- const pad = safe.startsWith("`") || safe.endsWith("`") || safe.trim() === "" ? " " : "";
47
- return `${fence}${pad}${safe}${pad}${fence}`;
48
- };
49
- /** Render the structured result as a markdown transcript. */
50
- const renderMarkdown = (data) => {
51
- const coexistingLines = data.coexisting.length === 0 ? [] : [
52
- ``,
53
- `## Coexisting prose changesets (untouched by regen)`,
54
- ``,
55
- ...data.coexisting.map((c) => `- ${mdInline(c.file)} (${c.packages.map(mdInline).join(", ")})`)
56
- ];
57
- if (data.packages.length === 0) return [
58
- `# changeset deps detect — ${mdInline(data.root)}`,
59
- ``,
60
- `No dependency changes detected.`
61
- ].concat(coexistingLines).join("\n").trimEnd();
62
- const lines = [`# changeset deps detect — ${mdInline(data.root)}`, ``];
63
- for (const pkg of data.packages) {
64
- lines.push(`## ${mdInline(pkg.package)} — ${mdInline(pkg.relativePath)}`, ``);
65
- lines.push(`| Dependency | Type | Action | From | To |`, `| --- | --- | --- | --- | --- |`);
66
- for (const r of pkg.rows) lines.push(`| ${mdInline(r.dependency)} | ${r.type} | ${r.action} | ${mdInline(r.from)} | ${mdInline(r.to)} |`);
67
- lines.push(``);
68
- }
69
- lines.push(...coexistingLines);
70
- return lines.join("\n").trimEnd();
71
- };
72
- /** One-way transform: result to markdown. Encoding back is forbidden. */
73
- const ChangesetDepsDetectAsMarkdown = ChangesetDepsDetectResult.pipe(Schema.decodeTo(Schema.String, {
74
- decode: SchemaGetter.transform(renderMarkdown),
75
- encode: SchemaGetter.forbidden(() => "ChangesetDepsDetectAsMarkdown is one-way: markdown cannot be parsed back.")
76
- }));
77
- /**
78
34
  * Effect handler: resolve the workspace root, then compute the cumulative
79
35
  * dependency diff via {@link Changesets.DepsRegen.plan} with `includeDevDeps`
80
36
  * so devDependency rows are retained. Maps `plan.toWrite` into the structured
@@ -117,14 +73,14 @@ const REMEDIATION = {
117
73
  };
118
74
  /** The `changeset_deps_detect` tool value. */
119
75
  const changesetDepsDetectTool = Tool.make("changeset_deps_detect", {
120
- description: "Read-only preview of the cumulative dependency diff (merge-base -> working tree) per workspace package. Returns each affected package's resolved dependency-table rows (catalog:/workspace: specifiers resolved per side; devDependencies retained) as the exact rows a pure-dependency changeset would carry, plus a coexisting list of untouched prose-only changesets that reference an in-scope package (informational — no need to re-list .changeset/). Does NOT write or delete any file. Prefer this over shelling out to savvy changeset deps detect.",
76
+ description: "Read-only preview of the cumulative dependency diff (merge-base -> working tree) per workspace package. Returns each affected package's resolved dependency-table rows (catalog:/workspace: specifiers resolved per side; devDependencies retained) as the exact rows a pure-dependency changeset would carry, plus a coexisting list of untouched prose-only changesets that reference an in-scope package (informational — no need to re-list .changeset/). Does NOT write or delete any file. Prefer this over shelling out to savvy changeset deps detect. Returns a typed object in structuredContent (content[] carries the same object as JSON).",
121
77
  parameters: ChangesetDepsDetectParams,
122
78
  success: ChangesetDepsDetectResult,
123
79
  failure: McpToolError,
124
80
  dependencies: [WorkspaceRoot, Changesets.DepsRegen]
125
- }).annotate(Tool.Title, "Detect dependency changesets").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true).annotate(Tool.OpenWorld, false).annotate(SilkMarkdown, Schema.decodeUnknownSync(ChangesetDepsDetectAsMarkdown));
81
+ }).annotate(Tool.Title, "Detect dependency changesets").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true).annotate(Tool.OpenWorld, false);
126
82
  /** Wire handler: {@link changesetDepsDetect} with its error channel mapped onto {@link McpToolError}. */
127
83
  const handleChangesetDepsDetect = (fallbackCwd, params) => changesetDepsDetect(params, fallbackCwd).pipe(Effect.mapError(mapEngineError(params.cwd ?? fallbackCwd, REMEDIATION)));
128
84
 
129
85
  //#endregion
130
- export { ChangesetDepsDetectAsMarkdown, ChangesetDepsDetectCoexisting, ChangesetDepsDetectPackage, ChangesetDepsDetectParams, ChangesetDepsDetectResult, changesetDepsDetect, changesetDepsDetectTool, handleChangesetDepsDetect };
86
+ export { ChangesetDepsDetectCoexisting, ChangesetDepsDetectPackage, ChangesetDepsDetectParams, ChangesetDepsDetectResult, changesetDepsDetect, changesetDepsDetectTool, handleChangesetDepsDetect };
@@ -1,6 +1,5 @@
1
1
  import { McpToolError, mapEngineError } from "../errors.js";
2
- import { SilkMarkdown } from "../markdown.js";
3
- import { Effect, Schema, SchemaGetter } from "effect";
2
+ import { Effect, Schema } from "effect";
4
3
  import { WorkspaceRoot } from "@effected/workspaces";
5
4
  import { Changesets } from "@savvy-web/silk-effects";
6
5
  import { Tool } from "effect/unstable/ai";
@@ -29,51 +28,6 @@ const ChangesetDepsRegenResult = Schema.Struct({
29
28
  description: "Regenerated pure-dependency changesets. Mutates .changeset/*.md unless dryRun is set."
30
29
  });
31
30
  /**
32
- * Render a repo-derived value (path) as an inert markdown code span. Control
33
- * characters are flattened to spaces and the span is fenced with a backtick run
34
- * longer than any in the value — CommonMark forbids backslash-escaping a
35
- * backtick inside a code span.
36
- */
37
- const mdInline = (value) => {
38
- const safe = value.replace(/\p{Cc}/gu, " ");
39
- const longest = safe.match(/`+/g)?.reduce((m, run) => Math.max(m, run.length), 0) ?? 0;
40
- const fence = "`".repeat(longest + 1);
41
- const pad = safe.startsWith("`") || safe.endsWith("`") || safe.trim() === "" ? " " : "";
42
- return `${fence}${pad}${safe}${pad}${fence}`;
43
- };
44
- /** Render the structured result as a markdown transcript. */
45
- const renderMarkdown = (data) => {
46
- const heading = `# changeset deps regen — ${mdInline(data.root)}${data.dryRun ? " (dry run)" : ""}`;
47
- if (data.deleted.length === 0 && data.written.length === 0 && data.skippedMixed.length === 0 && data.coexisting.length === 0) return `${heading}\n\nNo dependency changes to regenerate.`;
48
- const lines = [heading, ``];
49
- const verb = data.dryRun ? "Would delete" : "Deleted";
50
- if (data.deleted.length > 0) {
51
- lines.push(`${verb} ${data.deleted.length} pure dependency changeset(s):`);
52
- for (const file of data.deleted) lines.push(`- ${mdInline(file)}`);
53
- lines.push(``);
54
- }
55
- if (data.written.length > 0) {
56
- lines.push(`${data.dryRun ? "Would write" : "Wrote"} ${data.written.length} fresh dependency changeset(s):`);
57
- for (const file of data.written) lines.push(`- ${mdInline(file)}`);
58
- lines.push(``);
59
- }
60
- if (data.skippedMixed.length > 0) {
61
- lines.push(`Skipped ${data.skippedMixed.length} mixed changeset(s):`);
62
- for (const file of data.skippedMixed) lines.push(`- ${mdInline(file)}`);
63
- lines.push(``);
64
- }
65
- if (data.coexisting.length > 0) {
66
- lines.push(`Coexisting prose changeset(s) for in-scope packages, left untouched:`);
67
- for (const entry of data.coexisting) lines.push(`- ${mdInline(entry.file)} (${entry.packages.map(mdInline).join(", ")})`);
68
- }
69
- return lines.join("\n").trimEnd();
70
- };
71
- /** One-way transform: result to markdown. Encoding back is forbidden. */
72
- const ChangesetDepsRegenAsMarkdown = ChangesetDepsRegenResult.pipe(Schema.decodeTo(Schema.String, {
73
- decode: SchemaGetter.transform(renderMarkdown),
74
- encode: SchemaGetter.forbidden(() => "ChangesetDepsRegenAsMarkdown is one-way: markdown cannot be parsed back.")
75
- }));
76
- /**
77
31
  * Effect handler: resolve the workspace root, compute a {@link Changesets.RegenPlan}
78
32
  * via {@link Changesets.DepsRegen.plan}, then — unless `dryRun` — apply it via
79
33
  * `execute`. On a dry run the reported `deleted`/`written` reflect the plan's
@@ -128,14 +82,14 @@ const REMEDIATION = {
128
82
  };
129
83
  /** The `changeset_deps_regen` tool value. Mutating: not read-only, not idempotent. */
130
84
  const changesetDepsRegenTool = Tool.make("changeset_deps_regen", {
131
- description: "Regenerate pure-dependency changesets: delete stale single-package Dependencies-only changesets and write fresh single-package, patch-bump changesets from the cumulative dependency diff (catalog:/workspace: resolved; devDependencies dropped). Mixed changesets (Dependencies plus other content) are left untouched, and the result's coexisting list accounts for untouched prose-only changesets that reference an in-scope package (informational — no need to re-list .changeset/). Set dryRun=true to preview the plan without touching the filesystem. NOTE: without dryRun this tool MUTATES .changeset/*.md (git-reversible). Prefer this over shelling out to savvy changeset deps regen.",
85
+ description: "Regenerate pure-dependency changesets: delete stale single-package Dependencies-only changesets and write fresh single-package, patch-bump changesets from the cumulative dependency diff (catalog:/workspace: resolved; devDependencies dropped). Mixed changesets (Dependencies plus other content) are left untouched, and the result's coexisting list accounts for untouched prose-only changesets that reference an in-scope package (informational — no need to re-list .changeset/). Set dryRun=true to preview the plan without touching the filesystem. NOTE: without dryRun this tool MUTATES .changeset/*.md (git-reversible). Prefer this over shelling out to savvy changeset deps regen. Returns a typed object in structuredContent (content[] carries the same object as JSON).",
132
86
  parameters: ChangesetDepsRegenParams,
133
87
  success: ChangesetDepsRegenResult,
134
88
  failure: McpToolError,
135
89
  dependencies: [WorkspaceRoot, Changesets.DepsRegen]
136
- }).annotate(Tool.Title, "Regenerate dependency changesets").annotate(Tool.Readonly, false).annotate(Tool.Destructive, true).annotate(Tool.Idempotent, false).annotate(Tool.OpenWorld, false).annotate(SilkMarkdown, Schema.decodeUnknownSync(ChangesetDepsRegenAsMarkdown));
90
+ }).annotate(Tool.Title, "Regenerate dependency changesets").annotate(Tool.Readonly, false).annotate(Tool.Destructive, true).annotate(Tool.Idempotent, false).annotate(Tool.OpenWorld, false);
137
91
  /** Wire handler: {@link changesetDepsRegen} with its error channel mapped onto {@link McpToolError}. */
138
92
  const handleChangesetDepsRegen = (fallbackCwd, params) => changesetDepsRegen(params, fallbackCwd).pipe(Effect.mapError(mapEngineError(params.cwd ?? fallbackCwd, REMEDIATION)));
139
93
 
140
94
  //#endregion
141
- export { ChangesetCoexistingEntry, ChangesetDepsRegenAsMarkdown, ChangesetDepsRegenParams, ChangesetDepsRegenResult, changesetDepsRegen, changesetDepsRegenTool, handleChangesetDepsRegen };
95
+ export { ChangesetCoexistingEntry, ChangesetDepsRegenParams, ChangesetDepsRegenResult, changesetDepsRegen, changesetDepsRegenTool, handleChangesetDepsRegen };
@@ -1,6 +1,5 @@
1
1
  import { McpToolError, mapEngineError } from "../errors.js";
2
- import { SilkMarkdown } from "../markdown.js";
3
- import { Effect, Schema, SchemaGetter } from "effect";
2
+ import { Effect, Schema } from "effect";
4
3
  import { WorkspaceRoot } from "@effected/workspaces";
5
4
  import { Changesets } from "@savvy-web/silk-effects";
6
5
  import { Tool } from "effect/unstable/ai";
@@ -32,79 +31,6 @@ const ChangesetInspectResult = Schema.Union([
32
31
  description: "Read-only changeset analysis grouped by mode (branch | config | classify)."
33
32
  });
34
33
  /**
35
- * Render a repo/config-derived value as an inert markdown code span. Escapes
36
- * backticks and backslashes so a crafted filename or package name cannot inject
37
- * markdown structure into the transcript that an agent reads.
38
- */
39
- const mdInline = (value) => `\`${value.replace(/[`\\]/g, "\\$&")}\``;
40
- /** Render the structured result as a markdown transcript. */
41
- const renderMarkdown = (data) => {
42
- switch (data.mode) {
43
- case "branch": {
44
- const r = data.result;
45
- const lines = [
46
- `# changeset branch analysis — base ${mdInline(r.baseBranch)}`,
47
- ``,
48
- `merge base: ${mdInline(r.mergeBaseSha)}`,
49
- ``,
50
- `## Packages affected`,
51
- r.packagesAffected.map((p) => `- ${mdInline(p)}`).join("\n") || "(none)",
52
- ``,
53
- `## Files`
54
- ];
55
- for (const f of r.files) {
56
- const owner = f.package ? mdInline(f.package) : mdInline("<unmapped>");
57
- lines.push(`- ${mdInline(f.status)} ${mdInline(f.path)} -> ${owner}`);
58
- }
59
- if (r.unmappedFiles.length > 0) {
60
- const hints = /* @__PURE__ */ new Map();
61
- for (const f of r.files) if (typeof f.reason === "object" && f.reason !== null && f.reason.kind === "unmappedHint") hints.set(f.path, f.reason.hint);
62
- lines.push(``, `## Unmapped (ask the user)`);
63
- for (const p of r.unmappedFiles) {
64
- const hint = hints.get(p);
65
- lines.push(hint === void 0 ? `- ${mdInline(p)}` : `- ${mdInline(p)} — ${mdInline(hint)}`);
66
- }
67
- }
68
- return lines.join("\n");
69
- }
70
- case "config": {
71
- const r = data.result;
72
- const lines = [
73
- `# changeset config — ${mdInline(r.configPath)}`,
74
- ``,
75
- `base branch: ${mdInline(r.baseBranch)}`,
76
- `access: ${r.access}`,
77
- `changelog: ${r.changelog ? mdInline(r.changelog) : "(none)"}`,
78
- `ignored: ${r.ignore.map(mdInline).join(", ") || "(none)"}`,
79
- ``,
80
- `## Packages`
81
- ];
82
- for (const p of r.packages) {
83
- lines.push(p.version === void 0 ? `### ${mdInline(p.name)}` : `### ${mdInline(p.name)} (${mdInline(p.version)})`, `- dir: ${mdInline(p.workspaceDir)}`);
84
- if (p.additionalScopes.length > 0) lines.push(`- additionalScopes: ${p.additionalScopes.map(mdInline).join(", ")}`);
85
- if (p.versionFiles.length > 0) lines.push(`- versionFiles: ${p.versionFiles.map((v) => mdInline(v.glob)).join(", ")}`);
86
- }
87
- if (r.packages.length === 0) lines.push("(none resolved)");
88
- return lines.join("\n");
89
- }
90
- case "classify": {
91
- const lines = [`# changeset classify`, ``];
92
- for (const c of data.result) {
93
- const owner = c.package ? mdInline(c.package) : mdInline("<unmapped>");
94
- const hint = typeof c.reason === "object" && c.reason !== null && c.reason.kind === "unmappedHint" ? ` — ${mdInline(c.reason.hint)}` : "";
95
- lines.push(`- ${mdInline(c.path)} -> ${owner}${hint}`);
96
- }
97
- if (data.result.length === 0) lines.push("(no paths)");
98
- return lines.join("\n");
99
- }
100
- }
101
- };
102
- /** One-way transform: result to markdown. Encoding back is forbidden. */
103
- const ChangesetInspectAsMarkdown = ChangesetInspectResult.pipe(Schema.decodeTo(Schema.String, {
104
- decode: SchemaGetter.transform(renderMarkdown),
105
- encode: SchemaGetter.forbidden(() => "ChangesetInspectAsMarkdown is one-way: markdown cannot be parsed back.")
106
- }));
107
- /**
108
34
  * Effect handler: resolve the workspace root, then dispatch to the matching
109
35
  * Changesets service keyed by `mode`. Mirrors `turboInspect`.
110
36
  */
@@ -144,7 +70,7 @@ const REMEDIATION = {
144
70
  };
145
71
  /** The `changeset_inspect` tool value. */
146
72
  const changesetInspectTool = Tool.make("changeset_inspect", {
147
- description: "Read-only changeset analysis for the changeset-manager workflow. mode=branch diffs the current branch against its base and classifies every changed file by owning package (with packagesAffected and the unmapped paths to ask the user about; an unmapped path may carry a machine-readable unmappedHint reason — e.g. a deleted versionFiles/additionalScopes target or a known template mirror — meaning it is probably already accounted for). mode=config surfaces the resolved .changeset/config.json (release surfaces, versionFiles, ignore list). mode=classify maps arbitrary repo-relative paths to their owning package. Prefer this over shelling out to the savvy CLI.",
73
+ description: "Read-only changeset analysis for the changeset-manager workflow. mode=branch diffs the current branch against its base and classifies every changed file by owning package (with packagesAffected and the unmapped paths to ask the user about; an unmapped path may carry a machine-readable unmappedHint reason — e.g. a deleted versionFiles/additionalScopes target or a known template mirror — meaning it is probably already accounted for). mode=config surfaces the resolved .changeset/config.json (release surfaces, versionFiles, ignore list). mode=classify maps arbitrary repo-relative paths to their owning package. Prefer this over shelling out to the savvy CLI. Returns a typed object in structuredContent (content[] carries the same object as JSON).",
148
74
  parameters: ChangesetInspectParams,
149
75
  success: ChangesetInspectResult,
150
76
  failure: McpToolError,
@@ -153,9 +79,9 @@ const changesetInspectTool = Tool.make("changeset_inspect", {
153
79
  Changesets.ConfigInspector,
154
80
  WorkspaceRoot
155
81
  ]
156
- }).annotate(Tool.Title, "Inspect changesets").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true).annotate(Tool.OpenWorld, false).annotate(SilkMarkdown, Schema.decodeUnknownSync(ChangesetInspectAsMarkdown));
82
+ }).annotate(Tool.Title, "Inspect changesets").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true).annotate(Tool.OpenWorld, false);
157
83
  /** Wire handler: {@link changesetInspect} with its error channel mapped onto {@link McpToolError}. */
158
84
  const handleChangesetInspect = (fallbackCwd, params) => changesetInspect(params, fallbackCwd).pipe(Effect.mapError(mapEngineError(params.cwd ?? fallbackCwd, REMEDIATION)));
159
85
 
160
86
  //#endregion
161
- export { ChangesetBranchResult, ChangesetClassifyResult, ChangesetConfigResult, ChangesetInspectAsMarkdown, ChangesetInspectParams, ChangesetInspectResult, changesetInspect, changesetInspectTool, handleChangesetInspect };
87
+ export { ChangesetBranchResult, ChangesetClassifyResult, ChangesetConfigResult, ChangesetInspectParams, ChangesetInspectResult, changesetInspect, changesetInspectTool, handleChangesetInspect };
@@ -1,6 +1,5 @@
1
1
  import { McpToolError, mapEngineError } from "../errors.js";
2
- import { SilkMarkdown } from "../markdown.js";
3
- import { Effect, Schema, SchemaGetter } from "effect";
2
+ import { Effect, Schema } from "effect";
4
3
  import { WorkspaceRoot } from "@effected/workspaces";
5
4
  import { Changesets } from "@savvy-web/silk-effects";
6
5
  import { Tool } from "effect/unstable/ai";
@@ -12,28 +11,6 @@ const ChangesetPreviewResult = Changesets.ChangesetPreviewSchema.annotate({
12
11
  title: "changeset_preview result",
13
12
  description: "Read-only preview of the next release: version bumps + rendered CHANGELOG blocks."
14
13
  });
15
- /** Render a value as an inert markdown code span (escapes backticks/backslashes). */
16
- const mdInline = (value) => `\`${value.replace(/[`\\]/g, "\\$&")}\``;
17
- /** Render the structured preview as a markdown transcript. */
18
- const renderMarkdown = (data) => {
19
- if (data.releases.length === 0) return "# changeset preview\n\nNo pending changesets.";
20
- const lines = [
21
- `# changeset preview${data.preMode ? ` (pre: ${data.preMode})` : ""}`,
22
- ``,
23
- `## Version bumps`,
24
- ``
25
- ];
26
- lines.push(`| Package | Old | New | Bump |`, `| --- | --- | --- | --- |`);
27
- for (const r of data.releases) lines.push(`| ${mdInline(r.name)} | ${r.oldVersion} | ${r.newVersion} | ${r.type} |`);
28
- lines.push(``, `## Release notes`, ``);
29
- for (const r of data.releases) lines.push(`### ${mdInline(r.name)}`, ``, r.changelogEntry, ``);
30
- return lines.join("\n").trimEnd();
31
- };
32
- /** One-way transform: result to markdown. Encoding back is forbidden. */
33
- const ChangesetPreviewAsMarkdown = ChangesetPreviewResult.pipe(Schema.decodeTo(Schema.String, {
34
- decode: SchemaGetter.transform(renderMarkdown),
35
- encode: SchemaGetter.forbidden(() => "ChangesetPreviewAsMarkdown is one-way: markdown cannot be parsed back.")
36
- }));
37
14
  /**
38
15
  * Effect handler: resolve the workspace root, then render the preview via
39
16
  * ReleasePlanner. Mirrors `changesetInspect`.
@@ -50,14 +27,14 @@ const REMEDIATION = {
50
27
  };
51
28
  /** The `changeset_preview` tool value. */
52
29
  const changesetPreviewTool = Tool.make("changeset_preview", {
53
- description: "Read-only preview of the next release. Runs the genuine changesets engine over the pending changesets and returns each package's version bump (old -> new) plus the rendered CHANGELOG block (dependency tables included), exactly as it would ship. Does not modify the repo. Prefer this over hand-merging changeset files.",
30
+ description: "Read-only preview of the next release. Runs the genuine changesets engine over the pending changesets and returns each package's version bump (old -> new) plus the rendered CHANGELOG block (dependency tables included), exactly as it would ship. Does not modify the repo. Prefer this over hand-merging changeset files. Returns a typed object in structuredContent (content[] carries the same object as JSON).",
54
31
  parameters: ChangesetPreviewParams,
55
32
  success: ChangesetPreviewResult,
56
33
  failure: McpToolError,
57
34
  dependencies: [Changesets.ReleasePlanner, WorkspaceRoot]
58
- }).annotate(Tool.Title, "Preview the next release").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true).annotate(Tool.OpenWorld, false).annotate(SilkMarkdown, Schema.decodeUnknownSync(ChangesetPreviewAsMarkdown));
35
+ }).annotate(Tool.Title, "Preview the next release").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true).annotate(Tool.OpenWorld, false);
59
36
  /** Wire handler: {@link changesetPreview} with its error channel mapped onto {@link McpToolError}. */
60
37
  const handleChangesetPreview = (fallbackCwd, params) => changesetPreview(params, fallbackCwd).pipe(Effect.mapError(mapEngineError(params.cwd ?? fallbackCwd, REMEDIATION)));
61
38
 
62
39
  //#endregion
63
- export { ChangesetPreviewAsMarkdown, ChangesetPreviewParams, ChangesetPreviewResult, changesetPreview, changesetPreviewTool, handleChangesetPreview };
40
+ export { ChangesetPreviewParams, ChangesetPreviewResult, changesetPreview, changesetPreviewTool, handleChangesetPreview };