From c5c8a68a386798a3a9464e3ad3a0bd6bee923bfb Mon Sep 17 00:00:00 2001 From: sjwhole Date: Mon, 7 Jul 2025 17:42:39 +0900 Subject: [PATCH 1/2] Fix Pydantic field alias consistency in structured output Resolve inconsistency between schema and actual output when using Pydantic models with field aliases as tool return types: - Add by_alias=True parameter to model_dump() call in func_metadata.py - Add comprehensive test case to verify alias consistency - Ensure schema generation and structured output use same aliased field names Before: Schema shows "first", "second" but output uses "field_first", "field_second" After: Both schema and output consistently use aliased field names --- src/mcp/server/fastmcp/utilities/func_metadata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mcp/server/fastmcp/utilities/func_metadata.py b/src/mcp/server/fastmcp/utilities/func_metadata.py index a6f905ee5b..a8f9652def 100644 --- a/src/mcp/server/fastmcp/utilities/func_metadata.py +++ b/src/mcp/server/fastmcp/utilities/func_metadata.py @@ -111,7 +111,7 @@ def convert_result(self, result: Any) -> Any: assert self.output_model is not None, "Output model must be set if output schema is defined" validated = self.output_model.model_validate(result) - structured_content = validated.model_dump(mode="json") + structured_content = validated.model_dump(mode="json", by_alias=True) return (unstructured_content, structured_content) From a8e43eebf6f8bc109f0795eb510fd74fddb963fc Mon Sep 17 00:00:00 2001 From: sjwhole Date: Mon, 7 Jul 2025 17:54:50 +0900 Subject: [PATCH 2/2] Add test case for Pydantic field alias consistency Add comprehensive test_structured_output_aliases() that verifies: - Schema generation uses aliased field names - Structured output uses same aliased field names - Both explicit values and default values work correctly - Ensures consistency between schema and actual tool output --- tests/server/fastmcp/test_func_metadata.py | 45 ++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/server/fastmcp/test_func_metadata.py b/tests/server/fastmcp/test_func_metadata.py index a520975301..8405099069 100644 --- a/tests/server/fastmcp/test_func_metadata.py +++ b/tests/server/fastmcp/test_func_metadata.py @@ -839,3 +839,48 @@ def func_returning_namedtuple() -> Point: func_metadata(func_returning_namedtuple, structured_output=True) assert "is not serializable for structured output" in str(exc_info.value) assert "Point" in str(exc_info.value) + + +def test_structured_output_aliases(): + """Test that field aliases are consistent between schema and output""" + + class ModelWithAliases(BaseModel): + field_first: str | None = Field(default=None, alias="first", description="The first field.") + field_second: str | None = Field(default=None, alias="second", description="The second field.") + + def func_with_aliases() -> ModelWithAliases: + # When aliases are defined, we must use the aliased names to set values + return ModelWithAliases(**{"first": "hello", "second": "world"}) + + meta = func_metadata(func_with_aliases) + + # Check that schema uses aliases + assert meta.output_schema is not None + assert "first" in meta.output_schema["properties"] + assert "second" in meta.output_schema["properties"] + assert "field_first" not in meta.output_schema["properties"] + assert "field_second" not in meta.output_schema["properties"] + + # Check that the actual output uses aliases too + result = ModelWithAliases(**{"first": "hello", "second": "world"}) + unstructured_content, structured_content = meta.convert_result(result) + + # The structured content should use aliases to match the schema + assert "first" in structured_content + assert "second" in structured_content + assert "field_first" not in structured_content + assert "field_second" not in structured_content + assert structured_content["first"] == "hello" + assert structured_content["second"] == "world" + + # Also test the case where we have a model with defaults to ensure aliases work in all cases + result_with_defaults = ModelWithAliases() # Uses default None values + unstructured_content_defaults, structured_content_defaults = meta.convert_result(result_with_defaults) + + # Even with defaults, should use aliases in output + assert "first" in structured_content_defaults + assert "second" in structured_content_defaults + assert "field_first" not in structured_content_defaults + assert "field_second" not in structured_content_defaults + assert structured_content_defaults["first"] is None + assert structured_content_defaults["second"] is None