Skip to content

Commit dd0985c

Browse files
committed
fix: enhance resolution and enum handling in anyOf schemas
1 parent e87623c commit dd0985c

File tree

1 file changed

+48
-44
lines changed

1 file changed

+48
-44
lines changed

client/src/utils/schemaUtils.ts

Lines changed: 48 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -149,20 +149,46 @@ export function isPropertyRequired(
149149
* Resolves $ref references in JSON schema
150150
* @param schema The schema that may contain $ref
151151
* @param rootSchema The root schema to resolve references against
152+
* @param visitedRefs Optional set of visited $ref paths to detect circular references
152153
* @returns The resolved schema without $ref
153154
*/
154155
export function resolveRef(
155156
schema: JsonSchemaType,
156157
rootSchema: JsonSchemaType,
158+
visitedRefs: Set<string> = new Set(),
157159
): JsonSchemaType {
160+
if (!schema) return schema;
161+
158162
if (!("$ref" in schema) || !schema.$ref) {
163+
// Recursively resolve $ref in anyOf (and other nested structures)
164+
if (schema.anyOf && Array.isArray(schema.anyOf)) {
165+
const resolvedAnyOf = schema.anyOf.map((item) => {
166+
if (typeof item === "object" && item !== null) {
167+
return resolveRef(item, rootSchema, visitedRefs);
168+
}
169+
return item;
170+
});
171+
return {
172+
...schema,
173+
anyOf: resolvedAnyOf,
174+
};
175+
}
159176
return schema;
160177
}
161178

162179
const ref = schema.$ref;
163180

164-
// Handle simple #/properties/name references
181+
// Handle all #/ formats (#/properties/, #/$defs/, etc.)
165182
if (ref.startsWith("#/")) {
183+
// Check for circular reference
184+
if (visitedRefs.has(ref)) {
185+
console.warn(`Circular reference detected: ${ref}`);
186+
return schema;
187+
}
188+
189+
// Add current ref to visited set
190+
visitedRefs.add(ref);
191+
166192
const path = ref.substring(2).split("/");
167193
let current: unknown = rootSchema;
168194

@@ -176,12 +202,16 @@ export function resolveRef(
176202
current = (current as Record<string, unknown>)[segment];
177203
} else {
178204
// If reference cannot be resolved, return the original schema
205+
visitedRefs.delete(ref); // Clean up on failure
179206
console.warn(`Could not resolve $ref: ${ref}`);
180207
return schema;
181208
}
182209
}
183210

184-
return current as JsonSchemaType;
211+
const resolved = current as JsonSchemaType;
212+
213+
// Recursively resolve nested structures (anyOf, oneOf, items, properties)
214+
return resolveRef(resolved, rootSchema, visitedRefs);
185215
}
186216

187217
// For other types of references, return the original schema
@@ -195,54 +225,28 @@ export function resolveRef(
195225
* @returns A normalized schema or the original schema
196226
*/
197227
export function normalizeUnionType(schema: JsonSchemaType): JsonSchemaType {
198-
// Handle anyOf with exactly string and null (FastMCP pattern)
199-
if (
200-
schema.anyOf &&
201-
schema.anyOf.length === 2 &&
202-
schema.anyOf.some((t) => (t as JsonSchemaType).type === "string") &&
203-
schema.anyOf.some((t) => (t as JsonSchemaType).type === "null")
204-
) {
205-
return { ...schema, type: "string", anyOf: undefined, nullable: true };
206-
}
207-
208-
// Handle anyOf with exactly boolean and null (FastMCP pattern)
209-
if (
210-
schema.anyOf &&
211-
schema.anyOf.length === 2 &&
212-
schema.anyOf.some((t) => (t as JsonSchemaType).type === "boolean") &&
213-
schema.anyOf.some((t) => (t as JsonSchemaType).type === "null")
214-
) {
215-
return { ...schema, type: "boolean", anyOf: undefined, nullable: true };
216-
}
217-
218-
// Handle anyOf with exactly number and null (FastMCP pattern)
219-
if (
220-
schema.anyOf &&
221-
schema.anyOf.length === 2 &&
222-
schema.anyOf.some((t) => (t as JsonSchemaType).type === "number") &&
223-
schema.anyOf.some((t) => (t as JsonSchemaType).type === "null")
224-
) {
225-
return { ...schema, type: "number", anyOf: undefined, nullable: true };
226-
}
227-
228-
// Handle anyOf with exactly integer and null (FastMCP pattern)
228+
// Handle anyOf with exactly 2 items (type and null) - unified handling
229+
// Preserves enum and other properties automatically
229230
if (
230231
schema.anyOf &&
231232
schema.anyOf.length === 2 &&
232-
schema.anyOf.some((t) => (t as JsonSchemaType).type === "integer") &&
233233
schema.anyOf.some((t) => (t as JsonSchemaType).type === "null")
234234
) {
235-
return { ...schema, type: "integer", anyOf: undefined, nullable: true };
236-
}
235+
const nonNullItem = schema.anyOf.find((t) => {
236+
const item = t as JsonSchemaType;
237+
return item?.type !== "null";
238+
}) as JsonSchemaType;
237239

238-
// Handle anyOf with exactly array and null (FastMCP pattern)
239-
if (
240-
schema.anyOf &&
241-
schema.anyOf.length === 2 &&
242-
schema.anyOf.some((t) => (t as JsonSchemaType).type === "array") &&
243-
schema.anyOf.some((t) => (t as JsonSchemaType).type === "null")
244-
) {
245-
return { ...schema, type: "array", anyOf: undefined, nullable: true };
240+
// Only process if non-null item has type or enum
241+
if (nonNullItem?.type || nonNullItem?.enum) {
242+
return {
243+
...schema,
244+
...nonNullItem,
245+
type: nonNullItem?.type || (nonNullItem?.enum ? "string" : undefined),
246+
nullable: true,
247+
anyOf: undefined,
248+
};
249+
}
246250
}
247251

248252
// Handle array type with exactly string and null

0 commit comments

Comments
 (0)