@savvy-web/mcp 2.7.5 → 3.0.1
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/README.md +8 -3
- package/bin/savvy-mcp.js +4 -28
- package/errors.js +181 -0
- package/index.d.ts +695 -18
- package/index.js +5 -2
- package/internal/project-root.js +25 -0
- package/main.d.ts +25 -0
- package/main.js +42 -0
- package/markdown.js +32 -0
- package/package.json +10 -7
- package/runtime.js +3 -3
- package/server.js +119 -234
- package/toolkit.js +44 -0
- package/tools/biome-check.js +64 -11
- package/tools/changeset-deps-detect.js +27 -2
- package/tools/changeset-deps-regen.js +28 -2
- package/tools/changeset-inspect.js +34 -2
- package/tools/changeset-preview.js +21 -2
- package/tools/changeset-validate.js +28 -2
- package/tools/repos-inspect.js +38 -2
- package/tools/repos-manage.js +58 -2
- package/tools/turbo-inspect.js +27 -2
- package/tools/workspace-info.js +25 -2
- package/version.js +1 -1
- package/schema/effect-to-zod.js +0 -205
package/README.md
CHANGED
|
@@ -32,13 +32,18 @@ The server speaks MCP over stdio and is meant to be spawned by an MCP client. A
|
|
|
32
32
|
|
|
33
33
|
The single positional argument is the project directory; if omitted, the server resolves it from `SAVVY_MCP_PROJECT_DIR`, then `CLAUDE_PROJECT_DIR`, then the current working directory.
|
|
34
34
|
|
|
35
|
-
To exercise it by hand
|
|
35
|
+
To exercise it by hand, pipe JSON-RPC to it over stdio. Every response is one JSON line on stdout, logs go to stderr, and the server exits 0 when stdin closes. Keep stdin open until you have read the response you want — a request still in flight when stdin closes is dropped:
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
(printf '%s\n' \
|
|
39
|
+
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"probe","version":"0.0.0"}}}' \
|
|
40
|
+
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
|
|
41
|
+
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}'; sleep 2) | savvy-mcp .
|
|
42
|
+
# line 1: the initialize result; line 2: the ten tools
|
|
40
43
|
```
|
|
41
44
|
|
|
45
|
+
Any generic MCP client works the same way; nothing extra ships with the package for interactive use.
|
|
46
|
+
|
|
42
47
|
## Tools
|
|
43
48
|
|
|
44
49
|
- `workspace_info` — returns a flat, structured projection of the workspace analysis: linked and fixed package groups as name arrays plus resolved registry targets. Backed by the same `silk-effects` analyzer the `savvy` CLI uses.
|
package/bin/savvy-mcp.js
CHANGED
|
@@ -1,38 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { startMcpServer } from "../server.js";
|
|
4
|
-
import { Layer, ManagedRuntime } from "effect";
|
|
5
|
-
import { NodeServices } from "@effect/platform-node";
|
|
2
|
+
import { main } from "../main.js";
|
|
6
3
|
|
|
7
4
|
//#region src/bin.ts
|
|
8
5
|
/**
|
|
9
|
-
* Binary entrypoint for the `savvy-mcp` server.
|
|
10
|
-
*
|
|
11
|
-
* Resolves the project working directory, builds the long-lived runtime
|
|
12
|
-
* (root-bound to that directory), and starts the MCP server over stdio.
|
|
6
|
+
* Binary entrypoint for the `savvy-mcp` server. Owns nothing itself — see
|
|
7
|
+
* `main.ts` for the process bootstrap.
|
|
13
8
|
*
|
|
14
9
|
* @internal
|
|
15
10
|
*/
|
|
16
|
-
|
|
17
|
-
function resolveProjectDir() {
|
|
18
|
-
const argv = process.argv[2];
|
|
19
|
-
return (argv !== void 0 && argv.trim().length > 0 && !(argv.startsWith("${") && argv.endsWith("}")) ? argv.trim() : void 0) ?? process.env.SAVVY_MCP_PROJECT_DIR ?? process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
|
|
20
|
-
}
|
|
21
|
-
async function main() {
|
|
22
|
-
const cwd = resolveProjectDir();
|
|
23
|
-
const appLayer = makeSilkRuntimeLayer(cwd).pipe(Layer.provide(NodeServices.layer));
|
|
24
|
-
const ctx = {
|
|
25
|
-
runtime: ManagedRuntime.make(appLayer),
|
|
26
|
-
cwd
|
|
27
|
-
};
|
|
28
|
-
process.stderr.write(`[savvy-mcp] starting in ${cwd}\n`);
|
|
29
|
-
await startMcpServer(ctx);
|
|
30
|
-
}
|
|
31
|
-
main().catch((err) => {
|
|
32
|
-
process.stderr.write(`savvy-mcp: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
|
|
33
|
-
process.exit(1);
|
|
34
|
-
});
|
|
35
|
-
/* v8 ignore stop */
|
|
11
|
+
await main();
|
|
36
12
|
|
|
37
13
|
//#endregion
|
|
38
14
|
export { };
|
package/errors.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/errors.ts
|
|
4
|
+
/**
|
|
5
|
+
* The one typed failure union every savvy-mcp tool declares, plus the helpers
|
|
6
|
+
* that keep each member's `message` self-contained on the wire.
|
|
7
|
+
*
|
|
8
|
+
* @remarks
|
|
9
|
+
* Under `failureMode: "error"` (the only mode these tools use) a declared
|
|
10
|
+
* typed failure reaches the client as
|
|
11
|
+
* `{ isError: true, content: [{ type: "text", text: error.message }] }` and
|
|
12
|
+
* `structuredContent` is never populated for it
|
|
13
|
+
* (`.repos/effect/packages/effect/src/unstable/ai/McpServer.ts:1513-1517,1592-1607`
|
|
14
|
+
* at rc.115 — see the header of `server.ts`). So a structured `remediation`
|
|
15
|
+
* field is invisible to a real client: every member folds its hint into
|
|
16
|
+
* `message` at construction through {@link composeRemediatedMessage}, and any
|
|
17
|
+
* caller-supplied value echoed back passes through {@link truncateEchoed}.
|
|
18
|
+
*
|
|
19
|
+
* @packageDocumentation
|
|
20
|
+
*/
|
|
21
|
+
/** What a caller should do next about a failed tool call. @public */
|
|
22
|
+
const Remediation = Schema.Struct({
|
|
23
|
+
hint: Schema.String,
|
|
24
|
+
suggestedTool: Schema.optionalKey(Schema.String)
|
|
25
|
+
});
|
|
26
|
+
/**
|
|
27
|
+
* Compose a self-contained wire message from a raw cause message and its
|
|
28
|
+
* remediation: the human message, then the hint, then `Try <suggestedTool>.`
|
|
29
|
+
* when one is present. Every {@link McpToolError} member's `message` is built
|
|
30
|
+
* through this at construction, because `message` is the only field a
|
|
31
|
+
* declared failure delivers to the wire.
|
|
32
|
+
*
|
|
33
|
+
* @public
|
|
34
|
+
*/
|
|
35
|
+
const composeRemediatedMessage = (message, remediation) => remediation.suggestedTool === void 0 ? `${message} ${remediation.hint}` : `${message} ${remediation.hint} Try ${remediation.suggestedTool}.`;
|
|
36
|
+
const ECHO_LIMIT = 200;
|
|
37
|
+
/**
|
|
38
|
+
* The limit for an ENGINE message echoed through {@link engineError} or an
|
|
39
|
+
* argument-decode message: wider than {@link truncateEchoed}'s default because
|
|
40
|
+
* the engine's own rendering (a git stderr, a decode path) is the diagnostic,
|
|
41
|
+
* but still bounded — the kit embeds caller values (`base`, `task`, the
|
|
42
|
+
* decoded value) in those messages untruncated.
|
|
43
|
+
*/
|
|
44
|
+
const ENGINE_ECHO_LIMIT = 2e3;
|
|
45
|
+
/**
|
|
46
|
+
* Truncate a caller-supplied value before it is echoed back inside an error's
|
|
47
|
+
* `message`. Without this a pathological argument (a multi-megabyte `cwd`,
|
|
48
|
+
* say) is echoed once in the response's `content[0].text` and once more in the
|
|
49
|
+
* corresponding log line, burning an agent's context twice on what is usually
|
|
50
|
+
* a typo.
|
|
51
|
+
*
|
|
52
|
+
* @public
|
|
53
|
+
*/
|
|
54
|
+
const truncateEchoed = (value, limit = ECHO_LIMIT) => value.length > limit ? `${value.slice(0, limit)}…` : value;
|
|
55
|
+
/**
|
|
56
|
+
* No workspace root was found walking up from the requested directory.
|
|
57
|
+
* `message` is composed through {@link composeRemediatedMessage} at
|
|
58
|
+
* construction, so it is what reaches the wire as `content[0].text`.
|
|
59
|
+
*
|
|
60
|
+
* @public
|
|
61
|
+
*/
|
|
62
|
+
var WorkspaceNotFound = class extends Schema.TaggedError()("WorkspaceNotFound", {
|
|
63
|
+
cwd: Schema.String,
|
|
64
|
+
message: Schema.String,
|
|
65
|
+
remediation: Remediation
|
|
66
|
+
}) {};
|
|
67
|
+
/**
|
|
68
|
+
* A tool argument was structurally acceptable but semantically invalid — a
|
|
69
|
+
* `repos_manage` action missing the field it needs, a `biome_check` path
|
|
70
|
+
* outside the workspace, a `changeset_validate` directory that does not
|
|
71
|
+
* exist. `message` is composed through {@link composeRemediatedMessage}.
|
|
72
|
+
*
|
|
73
|
+
* @public
|
|
74
|
+
*/
|
|
75
|
+
var InvalidArgument = class extends Schema.TaggedError()("InvalidArgument", {
|
|
76
|
+
argument: Schema.String,
|
|
77
|
+
message: Schema.String,
|
|
78
|
+
remediation: Remediation
|
|
79
|
+
}) {};
|
|
80
|
+
/**
|
|
81
|
+
* A silk-effects engine program failed with one of its own typed errors
|
|
82
|
+
* (`TurboError`, `GitError`, `ReposConfigError`, …). `source` carries that
|
|
83
|
+
* error's `_tag`; `message` is its own rendering plus the remediation,
|
|
84
|
+
* composed through {@link composeRemediatedMessage}.
|
|
85
|
+
*
|
|
86
|
+
* @public
|
|
87
|
+
*/
|
|
88
|
+
var EngineError = class extends Schema.TaggedError()("EngineError", {
|
|
89
|
+
source: Schema.String,
|
|
90
|
+
message: Schema.String,
|
|
91
|
+
remediation: Remediation
|
|
92
|
+
}) {};
|
|
93
|
+
/**
|
|
94
|
+
* No Biome binary could be located. `message` is composed through
|
|
95
|
+
* {@link composeRemediatedMessage}.
|
|
96
|
+
*
|
|
97
|
+
* @public
|
|
98
|
+
*/
|
|
99
|
+
var BiomeUnavailable = class extends Schema.TaggedError()("BiomeUnavailable", {
|
|
100
|
+
message: Schema.String,
|
|
101
|
+
remediation: Remediation
|
|
102
|
+
}) {};
|
|
103
|
+
/**
|
|
104
|
+
* Biome itself failed (exit status above 1, a spawn error, or a timeout) —
|
|
105
|
+
* distinct from "lint issues found", which is a successful result. `message`
|
|
106
|
+
* is composed through {@link composeRemediatedMessage}.
|
|
107
|
+
*
|
|
108
|
+
* @public
|
|
109
|
+
*/
|
|
110
|
+
var BiomeFailed = class extends Schema.TaggedError()("BiomeFailed", {
|
|
111
|
+
exitCode: Schema.optionalKey(Schema.Number),
|
|
112
|
+
message: Schema.String,
|
|
113
|
+
remediation: Remediation
|
|
114
|
+
}) {};
|
|
115
|
+
/** The one failure schema every savvy-mcp tool declares. @public */
|
|
116
|
+
const McpToolError = Schema.Union([
|
|
117
|
+
WorkspaceNotFound,
|
|
118
|
+
InvalidArgument,
|
|
119
|
+
EngineError,
|
|
120
|
+
BiomeUnavailable,
|
|
121
|
+
BiomeFailed
|
|
122
|
+
]);
|
|
123
|
+
/** The remediation every `WorkspaceNotFound` carries. */
|
|
124
|
+
const WORKSPACE_REMEDIATION = {
|
|
125
|
+
hint: "Pass a cwd inside the project (a directory at or below a package.json with a workspace manifest), or omit cwd to use the server's project directory.",
|
|
126
|
+
suggestedTool: "workspace_info"
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Build a {@link WorkspaceNotFound} for the directory the caller asked about.
|
|
130
|
+
* The kit's own `WorkspaceRootNotFoundError` message renders the search path
|
|
131
|
+
* and probed markers; it is not echoed because it embeds the untruncated
|
|
132
|
+
* caller value, which `cwd` already carries through {@link truncateEchoed}.
|
|
133
|
+
*
|
|
134
|
+
* @public
|
|
135
|
+
*/
|
|
136
|
+
const workspaceNotFound = (cwd) => new WorkspaceNotFound({
|
|
137
|
+
cwd,
|
|
138
|
+
message: composeRemediatedMessage(`No workspace root was found walking up from "${truncateEchoed(cwd)}".`, WORKSPACE_REMEDIATION),
|
|
139
|
+
remediation: WORKSPACE_REMEDIATION
|
|
140
|
+
});
|
|
141
|
+
/**
|
|
142
|
+
* Build an {@link InvalidArgument}: `raw` is the human message (already
|
|
143
|
+
* truncated by the caller where it echoes an argument).
|
|
144
|
+
*
|
|
145
|
+
* @public
|
|
146
|
+
*/
|
|
147
|
+
const invalidArgument = (argument, raw, remediation) => new InvalidArgument({
|
|
148
|
+
argument,
|
|
149
|
+
message: composeRemediatedMessage(raw, remediation),
|
|
150
|
+
remediation
|
|
151
|
+
});
|
|
152
|
+
/**
|
|
153
|
+
* Build an {@link EngineError} from a silk-effects typed error. Every engine
|
|
154
|
+
* error in this server renders itself through a `message` getter, so that
|
|
155
|
+
* rendering is the raw message — passed through {@link truncateEchoed} at
|
|
156
|
+
* {@link ENGINE_ECHO_LIMIT}, since the kit embeds caller values in it;
|
|
157
|
+
* `source` keeps the tag for anything that inspects the typed error directly.
|
|
158
|
+
*
|
|
159
|
+
* @public
|
|
160
|
+
*/
|
|
161
|
+
const engineError = (cause, remediation) => new EngineError({
|
|
162
|
+
source: cause._tag,
|
|
163
|
+
message: composeRemediatedMessage(truncateEchoed(cause.message, ENGINE_ECHO_LIMIT), remediation),
|
|
164
|
+
remediation
|
|
165
|
+
});
|
|
166
|
+
/**
|
|
167
|
+
* The shared mapping every handler applies to its engine error channel: the
|
|
168
|
+
* kit's `WorkspaceRootNotFoundError` becomes {@link WorkspaceNotFound} for the
|
|
169
|
+
* directory the caller requested; anything else becomes {@link EngineError}
|
|
170
|
+
* with the tool's remediation.
|
|
171
|
+
*
|
|
172
|
+
* @param requestedCwd - the directory the caller asked about (already the
|
|
173
|
+
* fallback when the call omitted `cwd`)
|
|
174
|
+
* @param remediation - the tool-specific hint for an engine failure
|
|
175
|
+
*
|
|
176
|
+
* @public
|
|
177
|
+
*/
|
|
178
|
+
const mapEngineError = (requestedCwd, remediation) => (cause) => cause._tag === "WorkspaceRootNotFoundError" ? workspaceNotFound(requestedCwd) : engineError(cause, remediation);
|
|
179
|
+
|
|
180
|
+
//#endregion
|
|
181
|
+
export { BiomeFailed, BiomeUnavailable, ENGINE_ECHO_LIMIT, EngineError, InvalidArgument, McpToolError, Remediation, WorkspaceNotFound, composeRemediatedMessage, engineError, invalidArgument, mapEngineError, truncateEchoed, workspaceNotFound };
|