Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/fastmcp/utilities/json_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,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)
Comment on lines +175 to 180
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exception-block comment says the fallback “resolv[es] only root-level $ref”, but resolve_root_ref() is a no-op when there is no root $ref (as in the JSON Pointer cycle case). Consider rewording this comment to reflect the actual behavior: fall back to resolve_root_ref() (which may leave the schema unchanged) rather than implying root refs will always be resolved.

Copilot uses AI. Check for mistakes.

Expand Down
36 changes: 36 additions & 0 deletions tests/utilities/test_json_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,42 @@ def test_falls_back_for_circular_refs(self):
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 "$ref" in children_items
Comment on lines +149 to +153
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regression test only asserts that a $ref key exists in children_items, but it doesn’t verify that the pointer-style circular reference is preserved exactly (e.g. that the value remains #/properties/nodes/items). Strengthen the assertion to check the $ref value so the test will fail if dereferencing rewrites or partially inlines the reference.

Suggested change
# The circular $ref should be preserved (not inlined)
children_items = result["properties"]["nodes"]["items"]["properties"][
"children"
]["items"]
assert "$ref" in children_items
# The circular $ref should be preserved exactly (not inlined or rewritten)
children_items = result["properties"]["nodes"]["items"]["properties"][
"children"
]["items"]
assert children_items["$ref"] == "#/properties/nodes/items"

Copilot uses AI. Check for mistakes.

def test_preserves_sibling_keywords(self):
"""Test that sibling keywords (default, description) are preserved.
Expand Down
Loading