@savvy-web/mcp 2.7.4 → 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 +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 +12 -9
- 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/schema/effect-to-zod.js
DELETED
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
import { Schema } from "effect";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
|
|
4
|
-
//#region src/schema/effect-to-zod.ts
|
|
5
|
-
/**
|
|
6
|
-
* Bridge an Effect Schema to a zod schema by routing through JSON Schema, so a
|
|
7
|
-
* tool keeps Effect Schema as the canonical source of truth while the MCP SDK
|
|
8
|
-
* receives the zod instance its `registerTool` API requires.
|
|
9
|
-
*
|
|
10
|
-
* @packageDocumentation
|
|
11
|
-
*/
|
|
12
|
-
/**
|
|
13
|
-
* Convert an Effect `Schema.Codec<A, I>` to a zod schema:
|
|
14
|
-
* `Schema.toJsonSchemaDocument`, then inline every `#/$defs/*` `$ref` and
|
|
15
|
-
* normalize back to the wire contract, then `z.fromJSONSchema`.
|
|
16
|
-
*
|
|
17
|
-
* @remarks
|
|
18
|
-
* - Effect-only refinements (custom predicates, brands) erase during the
|
|
19
|
-
* round-trip; declare zod directly if boundary enforcement matters.
|
|
20
|
-
* - The source schema must not use `Schema.suspend` (recursive `$ref`s would
|
|
21
|
-
* make the inlining pass non-terminating). MCP tool output schemas in this
|
|
22
|
-
* package are non-recursive projections by construction.
|
|
23
|
-
* - The MCP SDK normalises `outputSchema` to an object; non-object results
|
|
24
|
-
* (e.g. a bare union) are wrapped in a permissive object so the SDK accepts
|
|
25
|
-
* them.
|
|
26
|
-
*/
|
|
27
|
-
const effectToZodSchema = (schema) => {
|
|
28
|
-
const inlined = effectSchemaToInlinedJsonSchema(schema);
|
|
29
|
-
const zodSchema = z.fromJSONSchema(inlined);
|
|
30
|
-
if (isObjectLike(zodSchema)) return zodSchema;
|
|
31
|
-
return z.object({}).catchall(z.unknown());
|
|
32
|
-
};
|
|
33
|
-
const isObjectLike = (schema) => schema instanceof z.ZodObject;
|
|
34
|
-
/**
|
|
35
|
-
* Produce the inlined, normalized JSON Schema object handed to
|
|
36
|
-
* `z.fromJSONSchema`. Exposed separately so schema-snapshot tooling can
|
|
37
|
-
* serialize exactly what the bridge feeds zod.
|
|
38
|
-
*
|
|
39
|
-
* @remarks
|
|
40
|
-
* v4's `Schema.toJsonSchemaDocument` returns `{ dialect, schema, definitions }`
|
|
41
|
-
* with every identifier-annotated subschema hoisted into `definitions` and
|
|
42
|
-
* referenced as `#/$defs/<name>` — including the root itself. Two v4 encoding
|
|
43
|
-
* changes are normalized back to the v3-era wire contract here (in the bridge,
|
|
44
|
-
* never in the public schemas):
|
|
45
|
-
*
|
|
46
|
-
* - `Schema.Number` now encodes non-finite values as strings, emitting
|
|
47
|
-
* `anyOf: [number, "NaN", "Infinity", "-Infinity"]`. Tool results never
|
|
48
|
-
* carry non-finite numbers, so this collapses to `{ type: "number" }`.
|
|
49
|
-
* - `Schema.optional(S)` now admits `undefined`, emitting
|
|
50
|
-
* `anyOf: [S, { type: "null" }]` on the (already non-required) key. The
|
|
51
|
-
* handlers build results with conditional spreads and never emit
|
|
52
|
-
* `undefined`/`null` for optional keys, so the null arm is dropped.
|
|
53
|
-
* Deliberate `Schema.NullOr` fields are all on required keys and keep
|
|
54
|
-
* their null arm.
|
|
55
|
-
* - Filter checks (`isMinLength`, `isPattern`, …) now emit as
|
|
56
|
-
* `allOf: [{ minLength: 1 }]` instead of inline keywords. Bare-constraint
|
|
57
|
-
* `allOf` members are folded back into the parent node.
|
|
58
|
-
*/
|
|
59
|
-
const effectSchemaToInlinedJsonSchema = (schema) => {
|
|
60
|
-
const doc = Schema.toJsonSchemaDocument(schema);
|
|
61
|
-
return inlineAllRefs(doc.schema, doc.definitions);
|
|
62
|
-
};
|
|
63
|
-
const REF_PREFIX = "#/$defs/";
|
|
64
|
-
/** A JSON-schema node matching `{ "type": "null" }` exactly. */
|
|
65
|
-
const isNullSchema = (value) => {
|
|
66
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
67
|
-
const obj = value;
|
|
68
|
-
return obj.type === "null" && Object.keys(obj).length === 1;
|
|
69
|
-
};
|
|
70
|
-
/** A JSON-schema node matching `{ "type": "string", "enum": ["NaN" | "Infinity" | "-Infinity"] }`. */
|
|
71
|
-
const isNonFiniteArm = (value) => {
|
|
72
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
73
|
-
const obj = value;
|
|
74
|
-
if (obj.type !== "string" || !Array.isArray(obj.enum) || obj.enum.length !== 1) return false;
|
|
75
|
-
const literal = obj.enum[0];
|
|
76
|
-
return literal === "NaN" || literal === "Infinity" || literal === "-Infinity";
|
|
77
|
-
};
|
|
78
|
-
/** A JSON-schema node matching `{ "type": "number" }` exactly. */
|
|
79
|
-
const isPlainNumberArm = (value) => {
|
|
80
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
81
|
-
const obj = value;
|
|
82
|
-
return obj.type === "number" && Object.keys(obj).length === 1;
|
|
83
|
-
};
|
|
84
|
-
/** JSON-schema keywords that are pure value constraints (no structural meaning). */
|
|
85
|
-
const CONSTRAINT_KEYS = /* @__PURE__ */ new Set([
|
|
86
|
-
"minLength",
|
|
87
|
-
"maxLength",
|
|
88
|
-
"pattern",
|
|
89
|
-
"minimum",
|
|
90
|
-
"maximum",
|
|
91
|
-
"exclusiveMinimum",
|
|
92
|
-
"exclusiveMaximum",
|
|
93
|
-
"multipleOf",
|
|
94
|
-
"minItems",
|
|
95
|
-
"maxItems",
|
|
96
|
-
"format"
|
|
97
|
-
]);
|
|
98
|
-
/** A node whose every key is a pure value-constraint keyword. */
|
|
99
|
-
const isBareConstraint = (value) => {
|
|
100
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
101
|
-
const keys = Object.keys(value);
|
|
102
|
-
return keys.length > 0 && keys.every((k) => CONSTRAINT_KEYS.has(k));
|
|
103
|
-
};
|
|
104
|
-
/**
|
|
105
|
-
* Fold `allOf` members that are bare constraint objects — v4's encoding of
|
|
106
|
-
* filter checks — back into the parent node (v3 inlined them). Keys already
|
|
107
|
-
* present on the parent are left inside `allOf` untouched.
|
|
108
|
-
*/
|
|
109
|
-
const flattenConstraintAllOf = (node) => {
|
|
110
|
-
const allOf = node.allOf;
|
|
111
|
-
if (!Array.isArray(allOf)) return node;
|
|
112
|
-
const remaining = [];
|
|
113
|
-
const folded = {};
|
|
114
|
-
for (const member of allOf) {
|
|
115
|
-
if (!isBareConstraint(member) || Object.keys(member).some((k) => k in node || k in folded)) {
|
|
116
|
-
remaining.push(member);
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
Object.assign(folded, member);
|
|
120
|
-
}
|
|
121
|
-
if (Object.keys(folded).length === 0) return node;
|
|
122
|
-
const { allOf: _dropped, ...rest } = node;
|
|
123
|
-
return remaining.length > 0 ? {
|
|
124
|
-
...rest,
|
|
125
|
-
...folded,
|
|
126
|
-
allOf: remaining
|
|
127
|
-
} : {
|
|
128
|
-
...rest,
|
|
129
|
-
...folded
|
|
130
|
-
};
|
|
131
|
-
};
|
|
132
|
-
/**
|
|
133
|
-
* Collapse v4's non-finite number encoding — `anyOf: [{ type: "number" },
|
|
134
|
-
* "NaN", "Infinity", "-Infinity"]` — to `{ type: "number" }`, keeping any
|
|
135
|
-
* sibling annotations (description, title) on the node.
|
|
136
|
-
*/
|
|
137
|
-
const collapseNonFiniteNumber = (node) => {
|
|
138
|
-
const anyOf = node.anyOf;
|
|
139
|
-
if (!Array.isArray(anyOf) || anyOf.length !== 4) return node;
|
|
140
|
-
const numberArms = anyOf.filter(isPlainNumberArm);
|
|
141
|
-
const nonFiniteArms = anyOf.filter(isNonFiniteArm);
|
|
142
|
-
if (numberArms.length !== 1 || nonFiniteArms.length !== 3) return node;
|
|
143
|
-
const { anyOf: _dropped, ...siblings } = node;
|
|
144
|
-
return {
|
|
145
|
-
type: "number",
|
|
146
|
-
...siblings
|
|
147
|
-
};
|
|
148
|
-
};
|
|
149
|
-
/**
|
|
150
|
-
* Drop the `{ type: "null" }` arm that v4's `Schema.optional` adds for its
|
|
151
|
-
* `undefined` case. Applied only to non-required properties, so deliberate
|
|
152
|
-
* `Schema.NullOr` fields (all on required keys) keep their null arm.
|
|
153
|
-
*/
|
|
154
|
-
const dropUndefinedArm = (node) => {
|
|
155
|
-
if (node === null || typeof node !== "object" || Array.isArray(node)) return node;
|
|
156
|
-
const obj = node;
|
|
157
|
-
if (!Array.isArray(obj.anyOf)) return obj;
|
|
158
|
-
const arms = obj.anyOf.filter((arm) => !isNullSchema(arm));
|
|
159
|
-
if (arms.length === obj.anyOf.length) return obj;
|
|
160
|
-
const { anyOf: _dropped, ...siblings } = obj;
|
|
161
|
-
if (arms.length === 1 && arms[0] !== null && typeof arms[0] === "object" && !Array.isArray(arms[0])) return collapseNonFiniteNumber({
|
|
162
|
-
...arms[0],
|
|
163
|
-
...siblings
|
|
164
|
-
});
|
|
165
|
-
return collapseNonFiniteNumber({
|
|
166
|
-
anyOf: arms,
|
|
167
|
-
...siblings
|
|
168
|
-
});
|
|
169
|
-
};
|
|
170
|
-
/**
|
|
171
|
-
* Replace every `$ref: "#/$defs/X"` node with the contents of
|
|
172
|
-
* `definitions.X`, recursively, and normalize the v4 encoding deltas back to
|
|
173
|
-
* the wire contract. Assumes acyclic refs.
|
|
174
|
-
*/
|
|
175
|
-
const inlineAllRefs = (root, defs) => {
|
|
176
|
-
const visit = (value) => {
|
|
177
|
-
if (Array.isArray(value)) return value.map(visit);
|
|
178
|
-
if (value === null || typeof value !== "object") return value;
|
|
179
|
-
const obj = value;
|
|
180
|
-
if (typeof obj.$ref === "string" && obj.$ref.startsWith(REF_PREFIX)) {
|
|
181
|
-
const target = defs[obj.$ref.slice(8)];
|
|
182
|
-
if (target !== void 0) return visit(target);
|
|
183
|
-
}
|
|
184
|
-
const required = Array.isArray(obj.required) ? obj.required : [];
|
|
185
|
-
const out = {};
|
|
186
|
-
for (const [k, v] of Object.entries(obj)) {
|
|
187
|
-
if (k === "$defs") continue;
|
|
188
|
-
if (k === "properties" && v !== null && typeof v === "object" && !Array.isArray(v)) {
|
|
189
|
-
const props = {};
|
|
190
|
-
for (const [propKey, propValue] of Object.entries(v)) {
|
|
191
|
-
const visited = visit(propValue);
|
|
192
|
-
props[propKey] = required.includes(propKey) ? visited : dropUndefinedArm(visited);
|
|
193
|
-
}
|
|
194
|
-
out[k] = props;
|
|
195
|
-
continue;
|
|
196
|
-
}
|
|
197
|
-
out[k] = visit(v);
|
|
198
|
-
}
|
|
199
|
-
return collapseNonFiniteNumber(flattenConstraintAllOf(out));
|
|
200
|
-
};
|
|
201
|
-
return visit(root);
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
//#endregion
|
|
205
|
-
export { effectSchemaToInlinedJsonSchema, effectToZodSchema };
|