@savvy-web/mcp 2.7.5 → 3.0.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.
@@ -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 };