Skip to content

Commit 5a3a23a

Browse files
authored
Treat typing.Dict[str, X] return annotations like dict[str, X] (#3756)
1 parent 72ff38c commit 5a3a23a

3 files changed

Lines changed: 31 additions & 8 deletions

File tree

‎docs/servers/structured-output.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ Ask for a two-day forecast and `structured_content` is `{"result": [{...}, {...}
147147

148148
## Dictionaries
149149

150-
`dict[str, ...]` is the one generic that already *is* a JSON object, so it isn't wrapped:
150+
`dict[str, ...]` (or `typing.Dict[str, ...]`) is the one generic that already *is* a JSON object, so it isn't wrapped:
151151

152152
```python title="server.py" hl_lines="9"
153153
--8<-- "docs_src/structured_output/tutorial006.py"

‎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
@@ -512,10 +511,8 @@ def _create_output_model(original_annotation: Any, type_expr: Any, func_name: st
512511
model = _create_wrapped_model(func_name, original_annotation)
513512
wrap_output = True
514513

515-
# Handle GenericAlias types (list[str], dict[str, int], Union[str, int], etc.)
516-
elif isinstance(type_expr, GenericAlias):
517-
origin = get_origin(type_expr)
518-
514+
# Handle parameterized generics (list[str], typing.Dict[str, int], Union[str, int], etc.)
515+
elif (origin := get_origin(type_expr)) is not None:
519516
if origin is dict:
520517
args = get_args(type_expr)
521518
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
558555

559556
# Handle any other types not covered above
560557
else:
561-
# This includes typing constructs that aren't GenericAlias in Python 3.10
562-
# (e.g., Union, Optional in some Python versions)
563558
model = _create_wrapped_model(func_name, original_annotation)
564559
wrap_output = True
565560

‎tests/server/mcpserver/test_func_metadata.py‎

Lines changed: 28 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
@@ -293,6 +294,18 @@ def func_dict_list() -> dict[str, list[int]]: # pragma: no cover
293294
"additionalProperties": {"type": "array", "items": {"type": "integer"}},
294295
}
295296

297+
def func_typing_dict() -> typing.Dict[str, str]: # noqa: UP006
298+
return {"name": "John"}
299+
300+
meta = func_metadata(func_typing_dict)
301+
assert meta.output_schema == {
302+
"type": "object",
303+
"additionalProperties": {"type": "string"},
304+
}
305+
result = meta.convert_result(func_typing_dict())
306+
assert isinstance(result, CallToolResult)
307+
assert result.structured_content == {"name": "John"}
308+
296309
# Test dict[int, str] - should be wrapped since key is not str
297310
def func_dict_int_key() -> dict[int, str]: # pragma: no cover
298311
return {1: "a", 2: "b"}
@@ -762,6 +775,21 @@ def func_optional() -> str | None: # pragma: no cover
762775
"required": ["result"],
763776
}
764777

778+
UserId = typing.NewType("UserId", int)
779+
780+
def func_new_type() -> UserId:
781+
return UserId(1)
782+
783+
meta = func_metadata(func_new_type)
784+
assert meta.output_schema == {
785+
"type": "object",
786+
"properties": {"result": {"type": "integer"}},
787+
"required": ["result"],
788+
}
789+
result = meta.convert_result(func_new_type())
790+
assert isinstance(result, CallToolResult)
791+
assert result.structured_content == {"result": 1}
792+
765793

766794
def test_structured_output_dataclass():
767795
"""Test structured output with dataclass return types"""

0 commit comments

Comments
 (0)