Skip to content

Commit 2311176

Browse files
committed
feat(schema): schema support helpers
1 parent 6634b01 commit 2311176

File tree

1 file changed

+161
-0
lines changed

1 file changed

+161
-0
lines changed

src/server.schema.ts

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import { z, fromJSONSchema, toJSONSchema } from 'zod';
2+
import { isPlainObject } from './server.helpers';
3+
4+
/**
5+
* Check if a value is a Zod schema (v3 or v4).
6+
*
7+
* @param value - Value to check
8+
* @returns `true` if the value appears to be a Zod schema
9+
*/
10+
const isZodSchema = (value: unknown): boolean => {
11+
if (!value || typeof value !== 'object') {
12+
return false;
13+
}
14+
15+
const obj = value as Record<string, unknown>;
16+
17+
// Zod v3 has _def property
18+
// Zod v4 has _zod property
19+
// Zod schemas have parse/safeParse methods
20+
return (
21+
('_def' in obj && obj._def !== undefined) ||
22+
('_zod' in obj && obj._zod !== undefined) ||
23+
(typeof obj.parse === 'function') ||
24+
(typeof obj.safeParse === 'function') ||
25+
(typeof obj.safeParseAsync === 'function')
26+
);
27+
};
28+
29+
/**
30+
* Check if a value is a ZodRawShapeCompat (object with Zod schemas as values).
31+
*
32+
* @param value - Value to check
33+
* @returns `true` if the value appears to be a ZodRawShapeCompat
34+
*/
35+
const isZodRawShape = (value: unknown): boolean => {
36+
if (!isPlainObject(value)) {
37+
return false;
38+
}
39+
40+
const obj = value as Record<string, unknown>;
41+
const values = Object.values(obj);
42+
43+
// Empty object is not a shape
44+
if (values.length === 0) {
45+
return false;
46+
}
47+
48+
// All values must be Zod schemas
49+
return values.every(isZodSchema);
50+
};
51+
52+
/**
53+
* Convert a plain JSON Schema object to a Zod schema.
54+
* - For simple cases, converts to appropriate Zod schemas.
55+
* - For complex cases, falls back to z.any() to accept any input.
56+
*
57+
* @param jsonSchema - Plain JSON Schema object
58+
* @param settings - Optional settings
59+
* @param settings.failFast - Fail fast on unsupported types, or be nice and attempt to convert. Defaults to true.
60+
* @returns Zod schema equivalent
61+
*/
62+
const jsonSchemaToZod = (
63+
jsonSchema: unknown,
64+
{ failFast = true }: { failFast?: boolean } = {}
65+
): z.ZodTypeAny | undefined => {
66+
if (!isPlainObject(jsonSchema)) {
67+
return failFast ? undefined : z.any();
68+
}
69+
70+
const schema = jsonSchema as Record<string, unknown>;
71+
72+
try {
73+
return fromJSONSchema(schema);
74+
} catch {
75+
if (failFast) {
76+
return undefined;
77+
}
78+
}
79+
80+
// Handle object type schemas
81+
if (schema.type === 'object') {
82+
// If additionalProperties is true, allow any properties
83+
if (schema.additionalProperties === true || schema.additionalProperties === undefined) {
84+
if (z.looseObject) {
85+
return z.looseObject({});
86+
}
87+
88+
// This is a simplified conversion - full JSON Schema to Zod conversion would be more complex
89+
return z.object({}).passthrough();
90+
}
91+
92+
// If additionalProperties is false, use strict object
93+
return z.object({}).strict();
94+
}
95+
96+
// For other types, fall back to z.any()
97+
// A full implementation would handle array, string, number, boolean, etc.
98+
return z.any();
99+
};
100+
101+
/**
102+
* Attempt to normalize an `inputSchema` to a Zod schema, compatible with the MCP SDK.
103+
* - If it's already a Zod schema or ZodRawShapeCompat, return as-is.
104+
* - If it's a plain JSON Schema, convert it to a Zod schema.
105+
*
106+
* @param inputSchema - Input schema (Zod schema, ZodRawShapeCompat, or plain JSON Schema)
107+
* @returns Returns a Zod instance for known inputs (Zod schema, raw shape, or JSON Schema), or the original value otherwise.
108+
*/
109+
const normalizeInputSchema = (inputSchema: unknown): z.ZodTypeAny | unknown => {
110+
// If it's already a Zod schema or a ZodRawShapeCompat (object with Zod schemas as values), return as-is
111+
if (isZodSchema(inputSchema)) {
112+
return inputSchema;
113+
}
114+
115+
// If it's a Zod raw shape (object of Zod schemas), wrap as a Zod object schema
116+
if (isZodRawShape(inputSchema)) {
117+
return z.object(inputSchema as Record<string, any>);
118+
}
119+
120+
// If it's a plain JSON Schema object, convert to Zod
121+
if (isPlainObject(inputSchema)) {
122+
return jsonSchemaToZod(inputSchema);
123+
}
124+
125+
// Fallback: return as-is (might be undefined or other types)
126+
return inputSchema;
127+
};
128+
129+
/**
130+
* Convert a Zod v4 schema to JSON Schema if supported, else return undefined.
131+
* Defaults target to JSON Schema 2020-12 and generates the INPUT schema (for args).
132+
*
133+
* @param schema - Zod schema
134+
* @param params - Optional parameters
135+
* @param params.target - JSON Schema version to generate. Defaults to "draft-2020-12".
136+
* @param params.io - Whether to generate the INPUT or OUTPUT schema. Defaults to "input".
137+
* @param params.unrepresentable - What to do with unrepresentable values. Defaults to "any".
138+
* @param params.params - Additional parameters to pass to toJSONSchema.
139+
*/
140+
const zodToJsonSchema = (
141+
schema: unknown,
142+
{ target = 'draft-2020-12', io = 'input', unrepresentable = 'any', ...params }:
143+
{ target?: string; io?: 'input' | 'output'; unrepresentable?: 'throw' | 'any', params?: Record<string, unknown> } = {}
144+
): unknown => {
145+
if (!isZodSchema(schema)) {
146+
return undefined;
147+
}
148+
149+
try {
150+
return toJSONSchema(schema as any, {
151+
target,
152+
io,
153+
unrepresentable,
154+
...params
155+
});
156+
} catch {}
157+
158+
return undefined;
159+
};
160+
161+
export { isZodSchema, isZodRawShape, jsonSchemaToZod, normalizeInputSchema, zodToJsonSchema };

0 commit comments

Comments
 (0)