ShiWei commited on
Commit
fe73cad
·
1 Parent(s): 9c9e880

replace nested requestBody's #/components/ with

Browse files
src/fastmcp/utilities/openapi.py CHANGED
@@ -872,6 +872,45 @@ def format_description_with_responses(
872
  return "\n".join(desc_parts)
873
 
874
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
875
  def _combine_schemas(route: HTTPRoute) -> dict[str, Any]:
876
  """
877
  Combines parameter and request body schemas into a single schema.
@@ -889,38 +928,17 @@ def _combine_schemas(route: HTTPRoute) -> dict[str, Any]:
889
  for param in route.parameters:
890
  if param.required:
891
  required.append(param.name)
892
-
893
- # Copy the schema and add description if available
894
- param_schema = param.schema_.copy() if isinstance(param.schema_, dict) else {}
895
-
896
- # Convert #/components/schemas references to #/$defs references
897
- if isinstance(param_schema, dict) and "$ref" in param_schema:
898
- ref_path = param_schema["$ref"]
899
- if ref_path.startswith("#/components/schemas/"):
900
- schema_name = ref_path.split("/")[-1]
901
- param_schema["$ref"] = f"#/$defs/{schema_name}"
902
-
903
- # Also handle anyOf, allOf, oneOf references
904
- for section in ["anyOf", "allOf", "oneOf"]:
905
- if section in param_schema and isinstance(param_schema[section], list):
906
- for i, item in enumerate(param_schema[section]):
907
- if isinstance(item, dict) and "$ref" in item:
908
- ref_path = item["$ref"]
909
- if ref_path.startswith("#/components/schemas/"):
910
- schema_name = ref_path.split("/")[-1]
911
- param_schema[section][i]["$ref"] = f"#/$defs/{schema_name}"
912
-
913
- # Add parameter description to schema if available and not already present
914
- if param.description and not param_schema.get("description"):
915
- param_schema["description"] = param.description
916
-
917
- properties[param.name] = param_schema
918
 
919
  # Add request body if it exists
920
  if route.request_body and route.request_body.content_schema:
921
  # For now, just use the first content type's schema
922
  content_type = next(iter(route.request_body.content_schema))
923
- body_schema = route.request_body.content_schema[content_type]
 
 
924
  body_props = body_schema.get("properties", {})
925
 
926
  # Add request body properties
@@ -935,7 +953,6 @@ def _combine_schemas(route: HTTPRoute) -> dict[str, Any]:
935
  "properties": properties,
936
  "required": required,
937
  }
938
-
939
  # Add schema definitions if available
940
  if route.schema_definitions:
941
  result["$defs"] = route.schema_definitions
 
872
  return "\n".join(desc_parts)
873
 
874
 
875
+ def _replace_ref_with_defs(
876
+ info: dict[str, Any], description: str | None = None
877
+ ) -> dict[str, Any]:
878
+ """
879
+ Replace openapi $ref with jsonschema $defs
880
+
881
+ Examples:
882
+ - {"type": "object", "properties": {"$ref": "#/components/schemas/..."}}
883
+ - {"$ref": "#/components/schemas/..."}
884
+ - {"items": {"$ref": "#/components/schemas/..."}}
885
+ - {"anyOf": [{"$ref": "#/components/schemas/..."}]}
886
+ - {"allOf": [{"$ref": "#/components/schemas/..."}]}
887
+ - {"oneOf": [{"$ref": "#/components/schemas/..."}]}
888
+
889
+ Args:
890
+ info: dict[str, Any]
891
+ description: str | None
892
+
893
+ Returns:
894
+ dict[str, Any]
895
+ """
896
+ schema = info.copy()
897
+ if properties := schema.get("properties"):
898
+ for prop_name, prop_schema in properties.items():
899
+ properties[prop_name] = _replace_ref_with_defs(prop_schema)
900
+ elif ref_path := schema.get("$ref"):
901
+ if ref_path.startswith("#/components/schemas/"):
902
+ schema_name = ref_path.split("/")[-1]
903
+ schema["$ref"] = f"#/$defs/{schema_name}"
904
+ elif item_schema := schema.get("items"):
905
+ schema["items"] = _replace_ref_with_defs(item_schema)
906
+ for section in ["anyOf", "allOf", "oneOf"]:
907
+ for i, item in enumerate(schema.get(section, [])):
908
+ schema[section][i] = _replace_ref_with_defs(item)
909
+ if info.get("description", description) and not schema.get("description"):
910
+ schema["description"] = description
911
+ return schema
912
+
913
+
914
  def _combine_schemas(route: HTTPRoute) -> dict[str, Any]:
915
  """
916
  Combines parameter and request body schemas into a single schema.
 
928
  for param in route.parameters:
929
  if param.required:
930
  required.append(param.name)
931
+ properties[param.name] = _replace_ref_with_defs(
932
+ param.schema_.copy(), param.description
933
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
934
 
935
  # Add request body if it exists
936
  if route.request_body and route.request_body.content_schema:
937
  # For now, just use the first content type's schema
938
  content_type = next(iter(route.request_body.content_schema))
939
+ body_schema = _replace_ref_with_defs(
940
+ route.request_body.content_schema[content_type].copy()
941
+ )
942
  body_props = body_schema.get("properties", {})
943
 
944
  # Add request body properties
 
953
  "properties": properties,
954
  "required": required,
955
  }
 
956
  # Add schema definitions if available
957
  if route.schema_definitions:
958
  result["$defs"] = route.schema_definitions
tests/utilities/openapi/test_openapi.py CHANGED
@@ -6,7 +6,10 @@ import pytest
6
  from fastapi import Body, FastAPI, Path, Query
7
  from pydantic import BaseModel, Field
8
 
9
- from fastmcp.utilities.openapi import parse_openapi_to_http_routes
 
 
 
10
 
11
  # --- Test Data: Static OpenAPI Schema Dictionaries --- #
12
 
@@ -1023,6 +1026,9 @@ def test_openapi_30_reference_resolution(openapi_30_with_references):
1023
  # or it still has a $ref field
1024
  assert "properties" in category or "$ref" in category
1025
 
 
 
 
1026
 
1027
  def test_openapi_31_reference_resolution(openapi_31_with_references):
1028
  """Test that references are correctly resolved in OpenAPI 3.1 schemas."""
@@ -1057,6 +1063,9 @@ def test_openapi_31_reference_resolution(openapi_31_with_references):
1057
  # or it still has a $ref field
1058
  assert "properties" in category or "$ref" in category
1059
 
 
 
 
1060
 
1061
  def test_consistent_output_across_versions(
1062
  openapi_30_with_references, openapi_31_with_references
 
6
  from fastapi import Body, FastAPI, Path, Query
7
  from pydantic import BaseModel, Field
8
 
9
+ from fastmcp.utilities.openapi import (
10
+ _combine_schemas,
11
+ parse_openapi_to_http_routes,
12
+ )
13
 
14
  # --- Test Data: Static OpenAPI Schema Dictionaries --- #
15
 
 
1026
  # or it still has a $ref field
1027
  assert "properties" in category or "$ref" in category
1028
 
1029
+ combined_schema = _combine_schemas(route)
1030
+ assert "#/$defs/" in combined_schema["properties"]["category"]["$ref"]
1031
+
1032
 
1033
  def test_openapi_31_reference_resolution(openapi_31_with_references):
1034
  """Test that references are correctly resolved in OpenAPI 3.1 schemas."""
 
1063
  # or it still has a $ref field
1064
  assert "properties" in category or "$ref" in category
1065
 
1066
+ combined_schema = _combine_schemas(route)
1067
+ assert "#/$defs/" in combined_schema["properties"]["category"]["$ref"]
1068
+
1069
 
1070
  def test_consistent_output_across_versions(
1071
  openapi_30_with_references, openapi_31_with_references