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
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ def _resolve_basic_type(
return self._create_list_type(schema, defs)
if self._is_simple_object(schema):
return self._create_dict_type(schema, defs)
if self._is_inline_nested_object(schema):
return self._create_nested_model(schema, defs)
return json_type_mapping.get(json_type, str)


Expand Down Expand Up @@ -129,6 +131,26 @@ def _is_simple_object(self: "McpToolSpec", schema: dict) -> bool:
and isinstance(additional_props, dict)
)

def _is_inline_nested_object(self: "McpToolSpec", schema: dict) -> bool:
"""Check if schema is an inline nested object (has properties defined directly)."""
return schema.get("type") == "object" and "properties" in schema

def _create_nested_model(self: "McpToolSpec", schema: dict, defs: dict) -> type:
"""Create a nested Pydantic model from an inline object schema."""
import hashlib

# Generate a unique model name based on schema content
schema_str = str(sorted(schema.get("properties", {}).keys()))
schema_hash = hashlib.md5(schema_str.encode()).hexdigest()[:8]
model_name = f"NestedModel_{schema_hash}"

# Use the title from schema if available
if "title" in schema:
model_name = schema["title"]

# Create the nested model recursively
return self._create_model(schema, model_name, defs)

def _extract_ref_name(self: "McpToolSpec", ref_path: str) -> str:
"""Extract reference name from $ref path."""
return ref_path.split("#/$defs/")[-1]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -777,3 +777,63 @@ async def test_aget_tools_from_mcp_url_propagates_combined_params(
# Verify merged params
assert add_tool.partial_params == {"a": 1.0, "user_id": "global", "b": 2.0}
assert update_user_tool.partial_params == {"a": 1.0, "user_id": "global"}


# --- Tests for nested object schema handling ---


base_test_cases = [
pytest.param(
{
"properties": {
"nested": {
"properties": {
"value": {"title": "Value", "type": "string"},
},
"required": ["value"],
"type": "object",
},
},
"required": ["nested"],
"type": "object",
},
id="nested-object",
),
pytest.param(
{
"$defs": {
"Inner": {
"properties": {
"value": {"title": "Value", "type": "string"},
},
"required": ["value"],
"title": "Inner",
"type": "object",
},
},
"properties": {
"nested": {"$ref": "#/$defs/Inner"},
},
"required": ["nested"],
"type": "object",
},
id="referenced-nested-object",
),
]


@pytest.mark.parametrize(
"json_schema",
[
*base_test_cases,
],
)
def test_create_model_from_json_schema(client: BasicMCPClient, json_schema: dict):
"""Converting a JSON schema into a Pydantic model and back to a JSON schema should yield the original schema."""
tool_spec = McpToolSpec(client, allowed_tools=[])
pydantic_model = tool_spec.create_model_from_json_schema(json_schema)
json_schema_from_pydantic_model = pydantic_model.model_json_schema()

del json_schema_from_pydantic_model["title"]

assert json_schema == json_schema_from_pydantic_model