Skip to content
Closed
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
9 changes: 9 additions & 0 deletions src/mcp/server/mcpserver/utilities/func_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,15 @@ def _create_output_model(original_annotation: Any, type_expr: Any, func_name: st
model = _create_wrapped_model(func_name, original_annotation)
wrap_output = True

# Case 3b: Bare containers. A bare `dict` / `list` / `tuple` carries no type
# hints, so it used to fall through to Case 4, where `get_type_hints()` is
# empty and the model stayed None: the tool silently lost its outputSchema and
# structuredContent. Treat them as their parameterised equivalents, which is
# what the documented behaviour promises (`dict` == `dict[Any, Any]`).
elif type_annotation in (dict, list, tuple):
model = _create_wrapped_model(func_name, original_annotation)
wrap_output = True

# Case 4: Other class types (dataclasses, regular classes with annotations)
else:
type_hints = get_type_hints(type_annotation)
Expand Down
27 changes: 27 additions & 0 deletions tests/server/mcpserver/test_func_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -1520,3 +1520,30 @@ def fn() -> StepA | StepB: ... # pragma: no branch

meta = func_metadata(fn)
assert meta.output_schema is None

@pytest.mark.parametrize("annotation", [dict, list, tuple])
def test_bare_container_return_annotations_get_a_wrapped_output_model(annotation: Any) -> None:
"""A bare `dict` / `list` / `tuple` must not silently opt out of structured output.

They carry no type hints, so the generic class branch produced no model at all:
no outputSchema, no structuredContent, and `structured_output=True` raised
InvalidSignature claiming the type was not serializable.
"""

def func() -> annotation: # type: ignore[valid-type]
return {} if annotation is dict else []

metadata = func_metadata(func)
assert metadata.output_model is not None, "bare container annotation lost its output model"
assert metadata.wrap_output is True


def test_bare_dict_structured_output_roundtrips() -> None:
def func() -> dict: # type: ignore[type-arg]
return {"a": 1}

metadata = func_metadata(func, structured_output=True)
assert metadata.output_model is not None
validated = metadata.output_model.model_validate(func())
assert validated.model_dump()["result"] == {"a": 1}

Loading