Skip to content

Commit e316fcc

Browse files
committed
fix(func_metadata): give bare dict/list/tuple returns a structured output model
A tool annotated with a bare `dict` (or `list` / `tuple`) produced no output model: those classes have no type hints, so they reached the generic class branch, `get_type_hints()` came back empty and the model stayed None. The tool therefore advertised no outputSchema and returned no structuredContent, and `structured_output=True` raised InvalidSignature claiming the return type was not serializable for structured output. Treat bare containers like their parameterised equivalents (wrapped in {"result": ...}), which is what `dict[str, Any]` already did. Fixes #3725
1 parent 2af9e30 commit e316fcc

2 files changed

Lines changed: 36 additions & 0 deletions

File tree

‎src/mcp/server/mcpserver/utilities/func_metadata.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,15 @@ def _create_output_model(original_annotation: Any, type_expr: Any, func_name: st
535535
model = _create_wrapped_model(func_name, original_annotation)
536536
wrap_output = True
537537

538+
# Case 3b: Bare containers. A bare `dict` / `list` / `tuple` carries no type
539+
# hints, so it used to fall through to Case 4, where `get_type_hints()` is
540+
# empty and the model stayed None: the tool silently lost its outputSchema and
541+
# structuredContent. Treat them as their parameterised equivalents, which is
542+
# what the documented behaviour promises (`dict` == `dict[Any, Any]`).
543+
elif type_annotation in (dict, list, tuple):
544+
model = _create_wrapped_model(func_name, original_annotation)
545+
wrap_output = True
546+
538547
# Case 4: Other class types (dataclasses, regular classes with annotations)
539548
else:
540549
type_hints = get_type_hints(type_annotation)

‎tests/server/mcpserver/test_func_metadata.py‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1520,3 +1520,30 @@ def fn() -> StepA | StepB: ... # pragma: no branch
15201520

15211521
meta = func_metadata(fn)
15221522
assert meta.output_schema is None
1523+
1524+
@pytest.mark.parametrize("annotation", [dict, list, tuple])
1525+
def test_bare_container_return_annotations_get_a_wrapped_output_model(annotation: Any) -> None:
1526+
"""A bare `dict` / `list` / `tuple` must not silently opt out of structured output.
1527+
1528+
They carry no type hints, so the generic class branch produced no model at all:
1529+
no outputSchema, no structuredContent, and `structured_output=True` raised
1530+
InvalidSignature claiming the type was not serializable.
1531+
"""
1532+
1533+
def func() -> annotation: # type: ignore[valid-type]
1534+
return {} if annotation is dict else []
1535+
1536+
metadata = func_metadata(func)
1537+
assert metadata.output_model is not None, "bare container annotation lost its output model"
1538+
assert metadata.wrap_output is True
1539+
1540+
1541+
def test_bare_dict_structured_output_roundtrips() -> None:
1542+
def func() -> dict: # type: ignore[type-arg]
1543+
return {"a": 1}
1544+
1545+
metadata = func_metadata(func, structured_output=True)
1546+
assert metadata.output_model is not None
1547+
validated = metadata.output_model.model_validate(func())
1548+
assert validated.model_dump()["result"] == {"a": 1}
1549+

0 commit comments

Comments
 (0)