@@ -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 */
154155export 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 */
197227export 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