@savvy-web/mcp 3.2.4 → 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/errors.js CHANGED
@@ -1,117 +1,59 @@
1
+ import { ToolFailure } from "@effected/mcp";
1
2
  import { Schema } from "effect";
2
3
 
3
4
  //#region src/errors.ts
4
5
  /**
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
- * (rc.116's `registerToolkit`, `declaredFailureResult` in
14
- * `.repos/effect/packages/effect/src/unstable/ai/McpServer.ts` — see the
15
- * header of `server.ts`). So a structured `remediation`
16
- * field is invisible to a real client: every member folds its hint into
17
- * `message` at construction through {@link composeRemediatedMessage}, and any
18
- * caller-supplied value echoed back passes through {@link truncateEchoed}.
19
- *
20
- * @packageDocumentation
21
- */
22
- /** What a caller should do next about a failed tool call. @public */
23
- const Remediation = Schema.Struct({
24
- hint: Schema.String,
25
- suggestedTool: Schema.optionalKey(Schema.String)
26
- });
27
- /**
28
- * Compose a self-contained wire message from a raw cause message and its
29
- * remediation: the human message, then the hint, then `Try <suggestedTool>.`
30
- * when one is present. Every {@link McpToolError} member's `message` is built
31
- * through this at construction, because `message` is the only field a
32
- * declared failure delivers to the wire.
33
- *
34
- * @public
35
- */
36
- const composeRemediatedMessage = (message, remediation) => remediation.suggestedTool === void 0 ? `${message} ${remediation.hint}` : `${message} ${remediation.hint} Try ${remediation.suggestedTool}.`;
37
- const ECHO_LIMIT = 200;
38
- /**
39
- * The limit for an ENGINE message echoed through {@link engineError} or an
40
- * argument-decode message: wider than {@link truncateEchoed}'s default because
41
- * the engine's own rendering (a git stderr, a decode path) is the diagnostic,
42
- * but still bounded — the kit embeds caller values (`base`, `task`, the
43
- * decoded value) in those messages untruncated.
44
- */
45
- const ENGINE_ECHO_LIMIT = 2e3;
46
- /**
47
- * Truncate a caller-supplied value before it is echoed back inside an error's
48
- * `message`. Without this a pathological argument (a multi-megabyte `cwd`,
49
- * say) is echoed once in the response's `content[0].text` and once more in the
50
- * corresponding log line, burning an agent's context twice on what is usually
51
- * a typo.
52
- *
53
- * @public
54
- */
55
- const truncateEchoed = (value, limit = ECHO_LIMIT) => value.length > limit ? `${value.slice(0, limit)}…` : value;
56
- /**
57
6
  * No workspace root was found walking up from the requested directory.
58
- * `message` is composed through {@link composeRemediatedMessage} at
59
- * construction, so it is what reaches the wire as `content[0].text`.
7
+ * `message` is composed through `ToolFailure.message` at construction, so it
8
+ * is what reaches the wire as `content[0].text`.
60
9
  *
61
10
  * @public
62
11
  */
63
12
  var WorkspaceNotFound = class extends Schema.TaggedError()("WorkspaceNotFound", {
64
- cwd: Schema.String,
65
- message: Schema.String,
66
- remediation: Remediation
13
+ ...ToolFailure.fields,
14
+ cwd: Schema.String
67
15
  }) {};
68
16
  /**
69
17
  * A tool argument was structurally acceptable but semantically invalid — a
70
18
  * `repos_manage` action missing the field it needs, a `biome_check` path
71
19
  * outside the workspace, a `changeset_validate` directory that does not
72
- * exist. `message` is composed through {@link composeRemediatedMessage}.
20
+ * exist. `message` is composed through `ToolFailure.message`.
73
21
  *
74
22
  * @public
75
23
  */
76
24
  var InvalidArgument = class extends Schema.TaggedError()("InvalidArgument", {
77
- argument: Schema.String,
78
- message: Schema.String,
79
- remediation: Remediation
25
+ ...ToolFailure.fields,
26
+ argument: Schema.String
80
27
  }) {};
81
28
  /**
82
29
  * A silk-effects engine program failed with one of its own typed errors
83
30
  * (`TurboError`, `GitError`, `ReposConfigError`, …). `source` carries that
84
31
  * error's `_tag`; `message` is its own rendering plus the remediation,
85
- * composed through {@link composeRemediatedMessage}.
32
+ * composed through `ToolFailure.message`.
86
33
  *
87
34
  * @public
88
35
  */
89
36
  var EngineError = class extends Schema.TaggedError()("EngineError", {
90
- source: Schema.String,
91
- message: Schema.String,
92
- remediation: Remediation
37
+ ...ToolFailure.fields,
38
+ source: Schema.String
93
39
  }) {};
94
40
  /**
95
41
  * No Biome binary could be located. `message` is composed through
96
- * {@link composeRemediatedMessage}.
42
+ * `ToolFailure.message`.
97
43
  *
98
44
  * @public
99
45
  */
100
- var BiomeUnavailable = class extends Schema.TaggedError()("BiomeUnavailable", {
101
- message: Schema.String,
102
- remediation: Remediation
103
- }) {};
46
+ var BiomeUnavailable = class extends Schema.TaggedError()("BiomeUnavailable", { ...ToolFailure.fields }) {};
104
47
  /**
105
48
  * Biome itself failed (exit status above 1, a spawn error, or a timeout) —
106
49
  * distinct from "lint issues found", which is a successful result. `message`
107
- * is composed through {@link composeRemediatedMessage}.
50
+ * is composed through `ToolFailure.message`.
108
51
  *
109
52
  * @public
110
53
  */
111
54
  var BiomeFailed = class extends Schema.TaggedError()("BiomeFailed", {
112
- exitCode: Schema.optionalKey(Schema.Number),
113
- message: Schema.String,
114
- remediation: Remediation
55
+ ...ToolFailure.fields,
56
+ exitCode: Schema.optionalKey(Schema.Number)
115
57
  }) {};
116
58
  /** The one failure schema every savvy-mcp tool declares. @public */
117
59
  const McpToolError = Schema.Union([
@@ -130,13 +72,13 @@ const WORKSPACE_REMEDIATION = {
130
72
  * Build a {@link WorkspaceNotFound} for the directory the caller asked about.
131
73
  * The kit's own `WorkspaceRootNotFoundError` message renders the search path
132
74
  * and probed markers; it is not echoed because it embeds the untruncated
133
- * caller value, which `cwd` already carries through {@link truncateEchoed}.
75
+ * caller value, which `cwd` already carries through `ToolFailure.truncate`.
134
76
  *
135
77
  * @public
136
78
  */
137
79
  const workspaceNotFound = (cwd) => new WorkspaceNotFound({
138
80
  cwd,
139
- message: composeRemediatedMessage(`No workspace root was found walking up from "${truncateEchoed(cwd)}".`, WORKSPACE_REMEDIATION),
81
+ message: ToolFailure.message(`No workspace root was found walking up from "${ToolFailure.truncate(cwd)}".`, WORKSPACE_REMEDIATION),
140
82
  remediation: WORKSPACE_REMEDIATION
141
83
  });
142
84
  /**
@@ -147,21 +89,21 @@ const workspaceNotFound = (cwd) => new WorkspaceNotFound({
147
89
  */
148
90
  const invalidArgument = (argument, raw, remediation) => new InvalidArgument({
149
91
  argument,
150
- message: composeRemediatedMessage(raw, remediation),
92
+ message: ToolFailure.message(raw, remediation),
151
93
  remediation
152
94
  });
153
95
  /**
154
96
  * Build an {@link EngineError} from a silk-effects typed error. Every engine
155
97
  * error in this server renders itself through a `message` getter, so that
156
- * rendering is the raw message — passed through {@link truncateEchoed} at
157
- * {@link ENGINE_ECHO_LIMIT}, since the kit embeds caller values in it;
98
+ * rendering is the raw message — passed through `ToolFailure.truncate` at
99
+ * `ToolFailure.ENGINE_ECHO_LIMIT`, since the kit embeds caller values in it;
158
100
  * `source` keeps the tag for anything that inspects the typed error directly.
159
101
  *
160
102
  * @public
161
103
  */
162
104
  const engineError = (cause, remediation) => new EngineError({
163
105
  source: cause._tag,
164
- message: composeRemediatedMessage(truncateEchoed(cause.message, ENGINE_ECHO_LIMIT), remediation),
106
+ message: ToolFailure.message(ToolFailure.truncate(cause.message, ToolFailure.ENGINE_ECHO_LIMIT), remediation),
165
107
  remediation
166
108
  });
167
109
  /**
@@ -179,4 +121,4 @@ const engineError = (cause, remediation) => new EngineError({
179
121
  const mapEngineError = (requestedCwd, remediation) => (cause) => cause._tag === "WorkspaceRootNotFoundError" ? workspaceNotFound(requestedCwd) : engineError(cause, remediation);
180
122
 
181
123
  //#endregion
182
- export { BiomeFailed, BiomeUnavailable, ENGINE_ECHO_LIMIT, EngineError, InvalidArgument, McpToolError, Remediation, WorkspaceNotFound, composeRemediatedMessage, engineError, invalidArgument, mapEngineError, truncateEchoed, workspaceNotFound };
124
+ export { BiomeFailed, BiomeUnavailable, EngineError, InvalidArgument, McpToolError, WorkspaceNotFound, engineError, invalidArgument, mapEngineError, workspaceNotFound };
package/index.d.ts CHANGED
@@ -1,82 +1,59 @@
1
- import { Context, FileSystem, Layer, Path, Schema, Stdio } from "effect";
1
+ import { Distribution } from "@effected/engine";
2
+ import { FileSystem, Layer, Path, Schema, Stdio } from "effect";
2
3
  import { WorkspaceRoot } from "@effected/workspaces";
3
4
  import { Changesets, Repos, SilkWorkspaceAnalyzer, Turbo } from "@savvy-web/silk-effects";
4
5
  import { ChildProcessSpawner } from "effect/unstable/process";
5
6
  import { Tool, Toolkit } from "effect/unstable/ai";
6
7
  //#region src/errors.d.ts
7
- /** What a caller should do next about a failed tool call. @public */
8
- export declare const Remediation: Schema.Struct<{
9
- readonly hint: Schema.String;
10
- readonly suggestedTool: Schema.optionalKey<Schema.String>;
11
- }>;
12
- /** @public */
13
- export type Remediation = typeof Remediation.Type;
14
- /**
15
- * Compose a self-contained wire message from a raw cause message and its
16
- * remediation: the human message, then the hint, then `Try <suggestedTool>.`
17
- * when one is present. Every {@link McpToolError} member's `message` is built
18
- * through this at construction, because `message` is the only field a
19
- * declared failure delivers to the wire.
20
- *
21
- * @public
22
- */
23
- export declare const composeRemediatedMessage: (message: string, remediation: Remediation) => string;
24
- /**
25
- * Truncate a caller-supplied value before it is echoed back inside an error's
26
- * `message`. Without this a pathological argument (a multi-megabyte `cwd`,
27
- * say) is echoed once in the response's `content[0].text` and once more in the
28
- * corresponding log line, burning an agent's context twice on what is usually
29
- * a typo.
30
- *
31
- * @public
32
- */
33
- export declare const truncateEchoed: (value: string, limit?: number) => string;
34
8
  declare const WorkspaceNotFound_base: Schema.Class<WorkspaceNotFound, Schema.TaggedStruct<"WorkspaceNotFound", {
35
- readonly cwd: Schema.String;
36
9
  readonly message: Schema.String;
37
10
  readonly remediation: Schema.Struct<{
38
11
  readonly hint: Schema.String;
39
12
  readonly suggestedTool: Schema.optionalKey<Schema.String>;
13
+ readonly suggestedArgs: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Unknown>>;
40
14
  }>;
15
+ readonly cwd: Schema.String;
41
16
  }>, import("effect/Cause").YieldableError>;
42
17
  /**
43
18
  * No workspace root was found walking up from the requested directory.
44
- * `message` is composed through {@link composeRemediatedMessage} at
45
- * construction, so it is what reaches the wire as `content[0].text`.
19
+ * `message` is composed through `ToolFailure.message` at construction, so it
20
+ * is what reaches the wire as `content[0].text`.
46
21
  *
47
22
  * @public
48
23
  */
49
24
  export declare class WorkspaceNotFound extends WorkspaceNotFound_base {}
50
25
  declare const InvalidArgument_base: Schema.Class<InvalidArgument, Schema.TaggedStruct<"InvalidArgument", {
51
- readonly argument: Schema.String;
52
26
  readonly message: Schema.String;
53
27
  readonly remediation: Schema.Struct<{
54
28
  readonly hint: Schema.String;
55
29
  readonly suggestedTool: Schema.optionalKey<Schema.String>;
30
+ readonly suggestedArgs: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Unknown>>;
56
31
  }>;
32
+ readonly argument: Schema.String;
57
33
  }>, import("effect/Cause").YieldableError>;
58
34
  /**
59
35
  * A tool argument was structurally acceptable but semantically invalid — a
60
36
  * `repos_manage` action missing the field it needs, a `biome_check` path
61
37
  * outside the workspace, a `changeset_validate` directory that does not
62
- * exist. `message` is composed through {@link composeRemediatedMessage}.
38
+ * exist. `message` is composed through `ToolFailure.message`.
63
39
  *
64
40
  * @public
65
41
  */
66
42
  export declare class InvalidArgument extends InvalidArgument_base {}
67
43
  declare const EngineError_base: Schema.Class<EngineError, Schema.TaggedStruct<"EngineError", {
68
- readonly source: Schema.String;
69
44
  readonly message: Schema.String;
70
45
  readonly remediation: Schema.Struct<{
71
46
  readonly hint: Schema.String;
72
47
  readonly suggestedTool: Schema.optionalKey<Schema.String>;
48
+ readonly suggestedArgs: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Unknown>>;
73
49
  }>;
50
+ readonly source: Schema.String;
74
51
  }>, import("effect/Cause").YieldableError>;
75
52
  /**
76
53
  * A silk-effects engine program failed with one of its own typed errors
77
54
  * (`TurboError`, `GitError`, `ReposConfigError`, …). `source` carries that
78
55
  * error's `_tag`; `message` is its own rendering plus the remediation,
79
- * composed through {@link composeRemediatedMessage}.
56
+ * composed through `ToolFailure.message`.
80
57
  *
81
58
  * @public
82
59
  */
@@ -86,27 +63,29 @@ declare const BiomeUnavailable_base: Schema.Class<BiomeUnavailable, Schema.Tagge
86
63
  readonly remediation: Schema.Struct<{
87
64
  readonly hint: Schema.String;
88
65
  readonly suggestedTool: Schema.optionalKey<Schema.String>;
66
+ readonly suggestedArgs: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Unknown>>;
89
67
  }>;
90
68
  }>, import("effect/Cause").YieldableError>;
91
69
  /**
92
70
  * No Biome binary could be located. `message` is composed through
93
- * {@link composeRemediatedMessage}.
71
+ * `ToolFailure.message`.
94
72
  *
95
73
  * @public
96
74
  */
97
75
  export declare class BiomeUnavailable extends BiomeUnavailable_base {}
98
76
  declare const BiomeFailed_base: Schema.Class<BiomeFailed, Schema.TaggedStruct<"BiomeFailed", {
99
- readonly exitCode: Schema.optionalKey<Schema.Number>;
100
77
  readonly message: Schema.String;
101
78
  readonly remediation: Schema.Struct<{
102
79
  readonly hint: Schema.String;
103
80
  readonly suggestedTool: Schema.optionalKey<Schema.String>;
81
+ readonly suggestedArgs: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Unknown>>;
104
82
  }>;
83
+ readonly exitCode: Schema.optionalKey<Schema.Number>;
105
84
  }>, import("effect/Cause").YieldableError>;
106
85
  /**
107
86
  * Biome itself failed (exit status above 1, a spawn error, or a timeout) —
108
87
  * distinct from "lint issues found", which is a successful result. `message`
109
- * is composed through {@link composeRemediatedMessage}.
88
+ * is composed through `ToolFailure.message`.
110
89
  *
111
90
  * @public
112
91
  */
@@ -116,18 +95,6 @@ export declare const McpToolError: Schema.Union<readonly [typeof WorkspaceNotFou
116
95
  /** @public */
117
96
  export type McpToolError = typeof McpToolError.Type;
118
97
  //#endregion
119
- //#region src/markdown.d.ts
120
- declare const SilkMarkdown_base: Context.ServiceClass<SilkMarkdown, "@savvy-web/mcp/SilkMarkdown", (data: unknown) => string>;
121
- /**
122
- * Renders a tool's decoded success value as the markdown transcript an agent
123
- * reads. Typed over `unknown` because the annotation is read generically at
124
- * registration; each tool supplies `Schema.decodeUnknownSync(<X>AsMarkdown)`,
125
- * whose own decoder is the type guard.
126
- *
127
- * @public
128
- */
129
- export declare class SilkMarkdown extends SilkMarkdown_base {}
130
- //#endregion
131
98
  //#region src/runtime.d.ts
132
99
  /**
133
100
  * Every service the MCP runtime provides to the tool handlers.
@@ -173,25 +140,38 @@ export declare const makeSilkRuntimeLayer: (cwd: string) => Layer.Layer<McpServi
173
140
  /**
174
141
  * Everything {@link ServerLayer} still needs from the platform: the three
175
142
  * services `makeSilkRuntimeLayer` requires, plus `Stdio`, which
176
- * `McpServer.layerStdio` itself requires. `NodeServices.layer` supplies all
143
+ * `McpStdio.layer` itself requires. `NodeServices.layer` supplies all
177
144
  * four in `main.ts`; tests swap `Stdio` for `Stdio.layerTest`.
178
145
  *
179
146
  * @public
180
147
  */
181
148
  type PlatformServices = FileSystem.FileSystem | Path.Path | ChildProcessSpawner.ChildProcessSpawner | Stdio.Stdio;
149
+ /**
150
+ * Options for {@link ServerLayer}.
151
+ *
152
+ * @public
153
+ */
154
+ interface ServerOptions {
155
+ /**
156
+ * The carrier this server was installed through (for example
157
+ * `@savvy-web/silk`), rendered into `serverInfo.version` as
158
+ * `" via <name> <version>"`. Absent for a direct install.
159
+ */
160
+ readonly distribution?: Distribution | undefined;
161
+ }
182
162
  /**
183
163
  * The whole server as one layer: the ten-tool toolkit, the silk-effects
184
- * runtime discharging its dependencies, over `McpServer.layerStdio`.
164
+ * runtime discharging its dependencies, over `@effected/mcp`'s
165
+ * `McpStdio.layer`.
185
166
  *
186
- * `protocols` lists the stateless `2026-07-28` adapter first, then the two
187
- * newest stateful ones — see gotcha 2 in the module remarks.
188
- * `Cause.IllegalArgumentError` in `layerStdio`'s error channel is `orDie`d:
189
- * `protocols` is a static literal with exactly one stateless member, so a
190
- * failure there is an implementer-time defect, not a runtime condition.
167
+ * `McpStdio.layer` is `McpServer.layerStdio` with the kit's default
168
+ * `protocols` (the stateless `2026-07-28` adapter first, then the two newest
169
+ * stateful ones — see gotcha 2), `LogToStderr` merged into its output, a
170
+ * stdin guard answering a non-JSON line with `-32700`, and `Layer.orDie`.
191
171
  *
192
172
  * @public
193
173
  */
194
- export declare const ServerLayer: (cwd: string) => Layer.Layer<never, never, PlatformServices>;
174
+ export declare const ServerLayer: (cwd: string, options?: ServerOptions) => Layer.Layer<never, never, PlatformServices>;
195
175
  //#endregion
196
176
  //#region src/tools/biome-check.d.ts
197
177
  /**
@@ -741,5 +721,5 @@ export declare const ToolsLayer: (cwd: string) => Layer.Layer<Tool.HandlersFor<S
741
721
  */
742
722
  export declare const CURRENT_MCP_VERSION: any;
743
723
  //#endregion
744
- export type { McpServices, PlatformServices, SilkTools };
724
+ export type { McpServices, PlatformServices, ServerOptions, SilkTools };
745
725
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -1,8 +1,7 @@
1
- import { BiomeFailed, BiomeUnavailable, EngineError, InvalidArgument, McpToolError, Remediation, WorkspaceNotFound, composeRemediatedMessage, truncateEchoed } from "./errors.js";
2
- import { SilkMarkdown } from "./markdown.js";
1
+ import { BiomeFailed, BiomeUnavailable, EngineError, InvalidArgument, McpToolError, WorkspaceNotFound } from "./errors.js";
3
2
  import { makeSilkRuntimeLayer } from "./runtime.js";
4
3
  import { SilkToolkit, ToolsLayer } from "./toolkit.js";
5
4
  import { CURRENT_MCP_VERSION } from "./version.js";
6
5
  import { ServerLayer } from "./server.js";
7
6
 
8
- export { BiomeFailed, BiomeUnavailable, CURRENT_MCP_VERSION, EngineError, InvalidArgument, McpToolError, Remediation, ServerLayer, SilkMarkdown, SilkToolkit, ToolsLayer, WorkspaceNotFound, composeRemediatedMessage, makeSilkRuntimeLayer, truncateEchoed };
7
+ export { BiomeFailed, BiomeUnavailable, CURRENT_MCP_VERSION, EngineError, InvalidArgument, McpToolError, ServerLayer, SilkToolkit, ToolsLayer, WorkspaceNotFound, makeSilkRuntimeLayer };
package/main.d.ts CHANGED
@@ -1,25 +1,30 @@
1
+ import { Distribution } from "@effected/engine";
1
2
  //#region src/main.d.ts
2
3
  /**
3
- * Owns the `savvy-mcp` process: crash guards, project-root resolution, and the
4
- * server layer launched over stdio.
4
+ * Options for {@link main}.
5
5
  *
6
- * @remarks
7
- * No static imports of the server graph — every module reachable from the
8
- * MCP tool surface is imported dynamically, after the crash guards are
9
- * registered, so an error during that import (or anything downstream) is
10
- * caught by `uncaughtException`/`unhandledRejection` rather than crashing
11
- * before a handler exists.
12
- *
13
- * @packageDocumentation
6
+ * @public
14
7
  */
8
+ export interface MainOptions {
9
+ /**
10
+ * The carrier this bin was installed through, for example
11
+ * `{ name: "@savvy-web/silk", version }`; rendered into the server's
12
+ * reported version. Absent for a direct install.
13
+ */
14
+ readonly distribution?: Distribution | undefined;
15
+ }
15
16
  /**
16
17
  * Run the savvy MCP server over stdio. Owns the process.
17
18
  *
18
19
  * @remarks
19
- * NO static imports of the server graph — see the module remarks.
20
+ * The project directory is the first positional argument, then
21
+ * `SAVVY_MCP_PROJECT_DIR`, then `CLAUDE_PROJECT_DIR`, then the working
22
+ * directory — an empty value or an unsubstituted `${VAR}` placeholder is
23
+ * skipped (`LaunchContext.projectDir`). NO static imports of the server
24
+ * graph — see the module remarks.
20
25
  *
21
26
  * @public
22
27
  */
23
- export declare const main: () => Promise<void>;
28
+ export declare const main: (options?: MainOptions) => Promise<void>;
24
29
  //#endregion
25
30
  //# sourceMappingURL=main.d.ts.map
package/main.js CHANGED
@@ -1,18 +1,4 @@
1
1
  //#region src/main.ts
2
- /**
3
- * Owns the `savvy-mcp` process: crash guards, project-root resolution, and the
4
- * server layer launched over stdio.
5
- *
6
- * @remarks
7
- * No static imports of the server graph — every module reachable from the
8
- * MCP tool surface is imported dynamically, after the crash guards are
9
- * registered, so an error during that import (or anything downstream) is
10
- * caught by `uncaughtException`/`unhandledRejection` rather than crashing
11
- * before a handler exists.
12
- *
13
- * @packageDocumentation
14
- */
15
- /* v8 ignore start -- process bootstrap; covered by the server-lifecycle e2e and the silk bins e2e */
16
2
  const fatal = (label, error) => {
17
3
  process.stderr.write(`savvy-mcp: ${label}: ${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
18
4
  process.exit(1);
@@ -21,20 +7,29 @@ const fatal = (label, error) => {
21
7
  * Run the savvy MCP server over stdio. Owns the process.
22
8
  *
23
9
  * @remarks
24
- * NO static imports of the server graph — see the module remarks.
10
+ * The project directory is the first positional argument, then
11
+ * `SAVVY_MCP_PROJECT_DIR`, then `CLAUDE_PROJECT_DIR`, then the working
12
+ * directory — an empty value or an unsubstituted `${VAR}` placeholder is
13
+ * skipped (`LaunchContext.projectDir`). NO static imports of the server
14
+ * graph — see the module remarks.
25
15
  *
26
16
  * @public
27
17
  */
28
- const main = async () => {
18
+ const main = async (options = {}) => {
29
19
  process.on("uncaughtException", (e) => fatal("uncaught exception", e));
30
20
  process.on("unhandledRejection", (r) => fatal("unhandled rejection", r));
31
21
  const { NodeRuntime, NodeServices } = await import("@effect/platform-node");
32
- const { Cause, Exit, Layer, Logger, Runtime } = await import("effect");
33
- const { resolveProjectDir } = await import("./internal/project-root.js");
22
+ const { Layer } = await import("effect");
23
+ const { LaunchContext } = await import("@effected/engine");
24
+ const { McpStdio } = await import("@effected/mcp");
34
25
  const { ServerLayer } = await import("./server.js");
35
- const cwd = resolveProjectDir(process.argv.slice(2), process.env, () => process.cwd());
36
- const program = Layer.launch(ServerLayer(cwd).pipe(Layer.provide(NodeServices.layer), Layer.provide(Logger.layer([Logger.consolePretty()])), Layer.provide(Layer.succeed(Logger.LogToStderr, true))));
37
- NodeRuntime.runMain(program, { teardown: (exit, onExit) => Exit.isSuccess(exit) || Cause.hasInterruptsOnly(exit.cause) ? onExit(0) : Runtime.defaultTeardown(exit, onExit) });
26
+ const cwd = LaunchContext.projectDir({
27
+ argv: process.argv.slice(2, 3),
28
+ env: process.env,
29
+ keys: ["SAVVY_MCP_PROJECT_DIR", "CLAUDE_PROJECT_DIR"],
30
+ cwd: process.cwd()
31
+ });
32
+ NodeRuntime.runMain(McpStdio.launch(ServerLayer(cwd, options).pipe(Layer.provide(NodeServices.layer))), { teardown: McpStdio.teardown });
38
33
  };
39
34
  /* v8 ignore stop */
40
35
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savvy-web/mcp",
3
- "version": "3.2.4",
3
+ "version": "3.3.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",
@@ -38,7 +38,9 @@
38
38
  "dependencies": {
39
39
  "@effect/platform-node": "4.0.0-rc.117",
40
40
  "@effected/commands": "^0.9.0",
41
+ "@effected/engine": "^0.1.0",
41
42
  "@effected/git": "^0.17.0",
43
+ "@effected/mcp": "^0.1.1",
42
44
  "@effected/templates": "^0.8.0",
43
45
  "@effected/workspaces": "^0.26.0",
44
46
  "@savvy-web/silk-effects": "9.1.3",