Give recursive tool return types an object-rooted output schema (#3376)
This commit is contained in:
parent
56af44727b
commit
d8b638304e
2 changed files with 72 additions and 1 deletions
|
|
@ -1202,6 +1202,34 @@ def test_structured_output_nested_models():
|
|||
}
|
||||
|
||||
|
||||
def test_structured_output_self_referential_model_gets_an_object_root():
|
||||
"""pydantic publishes a recursive model as a bare root `$ref`; the definition is inlined onto the
|
||||
root and `$defs` is kept for the nested reference."""
|
||||
|
||||
class Node(BaseModel):
|
||||
name: str
|
||||
children: list["Node"] = []
|
||||
|
||||
def tree() -> Node:
|
||||
return Node(name="root", children=[Node(name="leaf")])
|
||||
|
||||
node_definition: dict[str, Any] = {
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"children": {"default": [], "items": {"$ref": "#/$defs/Node"}, "title": "Children", "type": "array"},
|
||||
},
|
||||
"required": ["name"],
|
||||
"title": "Node",
|
||||
"type": "object",
|
||||
}
|
||||
meta = func_metadata(tree)
|
||||
assert meta.output_schema == {**node_definition, "$defs": {"Node": node_definition}}
|
||||
|
||||
result = meta.convert_result(tree())
|
||||
assert isinstance(result, CallToolResult)
|
||||
assert result.structured_content == {"name": "root", "children": [{"name": "leaf", "children": []}]}
|
||||
|
||||
|
||||
def test_structured_output_unserializable_type_error():
|
||||
"""Test error when structured_output=True is used with unserializable types"""
|
||||
|
||||
|
|
|
|||
|
|
@ -2167,6 +2167,29 @@ async def test_prompt_input_required_result_on_legacy_session_is_a_serialization
|
|||
assert exc.value.error.message == "Handler returned an invalid result"
|
||||
|
||||
|
||||
async def test_recursive_tool_return_type_lists_and_calls_on_legacy_session():
|
||||
"""A 2025-11-25 session requires `type: object` at the outputSchema root; a self-referential
|
||||
return type must not fail the whole listing, and its result validates client-side via `$defs`."""
|
||||
|
||||
class Node(BaseModel):
|
||||
name: str
|
||||
children: list["Node"] = []
|
||||
|
||||
mcp = MCPServer()
|
||||
|
||||
@mcp.tool()
|
||||
def tree() -> Node:
|
||||
return Node(name="root", children=[Node(name="leaf")])
|
||||
|
||||
async with Client(mcp, mode="legacy") as client:
|
||||
[tool] = (await client.list_tools()).tools
|
||||
assert tool.output_schema is not None
|
||||
assert tool.output_schema["type"] == "object"
|
||||
assert tool.output_schema["properties"]["children"]["items"] == {"$ref": "#/$defs/Node"}
|
||||
result = await client.call_tool("tree", {})
|
||||
assert result.structured_content == {"name": "root", "children": [{"name": "leaf", "children": []}]}
|
||||
|
||||
|
||||
async def test_resource_template_input_required_result_on_legacy_session_is_a_serialization_error():
|
||||
"""Pins the shared era gate for resources/read: a pre-2026 session has no
|
||||
input_required vocabulary, so the runner rejects the frame with -32603."""
|
||||
|
|
|
|||
Loading…
Reference in a new issue