Jeremiah Lowin commited on
Commit
05dfb42
·
unverified ·
2 Parent(s): 08d4e1680215f2

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

Browse files
src/fastmcp/utilities/openapi.py CHANGED
@@ -274,6 +274,12 @@ class OpenAPIParser(
274
  result = {}
275
 
276
  return _replace_ref_with_defs(result)
 
 
 
 
 
 
277
  except Exception as e:
278
  logger.error(f"Failed to extract schema as dict: {e}", exc_info=False)
279
  return {}
@@ -406,12 +412,30 @@ class OpenAPIParser(
406
  request_body_info.content_schema[media_type_str] = (
407
  schema_dict
408
  )
 
 
 
 
 
 
 
 
 
409
  except Exception as e:
410
  logger.error(
411
  f"Failed to extract schema for media type '{media_type_str}': {e}"
412
  )
413
 
414
  return request_body_info
 
 
 
 
 
 
 
 
 
415
  except Exception as e:
416
  ref_name = getattr(request_body_or_ref, "ref", "unknown")
417
  logger.error(
@@ -455,6 +479,17 @@ class OpenAPIParser(
455
  media_type_obj.media_type_schema
456
  )
457
  resp_info.content_schema[media_type_str] = schema_dict
 
 
 
 
 
 
 
 
 
 
 
458
  except Exception as e:
459
  logger.error(
460
  f"Failed to extract schema for media type '{media_type_str}' "
@@ -462,6 +497,16 @@ class OpenAPIParser(
462
  )
463
 
464
  extracted_responses[str(status_code)] = resp_info
 
 
 
 
 
 
 
 
 
 
465
  except Exception as e:
466
  ref_name = getattr(resp_or_ref, "ref", "unknown")
467
  logger.error(
@@ -562,6 +607,17 @@ class OpenAPIParser(
562
  logger.info(
563
  f"Successfully extracted route: {method_upper} {path_str}"
564
  )
 
 
 
 
 
 
 
 
 
 
 
565
  except Exception as op_error:
566
  op_id = getattr(operation, "operationId", "unknown")
567
  logger.error(
@@ -907,6 +963,12 @@ def _replace_ref_with_defs(
907
  if ref_path.startswith("#/components/schemas/"):
908
  schema_name = ref_path.split("/")[-1]
909
  schema["$ref"] = f"#/$defs/{schema_name}"
 
 
 
 
 
 
910
  elif properties := schema.get("properties"):
911
  if "$ref" in properties:
912
  schema["properties"] = _replace_ref_with_defs(properties)
 
274
  result = {}
275
 
276
  return _replace_ref_with_defs(result)
277
+ except ValueError as e:
278
+ # Re-raise ValueError for external reference errors and other validation issues
279
+ if "External or non-local reference not supported" in str(e):
280
+ raise
281
+ logger.error(f"Failed to extract schema as dict: {e}", exc_info=False)
282
+ return {}
283
  except Exception as e:
284
  logger.error(f"Failed to extract schema as dict: {e}", exc_info=False)
285
  return {}
 
412
  request_body_info.content_schema[media_type_str] = (
413
  schema_dict
414
  )
415
+ except ValueError as e:
416
+ # Re-raise ValueError for external reference errors
417
+ if "External or non-local reference not supported" in str(
418
+ e
419
+ ):
420
+ raise
421
+ logger.error(
422
+ f"Failed to extract schema for media type '{media_type_str}': {e}"
423
+ )
424
  except Exception as e:
425
  logger.error(
426
  f"Failed to extract schema for media type '{media_type_str}': {e}"
427
  )
428
 
429
  return request_body_info
430
+ except ValueError as e:
431
+ # Re-raise ValueError for external reference errors
432
+ if "External or non-local reference not supported" in str(e):
433
+ raise
434
+ ref_name = getattr(request_body_or_ref, "ref", "unknown")
435
+ logger.error(
436
+ f"Failed to extract request body '{ref_name}': {e}", exc_info=False
437
+ )
438
+ return None
439
  except Exception as e:
440
  ref_name = getattr(request_body_or_ref, "ref", "unknown")
441
  logger.error(
 
479
  media_type_obj.media_type_schema
480
  )
481
  resp_info.content_schema[media_type_str] = schema_dict
482
+ except ValueError as e:
483
+ # Re-raise ValueError for external reference errors
484
+ if (
485
+ "External or non-local reference not supported"
486
+ in str(e)
487
+ ):
488
+ raise
489
+ logger.error(
490
+ f"Failed to extract schema for media type '{media_type_str}' "
491
+ f"in response {status_code}: {e}"
492
+ )
493
  except Exception as e:
494
  logger.error(
495
  f"Failed to extract schema for media type '{media_type_str}' "
 
497
  )
498
 
499
  extracted_responses[str(status_code)] = resp_info
500
+ except ValueError as e:
501
+ # Re-raise ValueError for external reference errors
502
+ if "External or non-local reference not supported" in str(e):
503
+ raise
504
+ ref_name = getattr(resp_or_ref, "ref", "unknown")
505
+ logger.error(
506
+ f"Failed to extract response for status code {status_code} "
507
+ f"from reference '{ref_name}': {e}",
508
+ exc_info=False,
509
+ )
510
  except Exception as e:
511
  ref_name = getattr(resp_or_ref, "ref", "unknown")
512
  logger.error(
 
607
  logger.info(
608
  f"Successfully extracted route: {method_upper} {path_str}"
609
  )
610
+ except ValueError as op_error:
611
+ # Re-raise ValueError for external reference errors
612
+ if "External or non-local reference not supported" in str(
613
+ op_error
614
+ ):
615
+ raise
616
+ op_id = getattr(operation, "operationId", "unknown")
617
+ logger.error(
618
+ f"Failed to process operation {method_upper} {path_str} (ID: {op_id}): {op_error}",
619
+ exc_info=True,
620
+ )
621
  except Exception as op_error:
622
  op_id = getattr(operation, "operationId", "unknown")
623
  logger.error(
 
963
  if ref_path.startswith("#/components/schemas/"):
964
  schema_name = ref_path.split("/")[-1]
965
  schema["$ref"] = f"#/$defs/{schema_name}"
966
+ elif not ref_path.startswith("#/"):
967
+ raise ValueError(
968
+ f"External or non-local reference not supported: {ref_path}. "
969
+ f"FastMCP only supports local schema references starting with '#/'. "
970
+ f"Please include all schema definitions within the OpenAPI document."
971
+ )
972
  elif properties := schema.get("properties"):
973
  if "$ref" in properties:
974
  schema["properties"] = _replace_ref_with_defs(properties)
tests/utilities/openapi/test_openapi_advanced.py CHANGED
@@ -614,3 +614,52 @@ def test_http_trace_method_path(parsed_http_methods_routes):
614
 
615
  assert trace_route is not None
616
  assert trace_route.path == "/resource"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
614
 
615
  assert trace_route is not None
616
  assert trace_route.path == "/resource"
617
+
618
+
619
+ @pytest.fixture
620
+ def schema_with_external_reference() -> dict[str, Any]:
621
+ """Fixture that returns a schema with external schema references like in issue #926."""
622
+ return {
623
+ "openapi": "3.0.0",
624
+ "info": {"title": "External Reference API", "version": "1.0.0"},
625
+ "paths": {
626
+ "/products": {
627
+ "post": {
628
+ "summary": "Create a product",
629
+ "operationId": "createProduct",
630
+ "requestBody": {
631
+ "required": True,
632
+ "content": {
633
+ "application/json": {
634
+ "schema": {
635
+ "type": "object",
636
+ "properties": {
637
+ "obj": {
638
+ "$ref": "http://cyaninc.com/json-schemas/market-v1/product-constraints"
639
+ }
640
+ },
641
+ }
642
+ }
643
+ },
644
+ },
645
+ "responses": {"201": {"description": "Product created"}},
646
+ }
647
+ }
648
+ },
649
+ }
650
+
651
+
652
+ # --- Tests for external schema reference handling --- #
653
+
654
+
655
+ def test_external_reference_raises_clear_error(schema_with_external_reference):
656
+ """Test that external schema references raise a clear, helpful error message."""
657
+ with pytest.raises(ValueError) as exc_info:
658
+ parse_openapi_to_http_routes(schema_with_external_reference)
659
+
660
+ error_message = str(exc_info.value)
661
+ assert "External or non-local reference not supported" in error_message
662
+ assert (
663
+ "http://cyaninc.com/json-schemas/market-v1/product-constraints" in error_message
664
+ )
665
+ assert "FastMCP only supports local schema references" in error_message