Skip to content

Commit c75abde

Browse files
committed
fix(mcpserver): treat bare typing.Dict and typing sequence generics consistently in func_metadata
1 parent a525a3d commit c75abde

2 files changed

Lines changed: 43 additions & 1 deletion

File tree

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import inspect
33
import json
44
import sys
5+
import typing
56
from collections.abc import Awaitable, Callable, Sequence
67
from itertools import chain
78
from typing import Annotated, Any, NotRequired, Union, cast, get_args, get_origin
@@ -446,7 +447,9 @@ def func_metadata(
446447
else:
447448
original_annotation = effective_annotation
448449

449-
if structured_output is None and (return_type_expr in (list, tuple) or _returns_content(return_type_expr)):
450+
if structured_output is None and (
451+
return_type_expr in (list, tuple, typing.List, typing.Tuple) or _returns_content(return_type_expr)
452+
):
450453
# Content blocks and the Image/Audio helpers are what the model reads, not data for the
451454
# application: a derived schema would advertise the block's own model as output_schema (and,
452455
# unless the tool builds its own CallToolResult, echo every block into structured_content).
@@ -520,6 +523,9 @@ def _create_output_model(original_annotation: Any, type_expr: Any, func_name: st
520523
# TODO: should we use the original annotation? We are losing any potential `Annotated`
521524
# metadata for Pydantic here:
522525
model = type_expr
526+
elif len(args) == 0:
527+
# Bare typing.Dict is equivalent to bare dict -> top-level JSON object
528+
model = dict[str, Any]
523529
else:
524530
# dict with non-str keys needs wrapping
525531
model = _create_wrapped_model(func_name, original_annotation)

‎tests/server/mcpserver/test_func_metadata.py‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,17 @@ def func_typing_dict() -> typing.Dict[str, str]: # noqa: UP006
309309
assert isinstance(result, CallToolResult)
310310
assert result.structured_content == {"name": "John"}
311311

312+
# Test bare typing.Dict (unparameterized) - should match bare dict behavior without wrapping
313+
def func_bare_typing_dict() -> typing.Dict: # noqa: UP006
314+
return {"msg": "hello", "code": 200}
315+
316+
meta_bare = func_metadata(func_bare_typing_dict)
317+
assert meta_bare.output_schema == IsPartialDict(type="object")
318+
assert not meta_bare.wrap_output
319+
result_bare = meta_bare.convert_result(func_bare_typing_dict())
320+
assert isinstance(result_bare, CallToolResult)
321+
assert result_bare.structured_content == {"msg": "hello", "code": 200}
322+
312323
# Test dict[int, str] - should be wrapped since key is not str
313324
def func_dict_int_key() -> dict[int, str]: # pragma: no cover
314325
return {1: "a", 2: "b"}
@@ -318,6 +329,31 @@ def func_dict_int_key() -> dict[int, str]: # pragma: no cover
318329
assert "result" in meta.output_schema["properties"]
319330

320331

332+
def test_bare_typing_sequence_opts_out_of_structured_output():
333+
"""Test that bare typing.List and typing.Tuple opt out of structured output like bare list/tuple."""
334+
def func_bare_list() -> list:
335+
return ["a", "b"]
336+
337+
def func_bare_typing_list() -> typing.List: # noqa: UP006
338+
return ["a", "b"]
339+
340+
def func_bare_tuple() -> tuple:
341+
return ("a", "b")
342+
343+
def func_bare_typing_tuple() -> typing.Tuple: # noqa: UP006
344+
return ("a", "b")
345+
346+
meta_list = func_metadata(func_bare_list)
347+
meta_typing_list = func_metadata(func_bare_typing_list)
348+
meta_tuple = func_metadata(func_bare_tuple)
349+
meta_typing_tuple = func_metadata(func_bare_typing_tuple)
350+
351+
assert meta_list.output_schema is None
352+
assert meta_typing_list.output_schema is None
353+
assert meta_tuple.output_schema is None
354+
assert meta_typing_tuple.output_schema is None
355+
356+
321357
@pytest.mark.anyio
322358
async def test_lambda_function():
323359
"""Test lambda function schema and validation"""

0 commit comments

Comments
 (0)