fix(mcpserver): preserve Annotated/Field metadata for dict[str, T] return types - #2939
Conversation
…turn types When a tool returns `Annotated[dict[str, T], Field(description="...")]`, the `_try_create_model_and_schema` dict branch was passing the unwrapped `type_expr` (i.e. `dict[str, T]`) to `_create_dict_model` instead of `original_annotation`, so any `Field` description or other Pydantic metadata was dropped from the output schema. Fix by passing `original_annotation` so the `RootModel` picks it up. Fixes modelcontextprotocol#2935
Pre-commit ruff-format check failed because the return type annotation for get_headers() in the test exceeded line-length limits. Wrap it to three lines to satisfy the formatter.
|
Fixed the pre-commit failure — ruff-format wrapped a long return-type annotation in the test file. All other checks were already green. |
|
Nice catch! Using |
|
@maxisbey — this has been open since Jun 21 with no maintainer pass, so surfacing it. It should be a fast one: It answers a TODO this repo already wrote. # TODO: should we use the original annotation? We are losing any potential `Annotated`
# metadata for Pydantic here:
model = _create_dict_model(func_name, type_expr)The answer is yes, and that is the whole change — passing State: +30/-3 across 2 files, all 27 checks green, still Independent read: @itxaiohanglover looked at it on Jun 26 and reached the same conclusion about No objection if you would rather resolve the TODO differently — if you prefer the metadata to be dropped deliberately and the comment removed instead, say so and I will close this. I mostly want the TODO to stop being ambiguous either way. |
Summary
When a tool declares a
dict[str, T]return type wrapped inAnnotatedwithPydantic
Fieldmetadata (e.g. a description), that metadata was silently droppedfrom the generated output schema.
Root cause
In
_try_create_model_and_schema, thedict[str, T]branch called_create_dict_model(func_name, type_expr)wheretype_expris theAnnotated-stripped type. The existing TODO comment on that line even flaggedthe issue. The fix is to pass
original_annotationinstead, soRootModel[Annotated[dict[str, T], Field(...)]]picks up the metadata.Changes
src/mcp/server/mcpserver/utilities/func_metadata.py: passoriginal_annotationinstead of
type_exprto_create_dict_model; replace TODO comment with aclarifying one
tests/server/mcpserver/test_func_metadata.py: addtest_structured_output_dict_str_preserves_annotated_metadataregression testcovering
Field(description=...)andField(description=..., title=...)Fixes #2935