[Bug]: Inconsistent structured output wrapping and opt-out for typing.Dict and typing sequence generics in func_metadata
Maintainers usually reply within 1 day
Nobody has claimed this yet.
- #3772 by @chauvuusvn — closed without merging
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 82/100
Research direction
Start in src/mcp/server/mcpserver/utilities/func_metadata.py, reading _create_output_model() and func_metadata() where the issue identifies the two inconsistencies. Check the existing tests for return type introspection and add coverage for bare typing.Dict, typing.List, and typing.Tuple alongside their built-in counterparts. Done means bare typing.Dict produces an unwrapped object schema and bare typing sequences opt out of structured output as described.
Written by the indexing model from the issue text.
Description
Initial Checks
- I confirm that I'm using the newest release of my line (the latest 2.x, or the latest 1.x if I'm still on v1)
- I confirm that I searched for my issue in https://github.com/modelcontextprotocol/python-sdk/issues before opening this issue
Release line
2.x (current stable)
Description
In src/mcp/server/mcpserver/utilities/func_metadata.py, return type introspection handles built-in types differently from their unsubscripted typing generic counterparts (typing.Dict, typing.List, typing.Tuple).
Issue 1: typing.Dict is forcibly wrapped in {"result": ...} instead of top-level object
When a tool returns dict or dict[str, Any], _create_output_model sets wrap_output = False and derives a top-level JSON object schema:
elif type_annotation is dict:
model = dict[str, Any]
However, when a tool is annotated with typing.Dict (unsubscripted), get_origin(typing.Dict) is dict while get_args(typing.Dict) is (). In _create_output_model():
if origin is dict:
args = get_args(type_expr)
if len(args) == 2 and args[0] is str:
model = type_expr
else:
# dict with non-str keys needs wrapping
model = _create_wrapped_model(func_name, original_annotation)
wrap_output = True
Because len(args) == 0 != 2, it falls through to else (intended only for non-str keys like dict[int, str]). As a result:
dict: treated as top-level JSON object (wrap_output = False)typing.Dict: unexpectedly wrapped into{"result": ...}(wrap_output = True)
Issue 2: typing.List and typing.Tuple bypass the opt-out gate
In func_metadata():
if structured_output is None and (return_type_expr in (list, tuple) or _returns_content(return_type_expr)):
return FuncMetadata(arg_model=arguments_model)
Because typing.List is not list (typing.List in (list, tuple) evaluates to False), tools annotated with -> typing.List or -> typing.Tuple fail to opt out and are forced through _create_output_model(), generating an unwanted wrapper schema { "result": typing.List }.
Example Code
import typing
from mcp.server.mcpserver.utilities.func_metadata import func_metadata
def tool_built_in_dict() -> dict:
return {"status": "ok"}
def tool_typing_dict() -> typing.Dict:
return {"status": "ok"}
def tool_built_in_list() -> list:
return ["a", "b"]
def tool_typing_list() -> typing.List:
return ["a", "b"]
meta_bi_dict = func_metadata(tool_built_in_dict)
meta_ty_dict = func_metadata(tool_typing_dict)
meta_bi_list = func_metadata(tool_built_in_list)
meta_ty_list = func_metadata(tool_typing_list)
# Inconsistent wrapping:
print("dict wrap_output:", meta_bi_dict.wrap_output) # False
print("typing.Dict wrap_output:", meta_ty_dict.wrap_output) # True (Expected: False)
# Inconsistent opt-out:
print("list schema:", meta_bi_list.output_schema) # None
print("typing.List schema:", meta_ty_list.output_schema) # {'type': 'object', 'properties': {'result': ...}} (Expected: None)
Proposed Fix
- In
_create_output_model():
elif origin is dict:
args = get_args(type_expr)
if len(args) == 2 and args[0] is str:
model = type_expr
elif len(args) == 0:
# Bare typing.Dict is equivalent to bare dict
model = dict[str, Any]
else:
model = _create_wrapped_model(func_name, original_annotation)
wrap_output = True
- In
func_metadata():
if structured_output is None and (
return_type_expr in (list, tuple, typing.List, typing.Tuple) or _returns_content(return_type_expr)
):
return FuncMetadata(arg_model=arguments_model)
Version
Python 3.10+, MCP Python SDK 2.x
- Dominant language
- Python
- Stars
- 24.7k
- Forks
- 4.1k
- Avg merge
- 18h 27m
- Merged PRs (30d)
- 15
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from modelcontextprotocol/python-sdk
-
bug v2
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
modelcontextprotocol/python-sdk#3811 ·
Maintainers usually reply within 1 day
-
bug v2
Difficulty 2/5 1-3 hours Newbie friendliness 79/100
modelcontextprotocol/python-sdk#3810 ·
Maintainers usually reply within 1 day
-
bug v1 v2
Difficulty 2/5 1-3 hours Newbie friendliness 79/100
modelcontextprotocol/python-sdk#3781 · 2 comments ·
Maintainers usually reply within 1 day
-
bug v2
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
modelcontextprotocol/python-sdk#3769 · 2 comments ·
Maintainers usually reply within 1 day
-
2026 Streamable HTTP rejects whitespace-padded Mcp-Name valuesMay be free again A pull request for this issue was closed without being merged. Openbug spec-2026-07-28 v2
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
modelcontextprotocol/python-sdk#3763 · 4 comments ·
Maintainers usually reply within 1 day
All issues in modelcontextprotocol/python-sdk
Similar issues
-
Backend issue lists report in-patch warnings and out-of-patch errors as not publishablePossibly taken @RaphaelFakhri claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
mozilla/code-review#3794 ·
Maintainers usually reply within 1 day
-
[oblt-aw][autodoc] Update docs that still describe the security registry as emptyPossibly taken @github-actions claimed this today. Open
Difficulty 1/5 1-3 hours Newbie friendliness 85/100
Maintainers usually reply within 1 day
-
[Bug] adb backup script reports "Done" & exits 0 when the backup failsPossibly taken @bhattmanthan claimed this today. Openrequest-issue-assignment
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
OWASP/mastg#4034 · 5 comments ·
Maintainers usually reply within 1 day
-
agent-ui bug p2
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
correction metadata
Difficulty 1/5 Under an hour Newbie friendliness 72/100
acl-org/acl-anthology#10768 · 1 comment ·
Maintainers usually reply within 1 day