Skip to content

fix(mcpserver): preserve Annotated/Field metadata for dict[str, T] return types - #2939

Open
anneheartrecord wants to merge 2 commits into
modelcontextprotocol:mainfrom
anneheartrecord:fix/2935-dict-annotated-metadata
Open

fix(mcpserver): preserve Annotated/Field metadata for dict[str, T] return types#2939
anneheartrecord wants to merge 2 commits into
modelcontextprotocol:mainfrom
anneheartrecord:fix/2935-dict-annotated-metadata

Conversation

@anneheartrecord

Copy link
Copy Markdown

Summary

When a tool declares a dict[str, T] return type wrapped in Annotated with
Pydantic Field metadata (e.g. a description), that metadata was silently dropped
from the generated output schema.

def get_config() -> Annotated[dict[str, int], Field(description="Configuration values")]:
    return {"timeout": 30}

print(func_metadata(get_config).output_schema)
# Before: {'type': 'object', 'additionalProperties': {'type': 'integer'}, 'title': 'get_configDictOutput'}
# After:  {'type': 'object', 'additionalProperties': {'type': 'integer'}, 'title': 'get_configDictOutput', 'description': 'Configuration values'}

Root cause

In _try_create_model_and_schema, the dict[str, T] branch called
_create_dict_model(func_name, type_expr) where type_expr is the
Annotated-stripped type. The existing TODO comment on that line even flagged
the issue. The fix is to pass original_annotation instead, so
RootModel[Annotated[dict[str, T], Field(...)]] picks up the metadata.

Changes

  • src/mcp/server/mcpserver/utilities/func_metadata.py: pass original_annotation
    instead of type_expr to _create_dict_model; replace TODO comment with a
    clarifying one
  • tests/server/mcpserver/test_func_metadata.py: add
    test_structured_output_dict_str_preserves_annotated_metadata regression test
    covering Field(description=...) and Field(description=..., title=...)

Fixes #2935

…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.
@anneheartrecord

Copy link
Copy Markdown
Author

Fixed the pre-commit failure — ruff-format wrapped a long return-type annotation in the test file. All other checks were already green.

@itxaiohanglover

Copy link
Copy Markdown

Nice catch! Using original_annotation instead of type_expr preserves the Annotated metadata for Pydantic. Simple one-line fix with the right regression test covering both description and title metadata.

@anneheartrecord

Copy link
Copy Markdown
Author

@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. func_metadata.py currently has, at line 510 on main today:

# 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 original_annotation instead of type_expr, so Annotated[dict[str, T], Field(description=...)] keeps its metadata in the output schema. One line of source, plus a regression test covering both description and title. Fixes #2935.

State: +30/-3 across 2 files, all 27 checks green, still MERGEABLE. It also survived #3331 untouched — that PR reworked other parts of the same file, but this origin is dict branch and its TODO are unchanged on main, so the fix still applies as written. Happy to rebase for a fresh run if you want one.

Independent read: @itxaiohanglover looked at it on Jun 26 and reached the same conclusion about original_annotation vs type_expr.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

dict[str, T] tool return types lose Annotated/Field metadata in output schema

2 participants