diff --git a/src/mcp/server/mcpserver/utilities/func_metadata.py b/src/mcp/server/mcpserver/utilities/func_metadata.py index 6c553fbab9..03e10ce823 100644 --- a/src/mcp/server/mcpserver/utilities/func_metadata.py +++ b/src/mcp/server/mcpserver/utilities/func_metadata.py @@ -349,9 +349,9 @@ def _try_create_model_and_schema( if origin is dict: args = get_args(type_expr) if len(args) == 2 and args[0] is str: - # 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) + # Use the original annotation (which may be `Annotated[dict[str, T], Field(...)]`) + # so that any Annotated metadata (e.g. Field description) is preserved in the schema. + model = _create_dict_model(func_name, original_annotation) else: # dict with non-str keys needs wrapping model = _create_wrapped_model(func_name, original_annotation) diff --git a/tests/server/mcpserver/test_func_metadata.py b/tests/server/mcpserver/test_func_metadata.py index 2763b3f503..f82f708d29 100644 --- a/tests/server/mcpserver/test_func_metadata.py +++ b/tests/server/mcpserver/test_func_metadata.py @@ -245,6 +245,33 @@ def func_dict_int_key() -> dict[int, str]: # pragma: no cover assert "result" in meta.output_schema["properties"] +def test_structured_output_dict_str_preserves_annotated_metadata(): + """dict[str, T] return types should preserve Annotated/Field metadata in output schema. + + Regression test for https://github.com/modelcontextprotocol/python-sdk/issues/2935 + """ + + def get_config() -> Annotated[dict[str, int], Field(description="Configuration values")]: # pragma: no cover + return {"timeout": 30} + + meta = func_metadata(get_config) + assert meta.output_schema is not None + assert meta.output_schema.get("description") == "Configuration values", ( + f"Expected 'description' in schema, got: {meta.output_schema}" + ) + + # Additional metadata (title, max_length, etc.) should also be preserved + def get_headers() -> Annotated[ + dict[str, str], Field(description="HTTP headers", title="Headers") + ]: # pragma: no cover + return {"Content-Type": "application/json"} + + meta2 = func_metadata(get_headers) + assert meta2.output_schema is not None + assert meta2.output_schema.get("description") == "HTTP headers" + assert meta2.output_schema.get("title") == "Headers" + + @pytest.mark.anyio async def test_lambda_function(): """Test lambda function schema and validation"""