From 9d07811d75789c7168e16e8df0eeaf80dd6673a9 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 24 Oct 2026 15:43:03 +0000 Subject: [PATCH 1/3] Treat `typing.Dict[str, X]` return annotations like `dict[str, X]` --- .../mcpserver/utilities/func_metadata.py | 9 ++------ tests/server/mcpserver/test_func_metadata.py | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/mcp/server/mcpserver/utilities/func_metadata.py b/src/mcp/server/mcpserver/utilities/func_metadata.py index 96d25ec5ab..e0b7bdb27e 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 @@ -499,10 +498,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: @@ -545,8 +542,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 c707591287..c2e662d8aa 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 @@ -295,6 +296,15 @@ def func_dict_list() -> dict[str, list[int]]: # pragma: no cover "title": "func_dict_listDictOutput", } + def func_typing_dict() -> typing.Dict[str, str]: # noqa: UP006 # pragma: no cover + return {"name": "John"} + + assert func_metadata(func_typing_dict).output_schema == { + "type": "object", + "additionalProperties": {"type": "string"}, + "title": "func_typing_dictDictOutput", + } + # 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"} @@ -774,6 +784,19 @@ def func_optional() -> str | None: # pragma: no cover "title": "func_optionalOutput", } + UserId = typing.NewType("UserId", int) + + def func_new_type() -> UserId: # pragma: no cover + return UserId(1) + + meta = func_metadata(func_new_type) + assert meta.output_schema == { + "type": "object", + "properties": {"result": {"title": "Result", "type": "integer"}}, + "required": ["result"], + "title": "func_new_typeOutput", + } + def test_structured_output_dataclass(): """Test structured output with dataclass return types""" From fb9f5d1bbca5adb9f38e331839095cc2b6e086dc Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 24 Oct 2026 16:53:39 +0000 Subject: [PATCH 2/3] Assert structured content for `typing.Dict` and `NewType` returns --- docs/servers/structured-output.md | 2 +- tests/server/mcpserver/test_func_metadata.py | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/servers/structured-output.md b/docs/servers/structured-output.md index fbe09ba817..d276f77c07 100644 --- a/docs/servers/structured-output.md +++ b/docs/servers/structured-output.md @@ -149,7 +149,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/tests/server/mcpserver/test_func_metadata.py b/tests/server/mcpserver/test_func_metadata.py index c2e662d8aa..6d6f8a5f8f 100644 --- a/tests/server/mcpserver/test_func_metadata.py +++ b/tests/server/mcpserver/test_func_metadata.py @@ -296,14 +296,18 @@ def func_dict_list() -> dict[str, list[int]]: # pragma: no cover "title": "func_dict_listDictOutput", } - def func_typing_dict() -> typing.Dict[str, str]: # noqa: UP006 # pragma: no cover + def func_typing_dict() -> typing.Dict[str, str]: # noqa: UP006 return {"name": "John"} - assert func_metadata(func_typing_dict).output_schema == { + meta = func_metadata(func_typing_dict) + assert meta.output_schema == { "type": "object", "additionalProperties": {"type": "string"}, "title": "func_typing_dictDictOutput", } + 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 @@ -786,7 +790,7 @@ def func_optional() -> str | None: # pragma: no cover UserId = typing.NewType("UserId", int) - def func_new_type() -> UserId: # pragma: no cover + def func_new_type() -> UserId: return UserId(1) meta = func_metadata(func_new_type) @@ -796,6 +800,9 @@ def func_new_type() -> UserId: # pragma: no cover "required": ["result"], "title": "func_new_typeOutput", } + result = meta.convert_result(func_new_type()) + assert isinstance(result, CallToolResult) + assert result.structured_content == {"result": 1} def test_structured_output_dataclass(): From c38a6525d5f2a87de5a8e7a7301cb08565c042a6 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 24 Oct 2026 17:45:21 +0000 Subject: [PATCH 3/3] Drop generated titles from the new schema expectations --- tests/server/mcpserver/test_func_metadata.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/server/mcpserver/test_func_metadata.py b/tests/server/mcpserver/test_func_metadata.py index a8c54ec644..5dc67808d9 100644 --- a/tests/server/mcpserver/test_func_metadata.py +++ b/tests/server/mcpserver/test_func_metadata.py @@ -301,7 +301,6 @@ def func_typing_dict() -> typing.Dict[str, str]: # noqa: UP006 assert meta.output_schema == { "type": "object", "additionalProperties": {"type": "string"}, - "title": "func_typing_dictDictOutput", } result = meta.convert_result(func_typing_dict()) assert isinstance(result, CallToolResult) @@ -784,9 +783,8 @@ def func_new_type() -> UserId: meta = func_metadata(func_new_type) assert meta.output_schema == { "type": "object", - "properties": {"result": {"title": "Result", "type": "integer"}}, + "properties": {"result": {"type": "integer"}}, "required": ["result"], - "title": "func_new_typeOutput", } result = meta.convert_result(func_new_type()) assert isinstance(result, CallToolResult)