From 5009d64465b3360f4bb08c74bb7f28ac00f33ad9 Mon Sep 17 00:00:00 2001 From: chaoliang yan Date: Tue, 21 Apr 2026 03:00:18 +1000 Subject: [PATCH] fix: handle circular JSON Pointer $ref in dereference_refs (#3896) Co-authored-by: lawrence3699 Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> --- src/fastmcp/utilities/json_schema.py | 7 ++++-- tests/utilities/test_json_schema.py | 36 ++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/fastmcp/utilities/json_schema.py b/src/fastmcp/utilities/json_schema.py index 75320e1a3..4e71fc102 100644 --- a/src/fastmcp/utilities/json_schema.py +++ b/src/fastmcp/utilities/json_schema.py @@ -172,8 +172,11 @@ def dereference_refs(schema: dict[str, Any]) -> dict[str, Any]: return dereferenced - except JsonRefError: - # Self-referencing/circular schemas can't be fully dereferenced + except (JsonRefError, RecursionError): + # Self-referencing/circular schemas can't be fully dereferenced. + # RecursionError covers circular $ref using JSON Pointer paths + # (e.g. "#/properties/nodes/items") that bypass $defs-based cycle + # detection — common in schemas from .NET/System.Text.Json. # Fall back to resolving only root-level $ref (for MCP spec compliance) return resolve_root_ref(schema) diff --git a/tests/utilities/test_json_schema.py b/tests/utilities/test_json_schema.py index 5c5f4c860..28a898af6 100644 --- a/tests/utilities/test_json_schema.py +++ b/tests/utilities/test_json_schema.py @@ -128,6 +128,42 @@ class TestDereferenceRefs: assert result.get("type") == "object" assert "$defs" in result # $defs preserved for circular refs + def test_falls_back_for_circular_json_pointer_refs(self): + """Test that circular JSON Pointer $ref (non-$defs) does not crash. + + .NET/System.Text.Json emits $ref with JSON Pointer paths like + #/properties/nodes/items instead of $defs-based references. + Circular pointers must not cause a RecursionError. + """ + schema = { + "type": "object", + "properties": { + "nodes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "value": {"type": "string"}, + "children": { + "type": "array", + "items": {"$ref": "#/properties/nodes/items"}, + }, + }, + }, + }, + }, + } + result = dereference_refs(schema) + + # Should return the schema without crashing; circular refs stay unresolved + assert result["type"] == "object" + assert "properties" in result + # The circular $ref should be preserved (not inlined) + children_items = result["properties"]["nodes"]["items"]["properties"][ + "children" + ]["items"] + assert children_items["$ref"] == "#/properties/nodes/items" + def test_preserves_sibling_keywords(self): """Test that sibling keywords (default, description) are preserved.