Merge pull request #954 from jlowin/claude-wt-20250625-210215

Fix external schema reference handling in OpenAPI parser
This commit is contained in:
Jeremiah Lowin 2025-06-25 21:39:49 -04:00 committed by GitHub
commit 159324ea46
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 111 additions and 0 deletions

View file

@ -274,6 +274,12 @@ class OpenAPIParser(
result = {}
return _replace_ref_with_defs(result)
except ValueError as e:
# Re-raise ValueError for external reference errors and other validation issues
if "External or non-local reference not supported" in str(e):
raise
logger.error(f"Failed to extract schema as dict: {e}", exc_info=False)
return {}
except Exception as e:
logger.error(f"Failed to extract schema as dict: {e}", exc_info=False)
return {}
@ -406,12 +412,30 @@ class OpenAPIParser(
request_body_info.content_schema[media_type_str] = (
schema_dict
)
except ValueError as e:
# Re-raise ValueError for external reference errors
if "External or non-local reference not supported" in str(
e
):
raise
logger.error(
f"Failed to extract schema for media type '{media_type_str}': {e}"
)
except Exception as e:
logger.error(
f"Failed to extract schema for media type '{media_type_str}': {e}"
)
return request_body_info
except ValueError as e:
# Re-raise ValueError for external reference errors
if "External or non-local reference not supported" in str(e):
raise
ref_name = getattr(request_body_or_ref, "ref", "unknown")
logger.error(
f"Failed to extract request body '{ref_name}': {e}", exc_info=False
)
return None
except Exception as e:
ref_name = getattr(request_body_or_ref, "ref", "unknown")
logger.error(
@ -455,6 +479,17 @@ class OpenAPIParser(
media_type_obj.media_type_schema
)
resp_info.content_schema[media_type_str] = schema_dict
except ValueError as e:
# Re-raise ValueError for external reference errors
if (
"External or non-local reference not supported"
in str(e)
):
raise
logger.error(
f"Failed to extract schema for media type '{media_type_str}' "
f"in response {status_code}: {e}"
)
except Exception as e:
logger.error(
f"Failed to extract schema for media type '{media_type_str}' "
@ -462,6 +497,16 @@ class OpenAPIParser(
)
extracted_responses[str(status_code)] = resp_info
except ValueError as e:
# Re-raise ValueError for external reference errors
if "External or non-local reference not supported" in str(e):
raise
ref_name = getattr(resp_or_ref, "ref", "unknown")
logger.error(
f"Failed to extract response for status code {status_code} "
f"from reference '{ref_name}': {e}",
exc_info=False,
)
except Exception as e:
ref_name = getattr(resp_or_ref, "ref", "unknown")
logger.error(
@ -562,6 +607,17 @@ class OpenAPIParser(
logger.info(
f"Successfully extracted route: {method_upper} {path_str}"
)
except ValueError as op_error:
# Re-raise ValueError for external reference errors
if "External or non-local reference not supported" in str(
op_error
):
raise
op_id = getattr(operation, "operationId", "unknown")
logger.error(
f"Failed to process operation {method_upper} {path_str} (ID: {op_id}): {op_error}",
exc_info=True,
)
except Exception as op_error:
op_id = getattr(operation, "operationId", "unknown")
logger.error(
@ -907,6 +963,12 @@ def _replace_ref_with_defs(
if ref_path.startswith("#/components/schemas/"):
schema_name = ref_path.split("/")[-1]
schema["$ref"] = f"#/$defs/{schema_name}"
elif not ref_path.startswith("#/"):
raise ValueError(
f"External or non-local reference not supported: {ref_path}. "
f"FastMCP only supports local schema references starting with '#/'. "
f"Please include all schema definitions within the OpenAPI document."
)
elif properties := schema.get("properties"):
if "$ref" in properties:
schema["properties"] = _replace_ref_with_defs(properties)

View file

@ -614,3 +614,52 @@ def test_http_trace_method_path(parsed_http_methods_routes):
assert trace_route is not None
assert trace_route.path == "/resource"
@pytest.fixture
def schema_with_external_reference() -> dict[str, Any]:
"""Fixture that returns a schema with external schema references like in issue #926."""
return {
"openapi": "3.0.0",
"info": {"title": "External Reference API", "version": "1.0.0"},
"paths": {
"/products": {
"post": {
"summary": "Create a product",
"operationId": "createProduct",
"requestBody": {
"required": True,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"obj": {
"$ref": "http://cyaninc.com/json-schemas/market-v1/product-constraints"
}
},
}
}
},
},
"responses": {"201": {"description": "Product created"}},
}
}
},
}
# --- Tests for external schema reference handling --- #
def test_external_reference_raises_clear_error(schema_with_external_reference):
"""Test that external schema references raise a clear, helpful error message."""
with pytest.raises(ValueError) as exc_info:
parse_openapi_to_http_routes(schema_with_external_reference)
error_message = str(exc_info.value)
assert "External or non-local reference not supported" in error_message
assert (
"http://cyaninc.com/json-schemas/market-v1/product-constraints" in error_message
)
assert "FastMCP only supports local schema references" in error_message