Spaces:
Running
Running
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 |
-
|
| 894 |
-
|
| 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 =
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
| 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
|