Skip to content

Commit d6bd800

Browse files
authored
Optimize OpenAPI parser performance with single-pass schema processing (#1214)
1 parent 8209952 commit d6bd800

File tree

6 files changed

+676
-145
lines changed

6 files changed

+676
-145
lines changed

src/fastmcp/experimental/utilities/openapi/schemas.py

Lines changed: 124 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
import logging
44
from typing import Any, cast
55

6-
from fastmcp.utilities.json_schema import compress_schema
7-
86
from .models import HTTPRoute, JsonSchema, ResponseInfo
97

108
logger = logging.getLogger(__name__)
@@ -314,10 +312,42 @@ def _combine_schemas_and_map_params(
314312
}
315313
# Add schema definitions if available
316314
if route.schema_definitions:
317-
result["$defs"] = route.schema_definitions
318-
319-
# Use compress_schema to remove unused definitions
320-
result = compress_schema(result)
315+
result["$defs"] = route.schema_definitions.copy()
316+
317+
# Use lightweight compression - prune additionalProperties and unused definitions
318+
if result.get("additionalProperties") is False:
319+
result.pop("additionalProperties")
320+
321+
# Remove unused definitions (lightweight approach - just check direct $ref usage)
322+
if "$defs" in result:
323+
used_refs = set()
324+
325+
def find_refs_in_value(value):
326+
if isinstance(value, dict):
327+
if "$ref" in value and isinstance(value["$ref"], str):
328+
ref = value["$ref"]
329+
if ref.startswith("#/$defs/"):
330+
used_refs.add(ref.split("/")[-1])
331+
for v in value.values():
332+
find_refs_in_value(v)
333+
elif isinstance(value, list):
334+
for item in value:
335+
find_refs_in_value(item)
336+
337+
# Find refs in the main schema (excluding $defs section)
338+
for key, value in result.items():
339+
if key != "$defs":
340+
find_refs_in_value(value)
341+
342+
# Remove unused definitions
343+
if used_refs:
344+
result["$defs"] = {
345+
name: def_schema
346+
for name, def_schema in result["$defs"].items()
347+
if name in used_refs
348+
}
349+
else:
350+
result.pop("$defs")
321351

322352
return result, parameter_map
323353

@@ -339,17 +369,63 @@ def _combine_schemas(route: HTTPRoute) -> dict[str, Any]:
339369
return schema
340370

341371

372+
def _has_one_of(obj: dict[str, Any] | list[Any]) -> bool:
373+
"""Quickly check if schema contains any 'oneOf' keys without deep traversal."""
374+
if isinstance(obj, dict):
375+
if "oneOf" in obj:
376+
return True
377+
# Only check likely schema containers, skip examples/defaults
378+
for k, v in obj.items():
379+
if k in [
380+
"properties",
381+
"items",
382+
"allOf",
383+
"anyOf",
384+
"additionalProperties",
385+
] and isinstance(v, dict | list):
386+
if _has_one_of(v):
387+
return True
388+
elif isinstance(obj, list):
389+
for item in obj:
390+
if isinstance(item, dict | list) and _has_one_of(item):
391+
return True
392+
return False
393+
394+
342395
def _adjust_union_types(
343-
schema: dict[str, Any] | list[Any],
396+
schema: dict[str, Any] | list[Any], _depth: int = 0
344397
) -> dict[str, Any] | list[Any]:
345398
"""Recursively replace 'oneOf' with 'anyOf' in schema to handle overlapping unions."""
399+
# MAJOR OPTIMIZATION: Skip entirely if schema has no oneOf keys
400+
if _depth == 0 and not _has_one_of(schema):
401+
return schema
402+
403+
# OPTIMIZATION: Early termination for very deep structures to prevent exponential slowdown
404+
if _depth > 30: # Reduced from 50 for better performance
405+
return schema
406+
346407
if isinstance(schema, dict):
347-
if "oneOf" in schema:
348-
schema["anyOf"] = schema.pop("oneOf")
349-
for k, v in schema.items():
350-
schema[k] = _adjust_union_types(v)
408+
# Work on a copy to avoid mutating the input
409+
result = schema.copy()
410+
if "oneOf" in result:
411+
result["anyOf"] = result.pop("oneOf")
412+
# OPTIMIZATION: Only recurse into values that could contain more schemas
413+
for k, v in result.items():
414+
if isinstance(v, dict | list) and k not in [
415+
"examples",
416+
"example",
417+
"default",
418+
]:
419+
result[k] = _adjust_union_types(v, _depth + 1)
420+
return result
351421
elif isinstance(schema, list):
352-
return [_adjust_union_types(item) for item in schema]
422+
# Process list items without mutating the input list
423+
return [
424+
_adjust_union_types(item, _depth + 1)
425+
if isinstance(item, dict | list)
426+
else item
427+
for item in schema
428+
]
353429
return schema
354430

355431

@@ -436,10 +512,42 @@ def extract_output_schema_from_responses(
436512

437513
# Add schema definitions if available
438514
if schema_definitions:
439-
output_schema["$defs"] = schema_definitions
440-
441-
# Use compress_schema to remove unused definitions
442-
output_schema = compress_schema(output_schema)
515+
output_schema["$defs"] = schema_definitions.copy()
516+
517+
# Use lightweight compression - prune additionalProperties and unused definitions
518+
if output_schema.get("additionalProperties") is False:
519+
output_schema.pop("additionalProperties")
520+
521+
# Remove unused definitions (lightweight approach - just check direct $ref usage)
522+
if "$defs" in output_schema:
523+
used_refs = set()
524+
525+
def find_refs_in_value(value):
526+
if isinstance(value, dict):
527+
if "$ref" in value and isinstance(value["$ref"], str):
528+
ref = value["$ref"]
529+
if ref.startswith("#/$defs/"):
530+
used_refs.add(ref.split("/")[-1])
531+
for v in value.values():
532+
find_refs_in_value(v)
533+
elif isinstance(value, list):
534+
for item in value:
535+
find_refs_in_value(item)
536+
537+
# Find refs in the main schema (excluding $defs section)
538+
for key, value in output_schema.items():
539+
if key != "$defs":
540+
find_refs_in_value(value)
541+
542+
# Remove unused definitions
543+
if used_refs:
544+
output_schema["$defs"] = {
545+
name: def_schema
546+
for name, def_schema in output_schema["$defs"].items()
547+
if name in used_refs
548+
}
549+
else:
550+
output_schema.pop("$defs")
443551

444552
# Adjust union types to handle overlapping unions
445553
output_schema = cast(dict[str, Any], _adjust_union_types(output_schema))

0 commit comments

Comments
 (0)