Skip to content

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

Description

@zlucas03

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

from typing import Annotated
from pydantic import Field
from mcp.server.mcpserver.utilities.func_metadata import func_metadata

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

print(func_metadata(get_config).output_schema)
# {'type': 'object', 'additionalProperties': {'type': 'integer'}, 'title': 'get_configDictOutput'}
# expected to also include: 'description': 'Configuration values'

Python & MCP Python SDK

Python 3.14.5
mcp: main branch (mcpserver / V2)

Activity

  1. zlucas03 commented on Jun 22, 2026

    @zlucas03
    Author

    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.

  2. anneheartrecord commented on Jun 23, 2026

    @anneheartrecord

    Thanks @zlucas03, good to have that confirmed independently. The byte-for-byte match on un-annotated dict[str, str] and dict[str, Any] is exactly why I threaded original_annotation through instead of touching the stripped type_expr — existing schemas stay identical, only the annotated case changes. Fix is up in #2939.

  3. added
    bugSomething isn't working
    P3Nice to haves, rare edge cases
    v2Affects the v2 line (2.x on main)
    on Aug 14, 2026
  4. anneheartrecord commented on Oct 8, 2026

    @anneheartrecord

    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_model builds the schema from the stripped type_expr, and the TODO right above it already notes the lost Annotated metadata:

    # 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-annotated dict[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.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3Nice to haves, rare edge casesbugSomething isn't workingv2Affects the v2 line (2.x on main)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions