@@ -159,20 +159,46 @@ export function isPropertyRequired(
159159 * Resolves $ref references in JSON schema
160160 * @param schema The schema that may contain $ref
161161 * @param rootSchema The root schema to resolve references against
162+ * @param visitedRefs Optional set of visited $ref paths to detect circular references
162163 * @returns The resolved schema without $ref
163164 */
164165export function resolveRef (
165166 schema : JsonSchemaType ,
166167 rootSchema : JsonSchemaType ,
168+ visitedRefs : Set < string > = new Set ( ) ,
167169) : JsonSchemaType {
170+ if ( ! schema ) return schema ;
171+
168172 if ( ! ( "$ref" in schema ) || ! schema . $ref ) {
173+ // Recursively resolve $ref in anyOf (and other nested structures)
174+ if ( schema . anyOf && Array . isArray ( schema . anyOf ) ) {
175+ const resolvedAnyOf = schema . anyOf . map ( ( item ) => {
176+ if ( typeof item === "object" && item !== null ) {
177+ return resolveRef ( item , rootSchema , visitedRefs ) ;
178+ }
179+ return item ;
180+ } ) ;
181+ return {
182+ ...schema ,
183+ anyOf : resolvedAnyOf ,
184+ } ;
185+ }
169186 return schema ;
170187 }
171188
172189 const ref = schema . $ref ;
173190
174- // Handle simple #/properties/name references
191+ // Handle all #/ formats (#/ properties/, #/$defs/, etc.)
175192 if ( ref . startsWith ( "#/" ) ) {
193+ // Check for circular reference
194+ if ( visitedRefs . has ( ref ) ) {
195+ console . warn ( `Circular reference detected: ${ ref } ` ) ;
196+ return schema ;
197+ }
198+
199+ // Add current ref to visited set
200+ visitedRefs . add ( ref ) ;
201+
176202 const path = ref . substring ( 2 ) . split ( "/" ) ;
177203 let current : unknown = rootSchema ;
178204
@@ -186,12 +212,16 @@ export function resolveRef(
186212 current = ( current as Record < string , unknown > ) [ segment ] ;
187213 } else {
188214 // If reference cannot be resolved, return the original schema
215+ visitedRefs . delete ( ref ) ; // Clean up on failure
189216 console . warn ( `Could not resolve $ref: ${ ref } ` ) ;
190217 return schema ;
191218 }
192219 }
193220
194- return current as JsonSchemaType ;
221+ const resolved = current as JsonSchemaType ;
222+
223+ // Recursively resolve nested structures (anyOf, oneOf, items, properties)
224+ return resolveRef ( resolved , rootSchema , visitedRefs ) ;
195225 }
196226
197227 // For other types of references, return the original schema
@@ -205,54 +235,28 @@ export function resolveRef(
205235 * @returns A normalized schema or the original schema
206236 */
207237export function normalizeUnionType ( schema : JsonSchemaType ) : JsonSchemaType {
208- // Handle anyOf with exactly string and null (FastMCP pattern)
238+ // Handle anyOf with exactly 2 items (type and null) - unified handling
239+ // Preserves enum and other properties automatically
209240 if (
210241 schema . anyOf &&
211242 schema . anyOf . length === 2 &&
212- schema . anyOf . some ( ( t ) => ( t as JsonSchemaType ) . type === "string" ) &&
213243 schema . anyOf . some ( ( t ) => ( t as JsonSchemaType ) . type === "null" )
214244 ) {
215- return { ...schema , type : "string" , anyOf : undefined , nullable : true } ;
216- }
217-
218- // Handle anyOf with exactly boolean and null (FastMCP pattern)
219- if (
220- schema . anyOf &&
221- schema . anyOf . length === 2 &&
222- schema . anyOf . some ( ( t ) => ( t as JsonSchemaType ) . type === "boolean" ) &&
223- schema . anyOf . some ( ( t ) => ( t as JsonSchemaType ) . type === "null" )
224- ) {
225- return { ...schema , type : "boolean" , anyOf : undefined , nullable : true } ;
226- }
227-
228- // Handle anyOf with exactly number and null (FastMCP pattern)
229- if (
230- schema . anyOf &&
231- schema . anyOf . length === 2 &&
232- schema . anyOf . some ( ( t ) => ( t as JsonSchemaType ) . type === "number" ) &&
233- schema . anyOf . some ( ( t ) => ( t as JsonSchemaType ) . type === "null" )
234- ) {
235- return { ...schema , type : "number" , anyOf : undefined , nullable : true } ;
236- }
237-
238- // Handle anyOf with exactly integer and null (FastMCP pattern)
239- if (
240- schema . anyOf &&
241- schema . anyOf . length === 2 &&
242- schema . anyOf . some ( ( t ) => ( t as JsonSchemaType ) . type === "integer" ) &&
243- schema . anyOf . some ( ( t ) => ( t as JsonSchemaType ) . type === "null" )
244- ) {
245- return { ...schema , type : "integer" , anyOf : undefined , nullable : true } ;
246- }
247-
248- // Handle anyOf with exactly array and null (FastMCP pattern)
249- if (
250- schema . anyOf &&
251- schema . anyOf . length === 2 &&
252- schema . anyOf . some ( ( t ) => ( t as JsonSchemaType ) . type === "array" ) &&
253- schema . anyOf . some ( ( t ) => ( t as JsonSchemaType ) . type === "null" )
254- ) {
255- return { ...schema , type : "array" , anyOf : undefined , nullable : true } ;
245+ const nonNullItem = schema . anyOf . find ( ( t ) => {
246+ const item = t as JsonSchemaType ;
247+ return item ?. type !== "null" ;
248+ } ) as JsonSchemaType ;
249+
250+ // Only process if non-null item has type or enum
251+ if ( nonNullItem ?. type || nonNullItem ?. enum ) {
252+ return {
253+ ...schema ,
254+ ...nonNullItem ,
255+ type : nonNullItem ?. type || ( nonNullItem ?. enum ? "string" : undefined ) ,
256+ nullable : true ,
257+ anyOf : undefined ,
258+ } ;
259+ }
256260 }
257261
258262 // Handle array type with exactly string and null
0 commit comments