Repository navigation
dict[str, T] tool return types lose Annotated/Field metadata in output schema #2935
Description
Activity
Thanks for jumping on this, @anneheartrecord — this is the same approach I'd landed on (routing original_annotation through to _create_dict_model instead of the stripped type_expr), so +1 on the direction. One thing I confirmed while testing locally: un-annotated dict[str, str] and dict[str, Any] output schemas come out byte-for-byte identical, so it's safe as a non-breaking change.
Thanks @zlucas03, good to have that confirmed independently. The byte-for-byte match on un-annotated
dict[str, str]anddict[str, Any]is exactly why I threadedoriginal_annotationthrough instead of touching the strippedtype_expr— existing schemas stay identical, only the annotated case changes. Fix is up in #2939.- addedbugSomething isn't workingSomething isn't workingP3Nice to haves, rare edge casesNice to haves, rare edge casesv2Affects the v2 line (2.x on main)Affects the v2 line (2.x on main)
on Aug 14, 2026 Since #2939 was closed in the backlog sweep, I re-checked this on v2.3.0: still reproduces. The
dict[str, T]branch of_create_output_modelbuilds the schema from the strippedtype_expr, and theTODOright above it already notes the lostAnnotatedmetadata:python-sdk/src/mcp/server/mcpserver/utilities/func_metadata.py
Lines 509 to 511 in 91941ed
# TODO: should we use the original annotation? We are losing any potential `Annotated` # metadata for Pydantic here: model = Annotated[type_expr, Field(title=f"{func_name}DictOutput")] from typing import Annotated from pydantic import Field from mcp.server.mcpserver import MCPServer server = MCPServer("repro") @server.tool() def counts() -> Annotated[dict[str, int], Field(description="counts per key")]: return {"a": 1} print(server._tool_manager.get_tool("counts").output_schema) # {'additionalProperties': {'type': 'integer'}, 'title': 'countsDictOutput', 'type': 'object'} # -> no "description"
Use case is the one from the original report: the description a tool author puts on a dict return never reaches
outputSchema, so clients get the shape but not what it means.The fix is still the one-liner from #2939,
Annotated[original_annotation, Field(title=...)]in that branch. I ran it against the 2.3.0 install: un-annotateddict[str, int]output stays byte-identical, the annotated case gains"description": "counts per key". Happy to rebase #2939 onto v2 if you'd rather reopen it than redo it.
Initial Checks
Description
Tools that return a string-keyed dict (dict[str, T]) lose any Annotated/Field metadata on the return type when the output schema is generated. A Field(description=...) on the return annotation shows up in the output schema for every other return type, but for dict[str, T] it's dropped.
I'd expect the description to be preserved, the same as other return types. Looks like the dict[str, T] branch in _try_create_model_and_schema builds the model from the Annotated-stripped type instead of the original annotation (there's an existing TODO on that line).
Have a simple, tested fix ready to go. Love the project and would love to contribute if possible!
Example Code
Python & MCP Python SDK