Skip to content

Commit 9d07811

Browse files
committed
Treat typing.Dict[str, X] return annotations like dict[str, X]
1 parent 910efd9 commit 9d07811

2 files changed

Lines changed: 25 additions & 7 deletions

File tree

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

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import sys
55
from collections.abc import Awaitable, Callable, Sequence
66
from itertools import chain
7-
from types import GenericAlias
87
from typing import Annotated, Any, NotRequired, Union, cast, get_args, get_origin
98

109
import anyio
@@ -499,10 +498,8 @@ def _create_output_model(original_annotation: Any, type_expr: Any, func_name: st
499498
model = _create_wrapped_model(func_name, original_annotation)
500499
wrap_output = True
501500

502-
# Handle GenericAlias types (list[str], dict[str, int], Union[str, int], etc.)
503-
elif isinstance(type_expr, GenericAlias):
504-
origin = get_origin(type_expr)
505-
501+
# Handle parameterized generics (list[str], typing.Dict[str, int], Union[str, int], etc.)
502+
elif (origin := get_origin(type_expr)) is not None:
506503
if origin is dict:
507504
args = get_args(type_expr)
508505
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
545542

546543
# Handle any other types not covered above
547544
else:
548-
# This includes typing constructs that aren't GenericAlias in Python 3.10
549-
# (e.g., Union, Optional in some Python versions)
550545
model = _create_wrapped_model(func_name, original_annotation)
551546
wrap_output = True
552547

‎tests/server/mcpserver/test_func_metadata.py‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# pyright: reportMissingParameterType=false
44
# pyright: reportUnknownArgumentType=false
55
# pyright: reportUnknownLambdaType=false
6+
import typing
67
from collections.abc import Callable
78
from dataclasses import dataclass
89
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
295296
"title": "func_dict_listDictOutput",
296297
}
297298

299+
def func_typing_dict() -> typing.Dict[str, str]: # noqa: UP006 # pragma: no cover
300+
return {"name": "John"}
301+
302+
assert func_metadata(func_typing_dict).output_schema == {
303+
"type": "object",
304+
"additionalProperties": {"type": "string"},
305+
"title": "func_typing_dictDictOutput",
306+
}
307+
298308
# Test dict[int, str] - should be wrapped since key is not str
299309
def func_dict_int_key() -> dict[int, str]: # pragma: no cover
300310
return {1: "a", 2: "b"}
@@ -774,6 +784,19 @@ def func_optional() -> str | None: # pragma: no cover
774784
"title": "func_optionalOutput",
775785
}
776786

787+
UserId = typing.NewType("UserId", int)
788+
789+
def func_new_type() -> UserId: # pragma: no cover
790+
return UserId(1)
791+
792+
meta = func_metadata(func_new_type)
793+
assert meta.output_schema == {
794+
"type": "object",
795+
"properties": {"result": {"title": "Result", "type": "integer"}},
796+
"required": ["result"],
797+
"title": "func_new_typeOutput",
798+
}
799+
777800

778801
def test_structured_output_dataclass():
779802
"""Test structured output with dataclass return types"""

0 commit comments

Comments
 (0)