From 627c6cdad436cc6d81c268d8fc2c869e1b7e01da Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Wed, 7 Jan 2026 18:30:08 -0500 Subject: [PATCH] Dereference $ref in tool schemas for MCP client compatibility (#2808) --- pyproject.toml | 1 + src/fastmcp/tools/tool.py | 6 +- src/fastmcp/tools/tool_transform.py | 4 +- src/fastmcp/utilities/json_schema.py | 63 ++++- tests/tools/test_tool.py | 9 +- tests/tools/test_tool_transform.py | 94 ++++---- tests/utilities/openapi/test_schemas.py | 34 +-- tests/utilities/test_json_schema.py | 297 ++++-------------------- uv.lock | 11 + 9 files changed, 199 insertions(+), 320 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 22fdd6b68..593ddbf78 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ dependencies = [ "uvicorn>=0.35", "websockets>=15.0.1", "jsonschema-path>=0.3.4", + "jsonref>=1.1.0", ] requires-python = ">=3.10" diff --git a/src/fastmcp/tools/tool.py b/src/fastmcp/tools/tool.py index 538059dbc..88207221f 100644 --- a/src/fastmcp/tools/tool.py +++ b/src/fastmcp/tools/tool.py @@ -34,7 +34,7 @@ import fastmcp from fastmcp.server.dependencies import without_injected_parameters from fastmcp.server.tasks.config import TaskConfig, TaskMeta from fastmcp.utilities.components import FastMCPComponent -from fastmcp.utilities.json_schema import compress_schema, resolve_root_ref +from fastmcp.utilities.json_schema import compress_schema from fastmcp.utilities.logging import get_logger from fastmcp.utilities.types import ( Audio, @@ -686,10 +686,6 @@ class ParsedFunction: output_schema = compress_schema(output_schema, prune_titles=True) - # Resolve root-level $ref to meet MCP spec requirement for type: object - # Self-referential Pydantic models generate schemas with $ref at root - output_schema = resolve_root_ref(output_schema) - except PydanticSchemaGenerationError as e: if "_UnserializableType" not in str(e): logger.debug(f"Unable to generate schema for type {output_type!r}") diff --git a/src/fastmcp/tools/tool_transform.py b/src/fastmcp/tools/tool_transform.py index 92fa329cb..b05f86768 100644 --- a/src/fastmcp/tools/tool_transform.py +++ b/src/fastmcp/tools/tool_transform.py @@ -680,7 +680,7 @@ class TransformedTool(Tool): if parent_defs: schema["$defs"] = parent_defs - schema = compress_schema(schema, prune_defs=True) + schema = compress_schema(schema) # Create forwarding function that closes over everything it needs async def _forward(**kwargs: Any): @@ -863,7 +863,7 @@ class TransformedTool(Tool): if merged_defs: result["$defs"] = merged_defs - result = compress_schema(result, prune_defs=True) + result = compress_schema(result) return result diff --git a/src/fastmcp/utilities/json_schema.py b/src/fastmcp/utilities/json_schema.py index 7565c0dbb..81d054b64 100644 --- a/src/fastmcp/utilities/json_schema.py +++ b/src/fastmcp/utilities/json_schema.py @@ -3,6 +3,52 @@ from __future__ import annotations from collections import defaultdict from typing import Any +from jsonref import JsonRefError, replace_refs + + +def dereference_refs(schema: dict[str, Any]) -> dict[str, Any]: + """Resolve all $ref references in a JSON schema by inlining definitions. + + This function resolves $ref references that point to $defs, replacing them + with the actual definition content. This is necessary because some MCP clients + (e.g., VS Code Copilot) don't properly handle $ref in tool input schemas. + + For self-referencing/circular schemas where full dereferencing is not possible, + this function falls back to resolving only the root-level $ref while preserving + $defs for nested references. + + Args: + schema: JSON schema dict that may contain $ref references + + Returns: + A new schema dict with $ref resolved where possible and $defs removed + when no longer needed + + Example: + >>> schema = { + ... "$defs": {"Category": {"enum": ["a", "b"], "type": "string"}}, + ... "properties": {"cat": {"$ref": "#/$defs/Category"}} + ... } + >>> resolved = dereference_refs(schema) + >>> # Result: {"properties": {"cat": {"enum": ["a", "b"], "type": "string"}}} + """ + try: + # Use jsonref to resolve all $ref references + # proxies=False returns plain dicts (not proxy objects) + # lazy_load=False resolves immediately + dereferenced = replace_refs(schema, proxies=False, lazy_load=False) + + # Remove $defs since all references have been resolved + if isinstance(dereferenced, dict) and "$defs" in dereferenced: + dereferenced = {k: v for k, v in dereferenced.items() if k != "$defs"} + + return dereferenced + + except JsonRefError: + # Self-referencing/circular schemas can't be fully dereferenced + # Fall back to resolving only root-level $ref (for MCP spec compliance) + return resolve_root_ref(schema) + def resolve_root_ref(schema: dict[str, Any]) -> dict[str, Any]: """Resolve $ref at root level to meet MCP spec requirements. @@ -240,31 +286,38 @@ def _single_pass_optimize( def compress_schema( schema: dict, prune_params: list[str] | None = None, - prune_defs: bool = True, prune_additional_properties: bool = True, prune_titles: bool = False, ) -> dict: """ - Remove the given parameters from the schema. + Compress and optimize a JSON schema for MCP compatibility. + + This function dereferences all $ref entries (inlining definitions) to ensure + compatibility with MCP clients that don't properly handle $ref in schemas + (e.g., VS Code Copilot). It also applies various optimizations to reduce + schema size. Args: schema: The schema to compress prune_params: List of parameter names to remove from properties - prune_defs: Whether to remove unused definitions prune_additional_properties: Whether to remove additionalProperties: false prune_titles: Whether to remove title fields from the schema """ + # Dereference $ref - this inlines all definitions and removes $defs + # Required for MCP client compatibility + schema = dereference_refs(schema) + # Remove specific parameters if requested for param in prune_params or []: schema = _prune_param(schema, param=param) # Apply combined optimizations in a single tree traversal - if prune_titles or prune_additional_properties or prune_defs: + if prune_titles or prune_additional_properties: schema = _single_pass_optimize( schema, prune_titles=prune_titles, prune_additional_properties=prune_additional_properties, - prune_defs=prune_defs, + prune_defs=False, ) return schema diff --git a/tests/tools/test_tool.py b/tests/tools/test_tool.py index a7c32b9d4..b666f4cda 100644 --- a/tests/tools/test_tool.py +++ b/tests/tools/test_tool.py @@ -201,18 +201,15 @@ class TestToolFromFunction: "description": "Create a new user.", "tags": set(), "parameters": { - "$defs": { - "UserInput": { + "properties": { + "user": { "properties": { "name": {"type": "string"}, "age": {"type": "integer"}, }, "required": ["name", "age"], "type": "object", - } - }, - "properties": { - "user": {"$ref": "#/$defs/UserInput"}, + }, "flag": {"type": "boolean"}, }, "required": ["user", "flag"], diff --git a/tests/tools/test_tool_transform.py b/tests/tools/test_tool_transform.py index 89072401f..e0ac5aba5 100644 --- a/tests/tools/test_tool_transform.py +++ b/tests/tools/test_tool_transform.py @@ -218,10 +218,14 @@ async def test_hidden_param_prunes_defs(): schema = new_tool.parameters # Only 'a' should be visible assert list(schema["properties"].keys()) == ["a"] - # $defs should only contain VisibleType, not HiddenType - defs = schema.get("$defs", {}) - assert "VisibleType" in defs - assert "HiddenType" not in defs + # Schema should be fully dereferenced (no $defs) + assert "$defs" not in schema + # VisibleType should be inlined in the property + assert schema["properties"]["a"] == { + "properties": {"x": {"type": "integer"}}, + "required": ["x"], + "type": "object", + } async def test_forward_with_argument_mapping(add_tool): @@ -438,7 +442,10 @@ def test_transform_args_with_parent_defaults(): new_tool = Tool.from_tool(tool) - assert new_tool.parameters["$defs"] == tool.parameters["$defs"] + # Both tools should have the same dereferenced schema + assert new_tool.parameters == tool.parameters + # Schema should be fully dereferenced (no $defs) + assert "$defs" not in new_tool.parameters def test_transform_args_validation_unknown_arg(add_tool): @@ -1544,7 +1551,11 @@ class TestInputSchema: assert "examples" not in prop3 def test_merge_schema_with_defs_precedence(self): - """Test _merge_schema_with_precedence merges $defs correctly.""" + """Test _merge_schema_with_precedence merges $defs correctly. + + Note: This tests the raw merge behavior before dereferencing. + The final schema output will be dereferenced by compress_schema. + """ base_schema = { "type": "object", "properties": {"field1": {"$ref": "#/$defs/BaseType"}}, @@ -1567,26 +1578,27 @@ class TestInputSchema: base_schema, override_schema ) - # SharedType should no longer be present on the schema - assert "SharedType" not in transformed_tool_schema["$defs"] + # SharedType should no longer be present on the schema (unused) + assert "SharedType" not in transformed_tool_schema.get("$defs", {}) + # Schema is dereferenced so no $defs in final output assert transformed_tool_schema == snapshot( { "type": "object", "properties": { - "field1": {"$ref": "#/$defs/BaseType"}, - "field2": {"$ref": "#/$defs/OverrideType"}, + "field1": {"type": "string", "description": "base"}, + "field2": {"type": "boolean"}, }, "required": [], - "$defs": { - "BaseType": {"type": "string", "description": "base"}, - "OverrideType": {"type": "boolean"}, - }, } ) def test_transform_tool_with_complex_defs_pruning(self): - """Test that tool transformation properly prunes unused $defs.""" + """Test that tool transformation properly handles hidden params. + + With schema dereferencing, unused types are automatically removed + since $defs is eliminated entirely. + """ class UsedType(BaseModel): value: str @@ -1605,25 +1617,25 @@ class TestInputSchema: complex_tool, transform_args={"unused_param": ArgTransform(hide=True)} ) - assert "UnusedType" not in transformed_tool.parameters["$defs"] + # Schema is dereferenced - no $defs + assert "$defs" not in transformed_tool.parameters assert transformed_tool.parameters == snapshot( { "type": "object", - "properties": {"used_param": {"$ref": "#/$defs/UsedType"}}, - "required": ["used_param"], - "$defs": { - "UsedType": { + "properties": { + "used_param": { "properties": {"value": {"type": "string"}}, "required": ["value"], "type": "object", } }, + "required": ["used_param"], } ) - def test_transform_with_custom_function_preserves_needed_defs(self): - """Test that custom transform functions preserve necessary $defs.""" + def test_transform_with_custom_function_preserves_needed_types(self): + """Test that custom transform functions preserve necessary types inline.""" class InputType(BaseModel): data: str @@ -1645,23 +1657,25 @@ class TestInputSchema: transform_args={"input_data": ArgTransform(name="renamed_input")}, ) + # Schema is dereferenced - types are inlined + assert "$defs" not in transformed.parameters + assert transformed.parameters == snapshot( { "type": "object", - "properties": {"renamed_input": {"$ref": "#/$defs/InputType"}}, - "required": ["renamed_input"], - "$defs": { - "InputType": { + "properties": { + "renamed_input": { "properties": {"data": {"type": "string"}}, "required": ["data"], "type": "object", } }, + "required": ["renamed_input"], } ) - def test_chained_transforms_preserve_correct_defs(self): - """Test that chained transformations preserve correct $defs.""" + def test_chained_transforms_inline_types(self): + """Test that chained transformations produce correct inlined schemas.""" class TypeA(BaseModel): a: str @@ -1682,50 +1696,46 @@ class TestInputSchema: transform_args={"param_c": ArgTransform(hide=True, default=TypeC(c=True))}, ) + # Schema is dereferenced - types are inlined + assert "$defs" not in transform1.parameters + assert transform1.parameters == snapshot( { "type": "object", "properties": { - "param_a": {"$ref": "#/$defs/TypeA"}, - "param_b": {"$ref": "#/$defs/TypeB"}, - }, - "required": IsList("param_b", "param_a", check_order=False), - "$defs": { - "TypeA": { + "param_a": { "properties": {"a": {"type": "string"}}, "required": ["a"], "type": "object", }, - "TypeB": { + "param_b": { "properties": {"b": {"type": "integer"}}, "required": ["b"], "type": "object", }, }, + "required": IsList("param_b", "param_a", check_order=False), } ) - assert "TypeA" in transform1.parameters["$defs"] - # Second transform: hide param_b transform2 = Tool.from_tool( transform1, transform_args={"param_b": ArgTransform(hide=True, default=TypeB(b=42))}, ) - assert "TypeB" not in transform2.parameters["$defs"] + assert "$defs" not in transform2.parameters assert transform2.parameters == snapshot( { "type": "object", - "properties": {"param_a": {"$ref": "#/$defs/TypeA"}}, - "required": ["param_a"], - "$defs": { - "TypeA": { + "properties": { + "param_a": { "properties": {"a": {"type": "string"}}, "required": ["a"], "type": "object", } }, + "required": ["param_a"], } ) diff --git a/tests/utilities/openapi/test_schemas.py b/tests/utilities/openapi/test_schemas.py index eebcaecc2..54644bce6 100644 --- a/tests/utilities/openapi/test_schemas.py +++ b/tests/utilities/openapi/test_schemas.py @@ -580,8 +580,8 @@ class TestEdgeCases: len(properties) > 0 ) # Should have some properties from one of the content types - def test_oneof_reference_preserved(self): - """Test that schemas referenced in oneOf are preserved.""" + def test_oneof_reference_dereferenced(self): + """Test that schemas referenced in oneOf are dereferenced.""" schema = { "type": "object", @@ -594,14 +594,14 @@ class TestEdgeCases: result = compress_schema(schema) - # TestSchema should be preserved (referenced in oneOf) - assert "TestSchema" in result["$defs"] + # $defs should be removed (all refs dereferenced) + assert "$defs" not in result - # UnusedSchema should be removed - assert "UnusedSchema" not in result["$defs"] + # TestSchema should be inlined in oneOf + assert result["properties"]["data"]["oneOf"] == [{"type": "string"}] - def test_anyof_reference_preserved(self): - """Test that schemas referenced in anyOf are preserved.""" + def test_anyof_reference_dereferenced(self): + """Test that schemas referenced in anyOf are dereferenced.""" schema = { "type": "object", @@ -614,11 +614,14 @@ class TestEdgeCases: result = compress_schema(schema) - assert "TestSchema" in result["$defs"] - assert "UnusedSchema" not in result["$defs"] + # $defs should be removed (all refs dereferenced) + assert "$defs" not in result - def test_allof_reference_preserved(self): - """Test that schemas referenced in allOf are preserved.""" + # TestSchema should be inlined in anyOf + assert result["properties"]["data"]["anyOf"] == [{"type": "string"}] + + def test_allof_reference_dereferenced(self): + """Test that schemas referenced in allOf are dereferenced.""" schema = { "type": "object", @@ -631,5 +634,8 @@ class TestEdgeCases: result = compress_schema(schema) - assert "TestSchema" in result["$defs"] - assert "UnusedSchema" not in result["$defs"] + # $defs should be removed (all refs dereferenced) + assert "$defs" not in result + + # TestSchema should be inlined in allOf + assert result["properties"]["data"]["allOf"] == [{"type": "string"}] diff --git a/tests/utilities/test_json_schema.py b/tests/utilities/test_json_schema.py index 00162e2c1..f5174effa 100644 --- a/tests/utilities/test_json_schema.py +++ b/tests/utilities/test_json_schema.py @@ -1,18 +1,10 @@ from fastmcp.utilities.json_schema import ( _prune_param, compress_schema, + dereference_refs, resolve_root_ref, ) -# Wrapper for backward compatibility with tests - - -def _prune_additional_properties(schema): - """Wrapper for compress_schema that only prunes additionalProperties: false.""" - return compress_schema( - schema, prune_defs=False, prune_additional_properties=True, prune_titles=False - ) - class TestPruneParam: """Tests for the _prune_param function.""" @@ -55,32 +47,28 @@ class TestPruneParam: assert "required" not in result -class TestPruneUnusedDefs: - """Tests for unused definition pruning (via compress_schema).""" +class TestDereferenceRefs: + """Tests for the dereference_refs function.""" - def test_removes_unreferenced_defs(self): - """Test that unreferenced definitions are removed.""" + def test_dereferences_simple_ref(self): + """Test that simple $ref is dereferenced.""" schema = { "properties": { "foo": {"$ref": "#/$defs/foo_def"}, }, "$defs": { "foo_def": {"type": "string"}, - "unused_def": {"type": "integer"}, }, } - result = compress_schema( - schema, - prune_defs=True, - prune_additional_properties=False, - prune_titles=False, - ) + result = dereference_refs(schema) - assert "foo_def" in result["$defs"] - assert "unused_def" not in result["$defs"] + # $ref should be inlined + assert result["properties"]["foo"] == {"type": "string"} + # $defs should be removed + assert "$defs" not in result - def test_nested_references_kept(self): - """Test that definitions referenced via nesting are kept.""" + def test_dereferences_nested_refs(self): + """Test that nested $refs are dereferenced.""" schema = { "properties": { "foo": {"$ref": "#/$defs/foo_def"}, @@ -91,210 +79,59 @@ class TestPruneUnusedDefs: "properties": {"nested": {"$ref": "#/$defs/nested_def"}}, }, "nested_def": {"type": "string"}, - "unused_def": {"type": "integer"}, }, } - result = compress_schema( - schema, - prune_defs=True, - prune_additional_properties=False, - prune_titles=False, - ) - assert "foo_def" in result["$defs"] - assert "nested_def" in result["$defs"] - assert "unused_def" not in result["$defs"] + result = dereference_refs(schema) - def test_nested_references_removed(self): - """Test that definitions referenced via nesting in unused defs are removed.""" - schema = { - "properties": {}, - "$defs": { - "foo_def": { - "type": "object", - "properties": {"nested": {"$ref": "#/$defs/nested_def"}}, - }, - "nested_def": {"type": "string"}, - }, - } - result = compress_schema( - schema, - prune_defs=True, - prune_additional_properties=False, - prune_titles=False, - ) + # All refs should be inlined + assert result["properties"]["foo"]["properties"]["nested"] == {"type": "string"} + # $defs should be removed assert "$defs" not in result - def test_nested_references_with_recursion_kept(self): - """Test that definitions with recursion referenced via nesting are kept.""" + def test_falls_back_for_circular_refs(self): + """Test that circular references fall back to resolve_root_ref.""" schema = { - "properties": { - "foo": {"$ref": "#/$defs/foo_def"}, - }, "$defs": { - "foo_def": { + "Node": { "type": "object", - "properties": {"nested": {"$ref": "#/$defs/foo_def"}}, - }, - "unused_def": {"type": "integer"}, + "properties": { + "children": { + "type": "array", + "items": {"$ref": "#/$defs/Node"}, + } + }, + } }, + "$ref": "#/$defs/Node", } - result = compress_schema( - schema, - prune_defs=True, - prune_additional_properties=False, - prune_titles=False, - ) - assert "foo_def" in result["$defs"] - assert "unused_def" not in result["$defs"] + result = dereference_refs(schema) - def test_nested_references_with_recursion_removed(self): - """Test that definitions with recursion referenced via nesting in unused defs are removed.""" - schema = { - "properties": {}, - "$defs": { - "foo_def": { - "type": "object", - "properties": {"nested": {"$ref": "#/$defs/foo_def"}}, - }, - }, - } - result = compress_schema( - schema, - prune_defs=True, - prune_additional_properties=False, - prune_titles=False, - ) - assert "$defs" not in result - - def test_multiple_nested_references_with_recursion_kept(self): - """Test that definitions with multiple levels of recursion referenced via nesting are kept.""" - schema = { - "properties": { - "foo": {"$ref": "#/$defs/foo_def"}, - }, - "$defs": { - "foo_def": { - "type": "object", - "properties": {"nested": {"$ref": "#/$defs/nested_def"}}, - }, - "nested_def": { - "type": "object", - "properties": {"nested": {"$ref": "#/$defs/foo_def"}}, - }, - "unused_def": {"type": "integer"}, - }, - } - result = compress_schema( - schema, - prune_defs=True, - prune_additional_properties=False, - prune_titles=False, - ) - assert "foo_def" in result["$defs"] - assert "nested_def" in result["$defs"] - assert "unused_def" not in result["$defs"] - - def test_multiple_nested_references_with_recursion_removed(self): - """Test that definitions with multiple levels of recursion referenced via nesting in unused defs are removed.""" - schema = { - "properties": {}, - "$defs": { - "foo_def": { - "type": "object", - "properties": {"nested": {"$ref": "#/$defs/nested_def"}}, - }, - "nested_def": { - "type": "object", - "properties": {"nested": {"$ref": "#/$defs/foo_def"}}, - }, - }, - } - result = compress_schema( - schema, - prune_defs=True, - prune_additional_properties=False, - prune_titles=False, - ) - assert "$defs" not in result - - def test_array_references_kept(self): - """Test that definitions referenced in array items are kept.""" - schema = { - "properties": { - "items": {"type": "array", "items": {"$ref": "#/$defs/item_def"}}, - }, - "$defs": { - "item_def": {"type": "string"}, - "unused_def": {"type": "integer"}, - }, - } - result = compress_schema( - schema, - prune_defs=True, - prune_additional_properties=False, - prune_titles=False, - ) - assert "item_def" in result["$defs"] - assert "unused_def" not in result["$defs"] - - def test_removes_defs_field_when_empty(self): - """Test that $defs field is removed when all definitions are unused.""" - schema = { - "properties": { - "foo": {"type": "string"}, - }, - "$defs": { - "unused_def": {"type": "integer"}, - }, - } - result = compress_schema( - schema, - prune_defs=True, - prune_additional_properties=False, - prune_titles=False, - ) - assert "$defs" not in result - - -class TestPruneAdditionalProperties: - """Tests for the _prune_additional_properties function.""" - - def test_removes_when_false(self): - """Test that additionalProperties is removed when it's false.""" - schema = { - "type": "object", - "properties": {"foo": {"type": "string"}}, - "additionalProperties": False, - } - result = _prune_additional_properties(schema) - assert "additionalProperties" not in result - - def test_keeps_when_true(self): - """Test that additionalProperties is kept when it's true.""" - schema = { - "type": "object", - "properties": {"foo": {"type": "string"}}, - "additionalProperties": True, - } - result = _prune_additional_properties(schema) - assert "additionalProperties" in result - assert result["additionalProperties"] is True - - def test_keeps_when_object(self): - """Test that additionalProperties is kept when it's an object schema.""" - schema = { - "type": "object", - "properties": {"foo": {"type": "string"}}, - "additionalProperties": {"type": "string"}, - } - result = _prune_additional_properties(schema) - assert "additionalProperties" in result - assert result["additionalProperties"] == {"type": "string"} + # Should fall back to resolve_root_ref behavior + # Root should be resolved but nested refs preserved + assert result.get("type") == "object" + assert "$defs" in result # $defs preserved for circular refs class TestCompressSchema: """Tests for the compress_schema function.""" + def test_dereferences_by_default(self): + """Test that compress_schema dereferences $refs by default.""" + schema = { + "properties": { + "foo": {"$ref": "#/$defs/foo_def"}, + }, + "$defs": { + "foo_def": {"type": "string"}, + }, + } + result = compress_schema(schema) + + # $ref should be inlined + assert result["properties"]["foo"] == {"type": "string"} + # $defs should be removed + assert "$defs" not in result + def test_prune_params(self): """Test pruning parameters with compress_schema.""" schema = { @@ -309,38 +146,6 @@ class TestCompressSchema: assert result["properties"] == {"bar": {"type": "integer"}} assert result["required"] == ["bar"] - def test_prune_defs(self): - """Test pruning unused definitions with compress_schema.""" - schema = { - "properties": { - "foo": {"$ref": "#/$defs/foo_def"}, - "bar": {"type": "integer"}, - }, - "$defs": { - "foo_def": {"type": "string"}, - "unused_def": {"type": "number"}, - }, - } - result = compress_schema(schema) - assert "foo_def" in result["$defs"] - assert "unused_def" not in result["$defs"] - - def test_disable_prune_defs(self): - """Test disabling pruning of unused definitions.""" - schema = { - "properties": { - "foo": {"$ref": "#/$defs/foo_def"}, - "bar": {"type": "integer"}, - }, - "$defs": { - "foo_def": {"type": "string"}, - "unused_def": {"type": "number"}, - }, - } - result = compress_schema(schema, prune_defs=False) - assert "foo_def" in result["$defs"] - assert "unused_def" in result["$defs"] - def test_pruning_additional_properties(self): """Test pruning additionalProperties when False.""" schema = { @@ -382,8 +187,8 @@ class TestCompressSchema: assert "remove" not in result["properties"] # Check that required list was updated assert result["required"] == ["keep"] - # Check that unused definitions were removed - assert "$defs" not in result # Both defs should be gone + # Check that $defs was removed (dereferenced) + assert "$defs" not in result # Check that additionalProperties was removed assert "additionalProperties" not in result diff --git a/uv.lock b/uv.lock index 4419cd02d..092d02bdf 100644 --- a/uv.lock +++ b/uv.lock @@ -689,6 +689,7 @@ dependencies = [ { name = "cyclopts" }, { name = "exceptiongroup" }, { name = "httpx" }, + { name = "jsonref" }, { name = "jsonschema-path" }, { name = "mcp" }, { name = "openapi-pydantic" }, @@ -745,6 +746,7 @@ requires-dist = [ { name = "cyclopts", specifier = ">=4.0.0" }, { name = "exceptiongroup", specifier = ">=1.2.2" }, { name = "httpx", specifier = ">=0.28.1" }, + { name = "jsonref", specifier = ">=1.1.0" }, { name = "jsonschema-path", specifier = ">=0.3.4" }, { name = "mcp", specifier = ">=1.24.0,<2.0" }, { name = "openai", marker = "extra == 'openai'", specifier = ">=1.102.0" }, @@ -1100,6 +1102,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110, upload-time = "2025-11-09T20:49:21.817Z" }, ] +[[package]] +name = "jsonref" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/0d/c1f3277e90ccdb50d33ed5ba1ec5b3f0a242ed8c1b1a85d3afeb68464dca/jsonref-1.1.0.tar.gz", hash = "sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552", size = 8814, upload-time = "2023-01-16T16:10:04.455Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/ec/e1db9922bceb168197a558a2b8c03a7963f1afe93517ddd3cf99f202f996/jsonref-1.1.0-py3-none-any.whl", hash = "sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9", size = 9425, upload-time = "2023-01-16T16:10:02.255Z" }, +] + [[package]] name = "jsonschema" version = "4.25.1"