@savvy-web/mcp 2.7.5 → 3.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/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 during development, run it through the MCP inspector:
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
- npx @modelcontextprotocol/inspector savvy-mcp .
39
- # opens the inspector UI against a live savvy-mcp instance
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 { makeSilkRuntimeLayer } from "../runtime.js";
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
- /* v8 ignore start -- process bootstrap; covered by server.smoke.test.ts */
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 };