@savvy-web/mcp 0.5.0 → 1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savvy-web/mcp",
3
- "version": "0.5.0",
3
+ "version": "1.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",
@@ -29,9 +29,8 @@ savvy init orchestrator → changeset · commit · lint init in one pass
29
29
  savvy check orchestrator → runs all three checks
30
30
  savvy commit init · check · hook(session-start · pre-commit-message ·
31
31
  post-commit-verify · user-prompt-submit)
32
- savvy changeset init · check · lint · transform · validate-file · version ·
33
- classify · analyze-branch · release-surface ·
34
- config(show · validate) · deps(detect · regen)
32
+ savvy changeset lint · check · transform · validate-file · version ·
33
+ config(validate) · deps(detect · regen)
35
34
  savvy lint init · check · fmt(package-json · pnpm-workspace · yaml)
36
35
  ```
37
36
 
package/server.js CHANGED
@@ -3,6 +3,7 @@ import { stderrQueryLogger } from "./resources/query-log.js";
3
3
  import { effectToZodSchema } from "./schema/effect-to-zod.js";
4
4
  import { BiomeCheckAsMarkdown, BiomeCheckResult, runBiomeCheck } from "./tools/biome-check.js";
5
5
  import { ChangesetInspectAsMarkdown, ChangesetInspectResult, changesetInspect } from "./tools/changeset-inspect.js";
6
+ import { ChangesetValidateAsMarkdown, ChangesetValidateResult, changesetValidate } from "./tools/changeset-validate.js";
6
7
  import { DocsSearchResult, DocsSearchResultAsMarkdown, runDocsSearch } from "./tools/docs-search.js";
7
8
  import { TurboInspectAsMarkdown, TurboInspectResult, turboInspect } from "./tools/turbo-inspect.js";
8
9
  import { WorkspaceInfoAsMarkdown, WorkspaceInfoResult, workspaceInfo } from "./tools/workspace-info.js";
@@ -81,10 +82,15 @@ function buildServer(ctx) {
81
82
  return structuredResult(Schema.decodeSync(TurboInspectAsMarkdown)(data), data);
82
83
  });
83
84
  server.registerTool("changeset_inspect", {
84
- 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). mode=config surfaces the resolved .changeset/config.json (release surfaces, versionFiles, ignore list). Prefer this over shelling out to the savvy CLI.",
85
+ 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). 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.",
85
86
  inputSchema: {
86
- mode: z.enum(["branch", "config"]).describe("Which inspection to run."),
87
+ mode: z.enum([
88
+ "branch",
89
+ "config",
90
+ "classify"
91
+ ]).describe("Which inspection to run."),
87
92
  base: z.optional(z.string()).describe("Override the base branch (branch mode only)."),
93
+ paths: z.optional(z.array(z.string())).describe("Paths to classify (classify mode only)."),
88
94
  cwd: z.optional(z.string()).describe("Directory to resolve the workspace root from.")
89
95
  },
90
96
  outputSchema: effectToZodSchema(ChangesetInspectResult),
@@ -93,6 +99,18 @@ function buildServer(ctx) {
93
99
  const data = await ctx.runtime.runPromise(changesetInspect(args, ctx.cwd));
94
100
  return structuredResult(Schema.decodeSync(ChangesetInspectAsMarkdown)(data), data);
95
101
  });
102
+ server.registerTool("changeset_validate", {
103
+ description: "Read-only validation of changeset files against the section-aware rules. Pass dir (default .changeset). Returns typed diagnostics (file, rule, line, column, message) plus ok/errorCount in structuredContent. Prefer this over shelling out to savvy changeset lint.",
104
+ inputSchema: {
105
+ dir: z.optional(z.string()).describe("Changeset directory to validate (default .changeset)."),
106
+ cwd: z.optional(z.string()).describe("Directory to resolve the workspace root from.")
107
+ },
108
+ outputSchema: effectToZodSchema(ChangesetValidateResult),
109
+ annotations: { readOnlyHint: true }
110
+ }, async (args) => {
111
+ const data = await ctx.runtime.runPromise(changesetValidate(args, ctx.cwd));
112
+ return structuredResult(Schema.decodeSync(ChangesetValidateAsMarkdown)(data), data);
113
+ });
96
114
  server.registerTool("biome_check", {
97
115
  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). 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).",
98
116
  inputSchema: {
@@ -21,11 +21,16 @@ const ChangesetConfigResult = Schema.Struct({
21
21
  mode: Schema.Literal("config"),
22
22
  result: Changesets.InspectedConfigSchema
23
23
  }).annotations({ identifier: "ChangesetConfigResult" });
24
+ /** Classify variant — arbitrary paths to owning package. */
25
+ const ChangesetClassifyResult = Schema.Struct({
26
+ mode: Schema.Literal("classify"),
27
+ result: Schema.Array(Changesets.ClassificationSchema)
28
+ }).annotations({ identifier: "ChangesetClassifyResult" });
24
29
  /** The `changeset_inspect` tool result — a discriminated union keyed by `mode`. */
25
- const ChangesetInspectResult = Schema.Union(ChangesetBranchResult, ChangesetConfigResult).annotations({
30
+ const ChangesetInspectResult = Schema.Union(ChangesetBranchResult, ChangesetConfigResult, ChangesetClassifyResult).annotations({
26
31
  identifier: "ChangesetInspectResult",
27
32
  title: "changeset_inspect result",
28
- description: "Read-only changeset analysis grouped by mode (branch | config)."
33
+ description: "Read-only changeset analysis grouped by mode (branch | config | classify)."
29
34
  });
30
35
  /**
31
36
  * Render a repo/config-derived value as an inert markdown code span. Escapes
@@ -49,7 +54,7 @@ const renderMarkdown = (data) => {
49
54
  `## Files`
50
55
  ];
51
56
  for (const f of r.files) {
52
- const owner = f.package ? mdInline(f.package) : "<unmapped>";
57
+ const owner = f.package ? mdInline(f.package) : mdInline("<unmapped>");
53
58
  lines.push(`- ${mdInline(f.status)} ${mdInline(f.path)} -> ${owner}`);
54
59
  }
55
60
  if (r.unmappedFiles.length > 0) {
@@ -78,6 +83,15 @@ const renderMarkdown = (data) => {
78
83
  if (r.packages.length === 0) lines.push("(none resolved)");
79
84
  return lines.join("\n");
80
85
  }
86
+ case "classify": {
87
+ const lines = [`# changeset classify`, ``];
88
+ for (const c of data.result) {
89
+ const owner = c.package ? mdInline(c.package) : mdInline("<unmapped>");
90
+ lines.push(`- ${mdInline(c.path)} -> ${owner}`);
91
+ }
92
+ if (data.result.length === 0) lines.push("(no paths)");
93
+ return lines.join("\n");
94
+ }
81
95
  }
82
96
  };
83
97
  /** One-way transform: result to markdown. Encoding back is forbidden. */
@@ -101,6 +115,10 @@ const changesetInspect = (args, fallbackCwd) => Effect.gen(function* () {
101
115
  mode: "config",
102
116
  result: yield* (yield* Changesets.ConfigInspector).inspect(root)
103
117
  };
118
+ case "classify": return {
119
+ mode: "classify",
120
+ result: yield* (yield* Changesets.ConfigInspector).classify(root, args.paths ?? [])
121
+ };
104
122
  }
105
123
  });
106
124
 
@@ -0,0 +1,84 @@
1
+ import { Changesets } from "@savvy-web/silk-effects";
2
+ import { Data, Effect, ParseResult, Schema } from "effect";
3
+ import { WorkspaceRoot } from "workspaces-effect";
4
+ import { resolve } from "node:path";
5
+
6
+ //#region src/tools/changeset-validate.ts
7
+ /**
8
+ * The `changeset_validate` MCP tool: structured changeset-file validation over
9
+ * silk-effects' pure ChangesetLinter, returning typed diagnostics plus a
10
+ * pass/fail summary. Read-only.
11
+ *
12
+ * @packageDocumentation
13
+ */
14
+ /** A thrown failure from the pure {@link Changesets.ChangesetLinter.validate} (e.g. a missing directory). */
15
+ var ChangesetValidateError = class extends Data.TaggedError("ChangesetValidateError") {};
16
+ /** A single changeset lint diagnostic. Mirrors silk-effects' `LintMessage`. */
17
+ const ChangesetLintMessage = Schema.Struct({
18
+ file: Schema.String,
19
+ rule: Schema.String,
20
+ line: Schema.Number,
21
+ column: Schema.Number,
22
+ message: Schema.String
23
+ }).annotations({ identifier: "ChangesetLintMessage" });
24
+ /** The `changeset_validate` tool result. */
25
+ const ChangesetValidateResult = Schema.Struct({
26
+ dir: Schema.String,
27
+ ok: Schema.Boolean,
28
+ errorCount: Schema.Number,
29
+ messages: Schema.Array(ChangesetLintMessage)
30
+ }).annotations({
31
+ identifier: "ChangesetValidateResult",
32
+ title: "changeset_validate result",
33
+ description: "Read-only validation of changeset files against the section-aware rules."
34
+ });
35
+ /**
36
+ * Render a repo/lint-derived value as an inert markdown code span. Escapes
37
+ * backticks and backslashes so a crafted filename or message cannot inject
38
+ * markdown structure into the transcript that an agent reads.
39
+ */
40
+ const mdInline = (value) => `\`${value.replace(/[`\\]/g, "\\$&")}\``;
41
+ /** Render the structured result as a markdown transcript. */
42
+ const renderMarkdown = (data) => {
43
+ if (data.ok) return `# changeset validate — ${mdInline(data.dir)}\n\nNo changeset issues found.`;
44
+ const lines = [
45
+ `# changeset validate — ${mdInline(data.dir)}`,
46
+ ``,
47
+ `${data.errorCount} issue(s):`,
48
+ ``
49
+ ];
50
+ for (const m of data.messages) lines.push(`- ${mdInline(`${m.file}:${m.line}:${m.column}`)} ${mdInline(m.rule)} — ${m.message}`);
51
+ return lines.join("\n");
52
+ };
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
+ });
59
+ /**
60
+ * Effect handler: resolve the workspace root, then validate the changeset
61
+ * directory via the pure {@link Changesets.ChangesetLinter.validate}. The
62
+ * synchronous call is wrapped in {@link Effect.try} so a thrown error (e.g. a
63
+ * missing directory) surfaces as a typed {@link ChangesetValidateError} rather
64
+ * than escaping as a defect.
65
+ */
66
+ const changesetValidate = (args, fallbackCwd) => Effect.gen(function* () {
67
+ const dir = resolve(yield* (yield* WorkspaceRoot).find(args.cwd ?? fallbackCwd), args.dir ?? ".changeset");
68
+ const messages = yield* Effect.try({
69
+ try: () => Changesets.ChangesetLinter.validate(dir),
70
+ catch: (cause) => new ChangesetValidateError({
71
+ dir,
72
+ cause
73
+ })
74
+ });
75
+ return {
76
+ dir,
77
+ ok: messages.length === 0,
78
+ errorCount: messages.length,
79
+ messages
80
+ };
81
+ });
82
+
83
+ //#endregion
84
+ export { ChangesetValidateAsMarkdown, ChangesetValidateResult, changesetValidate };