diff --git a/src/fastmcp/utilities/openapi.py b/src/fastmcp/utilities/openapi.py index 59d558f72..c29b20661 100644 --- a/src/fastmcp/utilities/openapi.py +++ b/src/fastmcp/utilities/openapi.py @@ -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) diff --git a/tests/utilities/openapi/test_openapi_advanced.py b/tests/utilities/openapi/test_openapi_advanced.py index 979ca9b28..58a03143d 100644 --- a/tests/utilities/openapi/test_openapi_advanced.py +++ b/tests/utilities/openapi/test_openapi_advanced.py @@ -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