@savvy-web/mcp 3.1.3 → 3.2.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/errors.js CHANGED
@@ -10,8 +10,9 @@ import { Schema } from "effect";
10
10
  * typed failure reaches the client as
11
11
  * `{ isError: true, content: [{ type: "text", text: error.message }] }` and
12
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`
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`
15
16
  * field is invisible to a real client: every member folds its hint into
16
17
  * `message` at construction through {@link composeRemediatedMessage}, and any
17
18
  * caller-supplied value echoed back passes through {@link truncateEchoed}.
package/index.d.ts CHANGED
@@ -173,9 +173,8 @@ export declare const makeSilkRuntimeLayer: (cwd: string) => Layer.Layer<McpServi
173
173
  /**
174
174
  * Everything {@link ServerLayer} still needs from the platform: the three
175
175
  * services `makeSilkRuntimeLayer` requires, plus `Stdio`, which
176
- * `McpServer.layerStdio` itself requires (`McpServer.ts:1217-1225`).
177
- * `NodeServices.layer` supplies all four in `main.ts`; tests swap `Stdio`
178
- * for `Stdio.layerTest`.
176
+ * `McpServer.layerStdio` itself requires. `NodeServices.layer` supplies all
177
+ * four in `main.ts`; tests swap `Stdio` for `Stdio.layerTest`.
179
178
  *
180
179
  * @public
181
180
  */
@@ -184,10 +183,11 @@ type PlatformServices = FileSystem.FileSystem | Path.Path | ChildProcessSpawner.
184
183
  * The whole server as one layer: the ten-tool toolkit, the silk-effects
185
184
  * runtime discharging its dependencies, over `McpServer.layerStdio`.
186
185
  *
187
- * `protocols` lists the newest two adapters, newest first — see gotcha 2 in
188
- * the module remarks. `Cause.IllegalArgumentError` in `layerStdio`'s error
189
- * channel is `orDie`d: `protocols` is a static two-element literal, so it is
190
- * an implementer-time defect, not a runtime condition.
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.
191
191
  *
192
192
  * @public
193
193
  */
package/markdown.js CHANGED
@@ -6,10 +6,11 @@ import { Context } from "effect";
6
6
  * to the server's registration step.
7
7
  *
8
8
  * @remarks
9
- * rc.115's `McpServer.registerToolkit` renders every success as
9
+ * rc.116's `McpServer.registerToolkit` renders every success as
10
10
  * `content: [{ type: "text", text: JSON.stringify(encodedResult) }]` plus
11
11
  * `structuredContent` and offers no hook over the text
12
- * (`unstable/ai/McpServer.ts:1577-1585`). `server.ts` therefore registers the
12
+ * (`unstable/ai/McpServer.ts`, `toolResultContent`; Effect-TS/effect#8316
13
+ * proposes one). `server.ts` therefore registers the
13
14
  * toolkit itself through the public `McpServer.addTool` and reads this
14
15
  * annotation to put the tool's markdown projection in `content[0].text`
15
16
  * while `structuredContent` stays the typed object — the dual channel every
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savvy-web/mcp",
3
- "version": "3.1.3",
3
+ "version": "3.2.1",
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",
@@ -36,11 +36,11 @@
36
36
  "savvy-mcp": "bin/savvy-mcp.js"
37
37
  },
38
38
  "dependencies": {
39
- "@effect/platform-node": "4.0.0-rc.115",
40
- "@effected/commands": "^0.7.2",
41
- "@effected/git": "^0.15.2",
42
- "@effected/workspaces": "^0.22.1",
43
- "@savvy-web/silk-effects": "9.0.1",
44
- "effect": "4.0.0-rc.115"
39
+ "@effect/platform-node": "4.0.0-rc.116",
40
+ "@effected/commands": "^0.8.0",
41
+ "@effected/git": "^0.16.0",
42
+ "@effected/workspaces": "^0.24.0",
43
+ "@savvy-web/silk-effects": "9.1.0",
44
+ "effect": "4.0.0-rc.116"
45
45
  }
46
46
  }
package/server.js CHANGED
@@ -2,10 +2,31 @@ import { SilkMarkdown } from "./markdown.js";
2
2
  import { makeSilkRuntimeLayer } from "./runtime.js";
3
3
  import { SilkToolkit, ToolsLayer } from "./toolkit.js";
4
4
  import { CURRENT_MCP_VERSION } from "./version.js";
5
- import { Cause, Context, Effect, ErrorReporter, Layer, Option, Result, Schema, Sink, Stream } from "effect";
6
- import { AiError, McpProtocol, McpSchema, McpServer, Tool } from "effect/unstable/ai";
5
+ import { Cause, Context, Effect, ErrorReporter, Layer, Option, Result, Schema, Stream } from "effect";
6
+ import { CurrentLogLevel } from "effect/References";
7
+ import { AiError, McpProtocol, McpSchema, McpServer, Tool, Toolkit } from "effect/unstable/ai";
8
+ import { HttpServerRequest } from "effect/unstable/http";
7
9
 
8
10
  //#region src/server.ts
11
+ /**
12
+ * The agent-facing orientation every client receives: in the `initialize`
13
+ * result on the stateful protocols and in the `server/discover` result on
14
+ * `2026-07-28`. Says what the server is for, which tool to reach for first,
15
+ * and the traps; the one-line human summary stays in `description`.
16
+ *
17
+ * @public
18
+ */
19
+ const SERVER_INSTRUCTIONS = [
20
+ "savvy-mcp serves ten structured tools over a Silk Suite workspace: a pnpm + Turborepo monorepo on @savvy-web/silk.",
21
+ "Reach for the matching tool before reconstructing an answer from shell output or memory; each encodes the repo's package boundaries, conventions and exclusion rules.",
22
+ "workspace_info: layout, package names, publish and version state; the default for any structural fact about the repo.",
23
+ "turbo_inspect: cache, graph and affected-package inspection; read-only, never runs tasks.",
24
+ "biome_check: structured Biome diagnostics and the ONLY sanctioned route for a Biome fix pass (write/unsafe); a direct Biome invocation skips the repo config and can corrupt vendored trees under .repos/.",
25
+ "changeset_inspect, changeset_validate, changeset_preview: the branch diff by owning package, typed CSH001-CSH005 diagnostics, and the CHANGELOG the pending changesets would produce.",
26
+ "changeset_deps_detect (reads) and changeset_deps_regen (deletes and recreates) dependency changesets.",
27
+ "repos_inspect (reads) and repos_manage (mutates) the vendored reference repos under .repos/; repos_manage restore discards uncommitted worktree edits.",
28
+ "Every successful result carries a markdown transcript in content[0].text and the typed object in structuredContent; read structuredContent when you need fields. A failure is an isError result whose content[0].text carries the message and remediation, with no structuredContent. Every tool takes an optional cwd; omit it to use the server's project directory."
29
+ ].join("\n");
9
30
  const INTERNAL_TOOL_ERROR_MESSAGE = "Tool execution failed due to an internal server error.";
10
31
  const toolErrorResult = (message) => new McpSchema.CallToolResult({
11
32
  isError: true,
@@ -14,13 +35,21 @@ const toolErrorResult = (message) => new McpSchema.CallToolResult({
14
35
  text: message
15
36
  }]
16
37
  });
38
+ const toolResultContent = (encoded) => encoded === void 0 ? [] : [{
39
+ type: "text",
40
+ text: JSON.stringify(encoded)
41
+ }];
42
+ const omitRequestServices = Context.omit(McpSchema.McpRequestContext, McpSchema.McpServerClient, HttpServerRequest.HttpServerRequest, CurrentLogLevel);
43
+ const isParameterValidationError = (error) => AiError.isAiError(error) && error.reason._tag === "ToolParameterValidationError";
17
44
  /**
18
45
  * Hoist a `$ref`-rooted JSON Schema document onto its referenced definition.
19
46
  * `Schema.toJsonSchemaDocument` emits `{ $ref: "#/$defs/<id>", $defs }` for
20
47
  * any schema annotated with an `identifier` — every result schema here is —
21
- * and `McpSchema.ToolJsonSchema` needs a `type: "object"` root, so without
22
- * this no tool would serve an `outputSchema` (rc.115's own `registerToolkit`
23
- * has the same blind spot). The definitions stay attached for nested refs.
48
+ * and a strict MCP client needs a `type: "object"` root on `outputSchema`,
49
+ * so without this no tool would serve one. The definitions stay attached
50
+ * for nested refs. (rc.116's own `registerToolkit` inlines the top-level
51
+ * reference of the INPUT schema through an internal helper and serves the
52
+ * output document verbatim; this is the public-API equivalent for both.)
24
53
  */
25
54
  const hoistRootRef = (schema) => {
26
55
  const ref = schema.$ref;
@@ -35,33 +64,88 @@ const hoistRootRef = (schema) => {
35
64
  $defs: defs
36
65
  };
37
66
  };
67
+ /**
68
+ * The served input schema: MCP requires an object root, so a top-level `$ref`
69
+ * is inlined, and a strict tool is emitted with `onExcessProperty: "error"`
70
+ * so the client sees `additionalProperties: false`. Mirrors rc.116's
71
+ * `toolInputJsonSchema` through public API (`Schema.toJsonSchemaDocument` +
72
+ * {@link hoistRootRef} in place of the internal `resolveTopLevelReference`).
73
+ */
74
+ const toolInputJsonSchema = (schema, strict) => {
75
+ const document = Schema.toJsonSchemaDocument(schema, { onExcessProperty: strict ? "error" : "ignore" });
76
+ const withDefs = Object.keys(document.definitions).length === 0 ? document.schema : {
77
+ ...document.schema,
78
+ $defs: document.definitions
79
+ };
80
+ return hoistRootRef(withDefs);
81
+ };
38
82
  /** MCP models `structuredContent` as a JSON object, so a `null` or array encoded result is omitted. */
39
83
  const toStructuredContent = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
40
84
  /**
41
85
  * Register a toolkit with the running `McpServer`, rendering each success as
42
86
  * the tool's markdown transcript in `content[0].text` and the encoded result
43
- * in `structuredContent`. A port of rc.115's `McpServer.registerToolkit`
44
- * (`McpServer.ts:1525-1611`) that differs in the success branch only.
87
+ * in `structuredContent`. A port of rc.116's `McpServer.registerToolkit`
88
+ * that differs in the success branch and the `outputSchema` gate only (see
89
+ * the module remarks).
45
90
  *
46
91
  * @remarks
47
92
  * `Effect.context<never>()` captures the registration-time services (the
48
93
  * handlers' declared dependencies, discharged by the runtime layer) so each
49
- * call runs against them; `McpServerClient` is provided per call by the
50
- * framework and is the one service excluded from that capture.
94
+ * call runs against them; the per-request services (`McpRequestContext`,
95
+ * the legacy `McpServerClient`, the HTTP request, the request log level) are
96
+ * omitted from that capture because the framework provides them per call.
97
+ *
98
+ * @internal exported for the in-process suite, which registers fixture
99
+ * toolkits (a strict tool, for one) through the same path the served
100
+ * toolkit takes.
51
101
  */
52
102
  const registerSilkToolkit = (toolkit) => Effect.gen(function* () {
53
103
  const registry = yield* McpServer.McpServer;
54
- const built = yield* toolkit;
55
- const services = yield* Effect.context();
104
+ const built = yield* toolkit.pipe(Effect.updateContext((context) => {
105
+ const services = new Map(context.mapUnsafe);
106
+ for (const tool of Object.values(toolkit.tools)) {
107
+ const handler = services.get(tool.id);
108
+ if (handler !== void 0) services.set(tool.id, {
109
+ ...handler,
110
+ context: omitRequestServices(handler.context)
111
+ });
112
+ }
113
+ return Context.makeUnsafe(services);
114
+ }));
115
+ const services = omitRequestServices(yield* Effect.context());
56
116
  const reportCause = (cause) => Effect.provideContext(ErrorReporter.report(cause), services);
117
+ const internalToolError = (cause) => {
118
+ const failure = Cause.findFail(cause);
119
+ return Result.isFailure(failure) && !Cause.hasDies(cause) ? Effect.failCause(failure.failure) : Effect.logError(cause).pipe(Effect.andThen(reportCause(cause)), Effect.as(toolErrorResult(INTERNAL_TOOL_ERROR_MESSAGE)));
120
+ };
121
+ const registrations = [];
57
122
  for (const tool of Object.values(built.tools)) {
123
+ const strict = Tool.getStrictMode(tool) === true;
124
+ const rawJsonSchema = Tool.isDynamic(tool) ? tool.jsonSchema : void 0;
125
+ if (strict && rawJsonSchema !== void 0) return yield* Effect.die(`McpServer cannot strictly validate the raw JSON Schema for tool '${tool.name}'; use an Effect Schema instead`);
126
+ const decodeOptions = strict ? { onExcessProperty: "error" } : void 0;
58
127
  const annotations = tool.annotations;
59
128
  const renderMarkdown = Context.getOrUndefined(annotations, SilkMarkdown);
129
+ const toolMeta = Context.getOrUndefined(annotations, Tool.Meta);
60
130
  const isDeclaredFailure = Schema.is(tool.failureSchema);
131
+ const encodeFailure = Schema.encodeUnknownEffect(tool.failureSchema);
132
+ const declaredFailureResult = (error) => error instanceof Error ? Effect.succeed(toolErrorResult(error.message)) : Effect.map(encodeFailure(error), (encoded) => new McpSchema.CallToolResult({
133
+ isError: true,
134
+ content: toolResultContent(encoded)
135
+ }));
136
+ const handleCause = (cause) => {
137
+ const failure = Cause.findFail(cause);
138
+ if (Result.isSuccess(failure)) {
139
+ const error = failure.success.error;
140
+ const origin = Context.get(Cause.reasonAnnotations(failure.success), Toolkit.FailureOrigin);
141
+ if (origin === "parameters" && isParameterValidationError(error)) return Effect.fail(new McpSchema.InvalidParams({ message: error.reason.message }));
142
+ if (origin === "handler" && isDeclaredFailure(error)) return Effect.logError(cause).pipe(Effect.andThen(Effect.catchCause(declaredFailureResult(error), internalToolError)));
143
+ }
144
+ return internalToolError(cause);
145
+ };
61
146
  const outputJsonSchema = hoistRootRef(Tool.getJsonSchemaFromSchema(tool.successSchema));
62
- const outputSchema = outputJsonSchema.type === "object" ? yield* Schema.decodeUnknownEffect(McpSchema.ToolJsonSchema)(outputJsonSchema).pipe(Effect.orDie) : void 0;
63
- const inputSchema = yield* Schema.decodeUnknownEffect(McpSchema.ToolJsonSchema)(Tool.getJsonSchema(tool)).pipe(Effect.orDie);
64
- const toolMeta = Context.getOrUndefined(annotations, Tool.Meta);
147
+ const outputSchema = outputJsonSchema.type === "object" ? yield* Schema.decodeUnknownEffect(McpSchema.ToolOutputJson)(outputJsonSchema).pipe(Effect.orDie) : void 0;
148
+ const inputSchema = yield* Schema.decodeUnknownEffect(McpSchema.ToolJson)(rawJsonSchema ?? toolInputJsonSchema(tool.parametersSchema, strict)).pipe(Effect.orDie);
65
149
  const description = Tool.getDescription(tool);
66
150
  const mcpTool = new McpSchema.Tool({
67
151
  name: tool.name,
@@ -77,29 +161,20 @@ const registerSilkToolkit = (toolkit) => Effect.gen(function* () {
77
161
  },
78
162
  ...toolMeta === void 0 ? {} : { _meta: toolMeta }
79
163
  });
80
- yield* registry.addTool({
164
+ registrations.push({
81
165
  tool: mcpTool,
82
166
  annotations,
83
- handle: (payload) => built.handle(tool.name, payload ?? {}).pipe(Stream.unwrap, Stream.run(Sink.last()), Effect.flatMap(Effect.fromOption), Effect.map((result) => new McpSchema.CallToolResult({
84
- isError: false,
85
- structuredContent: toStructuredContent(result.encodedResult),
86
- content: [{
167
+ handle: (payload) => built.handle(tool.name, payload ?? {}, void 0, decodeOptions).pipe(Stream.unwrap, Stream.runLast, Effect.flatMap(Effect.fromOption), Effect.flatMap((result) => result.isFailure && result.failureOrigin !== "handler" ? Effect.failCause(Cause.annotate(Cause.fail(result.result), Context.make(Toolkit.FailureOrigin, result.failureOrigin ?? "result"))) : Effect.succeed(new McpSchema.CallToolResult({
168
+ isError: result.isFailure,
169
+ structuredContent: result.isFailure ? void 0 : toStructuredContent(result.encodedResult),
170
+ content: result.isFailure || renderMarkdown === void 0 ? toolResultContent(result.encodedResult) : [{
87
171
  type: "text",
88
- text: renderMarkdown === void 0 ? JSON.stringify(result.encodedResult) : renderMarkdown(result.result)
172
+ text: renderMarkdown(result.result)
89
173
  }]
90
- })), Effect.provideContext(services), Effect.tapCause(Effect.logError), Effect.catchCause((cause) => {
91
- const failure = Cause.findError(cause);
92
- if (Result.isFailure(failure)) return Cause.hasDies(cause) ? Effect.as(reportCause(cause), toolErrorResult(INTERNAL_TOOL_ERROR_MESSAGE)) : Effect.failCause(failure.failure);
93
- const error = failure.success;
94
- if (AiError.isAiError(error)) {
95
- const reason = error.reason;
96
- return reason._tag === "ToolParameterValidationError" ? Effect.fail(new McpSchema.InvalidParams({ message: reason.message })) : Effect.as(reportCause(cause), toolErrorResult(INTERNAL_TOOL_ERROR_MESSAGE));
97
- }
98
- const message = isDeclaredFailure(error) && error instanceof Error ? error.message : INTERNAL_TOOL_ERROR_MESSAGE;
99
- return Effect.as(reportCause(cause), toolErrorResult(message));
100
- }))
174
+ }))), Effect.catchCause(handleCause), Effect.provideContext(services))
101
175
  });
102
176
  }
177
+ for (const registration of registrations) yield* registry.addTool(registration);
103
178
  });
104
179
  /**
105
180
  * The toolkit registration as a layer: {@link registerSilkToolkit} over
@@ -112,18 +187,25 @@ const SilkToolsLayer = (cwd) => Layer.effectDiscard(registerSilkToolkit(SilkTool
112
187
  * The whole server as one layer: the ten-tool toolkit, the silk-effects
113
188
  * runtime discharging its dependencies, over `McpServer.layerStdio`.
114
189
  *
115
- * `protocols` lists the newest two adapters, newest first — see gotcha 2 in
116
- * the module remarks. `Cause.IllegalArgumentError` in `layerStdio`'s error
117
- * channel is `orDie`d: `protocols` is a static two-element literal, so it is
118
- * an implementer-time defect, not a runtime condition.
190
+ * `protocols` lists the stateless `2026-07-28` adapter first, then the two
191
+ * newest stateful ones — see gotcha 2 in the module remarks.
192
+ * `Cause.IllegalArgumentError` in `layerStdio`'s error channel is `orDie`d:
193
+ * `protocols` is a static literal with exactly one stateless member, so a
194
+ * failure there is an implementer-time defect, not a runtime condition.
119
195
  *
120
196
  * @public
121
197
  */
122
198
  const ServerLayer = (cwd) => SilkToolsLayer(cwd).pipe(Layer.provide(makeSilkRuntimeLayer(cwd)), Layer.provide(McpServer.layerStdio({
123
199
  name: "savvy-mcp",
124
200
  version: CURRENT_MCP_VERSION,
125
- protocols: [McpProtocol.v2025_11_25, McpProtocol.v2025_06_18]
201
+ description: "Structured Silk Suite workspace tools: workspace, turbo, biome, changesets and vendored repos.",
202
+ instructions: SERVER_INSTRUCTIONS,
203
+ protocols: [
204
+ McpProtocol.v2026_07_28,
205
+ McpProtocol.v2025_11_25,
206
+ McpProtocol.v2025_06_18
207
+ ]
126
208
  })), Layer.orDie);
127
209
 
128
210
  //#endregion
129
- export { ServerLayer };
211
+ export { SERVER_INSTRUCTIONS, ServerLayer, registerSilkToolkit };
package/version.js CHANGED
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * @packageDocumentation
8
8
  */
9
- const CURRENT_MCP_VERSION = "3.1.3";
9
+ const CURRENT_MCP_VERSION = "3.2.1";
10
10
 
11
11
  //#endregion
12
12
  export { CURRENT_MCP_VERSION };