diff --git a/docs/servers/structured-output.md b/docs/servers/structured-output.md index 35c2585616..1dcf1907cc 100644 --- a/docs/servers/structured-output.md +++ b/docs/servers/structured-output.md @@ -147,7 +147,7 @@ Ask for a two-day forecast and `structured_content` is `{"result": [{...}, {...} ## Dictionaries -`dict[str, ...]` is the one generic that already *is* a JSON object, so it isn't wrapped: +`dict[str, ...]` (or `typing.Dict[str, ...]`) is the one generic that already *is* a JSON object, so it isn't wrapped: ```python title="server.py" hl_lines="9" --8<-- "docs_src/structured_output/tutorial006.py" diff --git a/src/mcp/server/mcpserver/utilities/func_metadata.py b/src/mcp/server/mcpserver/utilities/func_metadata.py index 995b89a0a1..5ef4173174 100644 --- a/src/mcp/server/mcpserver/utilities/func_metadata.py +++ b/src/mcp/server/mcpserver/utilities/func_metadata.py @@ -4,7 +4,6 @@ import sys from collections.abc import Awaitable, Callable, Sequence from itertools import chain -from types import GenericAlias from typing import Annotated, Any, NotRequired, Union, cast, get_args, get_origin import anyio @@ -512,10 +511,8 @@ def _create_output_model(original_annotation: Any, type_expr: Any, func_name: st model = _create_wrapped_model(func_name, original_annotation) wrap_output = True - # Handle GenericAlias types (list[str], dict[str, int], Union[str, int], etc.) - elif isinstance(type_expr, GenericAlias): - origin = get_origin(type_expr) - + # Handle parameterized generics (list[str], typing.Dict[str, int], Union[str, int], etc.) + elif (origin := get_origin(type_expr)) is not None: if origin is dict: args = get_args(type_expr) if len(args) == 2 and args[0] is str: @@ -558,8 +555,6 @@ def _create_output_model(original_annotation: Any, type_expr: Any, func_name: st # Handle any other types not covered above else: - # This includes typing constructs that aren't GenericAlias in Python 3.10 - # (e.g., Union, Optional in some Python versions) model = _create_wrapped_model(func_name, original_annotation) wrap_output = True diff --git a/tests/server/mcpserver/test_func_metadata.py b/tests/server/mcpserver/test_func_metadata.py index fe6c7211ac..5dc67808d9 100644 --- a/tests/server/mcpserver/test_func_metadata.py +++ b/tests/server/mcpserver/test_func_metadata.py @@ -3,6 +3,7 @@ # pyright: reportMissingParameterType=false # pyright: reportUnknownArgumentType=false # pyright: reportUnknownLambdaType=false +import typing from collections.abc import Callable from dataclasses import dataclass from typing import TYPE_CHECKING, Annotated, Any, Final, NamedTuple, NotRequired, Required, TypedDict @@ -293,6 +294,18 @@ def func_dict_list() -> dict[str, list[int]]: # pragma: no cover "additionalProperties": {"type": "array", "items": {"type": "integer"}}, } + def func_typing_dict() -> typing.Dict[str, str]: # noqa: UP006 + return {"name": "John"} + + meta = func_metadata(func_typing_dict) + assert meta.output_schema == { + "type": "object", + "additionalProperties": {"type": "string"}, + } + result = meta.convert_result(func_typing_dict()) + assert isinstance(result, CallToolResult) + assert result.structured_content == {"name": "John"} + # Test dict[int, str] - should be wrapped since key is not str def func_dict_int_key() -> dict[int, str]: # pragma: no cover return {1: "a", 2: "b"} @@ -762,6 +775,21 @@ def func_optional() -> str | None: # pragma: no cover "required": ["result"], } + UserId = typing.NewType("UserId", int) + + def func_new_type() -> UserId: + return UserId(1) + + meta = func_metadata(func_new_type) + assert meta.output_schema == { + "type": "object", + "properties": {"result": {"type": "integer"}}, + "required": ["result"], + } + result = meta.convert_result(func_new_type()) + assert isinstance(result, CallToolResult) + assert result.structured_content == {"result": 1} + def test_structured_output_dataclass(): """Test structured output with dataclass return types"""