@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 +3 -2
- package/index.d.ts +7 -7
- package/markdown.js +3 -2
- package/package.json +7 -7
- package/server.js +119 -37
- package/version.js +1 -1
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
|
-
* (
|
|
14
|
-
*
|
|
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
|
|
177
|
-
*
|
|
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
|
|
188
|
-
*
|
|
189
|
-
* channel is `orDie`d:
|
|
190
|
-
*
|
|
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.
|
|
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
|
|
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
|
+
"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.
|
|
40
|
-
"@effected/commands": "^0.
|
|
41
|
-
"@effected/git": "^0.
|
|
42
|
-
"@effected/workspaces": "^0.
|
|
43
|
-
"@savvy-web/silk-effects": "9.0
|
|
44
|
-
"effect": "4.0.0-rc.
|
|
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,
|
|
6
|
-
import {
|
|
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
|
|
22
|
-
* this no tool would serve
|
|
23
|
-
*
|
|
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.
|
|
44
|
-
*
|
|
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;
|
|
50
|
-
*
|
|
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
|
-
|
|
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.
|
|
63
|
-
const inputSchema = yield* Schema.decodeUnknownEffect(McpSchema.
|
|
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
|
-
|
|
164
|
+
registrations.push({
|
|
81
165
|
tool: mcpTool,
|
|
82
166
|
annotations,
|
|
83
|
-
handle: (payload) => built.handle(tool.name, payload ?? {}).pipe(Stream.unwrap, Stream.
|
|
84
|
-
isError:
|
|
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
|
|
172
|
+
text: renderMarkdown(result.result)
|
|
89
173
|
}]
|
|
90
|
-
}))
|
|
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
|
|
116
|
-
*
|
|
117
|
-
* channel is `orDie`d:
|
|
118
|
-
*
|
|
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
|
-
|
|
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 };
|