@savvy-web/mcp 0.5.0 → 1.1.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": "
|
|
3
|
+
"version": "1.1.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",
|
|
@@ -30,10 +30,13 @@
|
|
|
30
30
|
"savvy-mcp": "bin/savvy-mcp.js"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
+
"@effect/cluster": "^0.59.0",
|
|
33
34
|
"@effect/platform": "^0.96.1",
|
|
34
35
|
"@effect/platform-node": "^0.107.0",
|
|
36
|
+
"@effect/rpc": "^0.75.1",
|
|
37
|
+
"@effect/sql": "^0.51.1",
|
|
35
38
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
36
|
-
"@savvy-web/silk-effects": "1.
|
|
39
|
+
"@savvy-web/silk-effects": "1.2.0",
|
|
37
40
|
"effect": "^3.21.3",
|
|
38
41
|
"fuse.js": "^7.4.0",
|
|
39
42
|
"workspaces-effect": "^1.2.0",
|
|
@@ -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
|
|
33
|
-
|
|
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([
|
|
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 };
|