Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/mcp/server/fastmcp/utilities/func_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
45 changes: 45 additions & 0 deletions tests/server/fastmcp/test_func_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading