|
11 | 11 | import pytest |
12 | 12 | from dirty_equals import IsPartialDict |
13 | 13 | from mcp_types import CallToolResult, InputRequiredResult |
14 | | -from pydantic import BaseModel, Field |
| 14 | +from pydantic import BaseModel, Field, computed_field |
15 | 15 |
|
16 | 16 | from mcp.server.mcpserver.exceptions import InvalidSignature |
17 | 17 | from mcp.server.mcpserver.utilities.func_metadata import func_metadata |
@@ -1112,6 +1112,56 @@ def func_with_aliases() -> ModelWithAliases: # pragma: no cover |
1112 | 1112 | assert structured_content_defaults["second"] is None |
1113 | 1113 |
|
1114 | 1114 |
|
| 1115 | +def test_structured_output_computed_field(): |
| 1116 | + """A computed field is in the serialized output, so it must appear in the schema.""" |
| 1117 | + |
| 1118 | + class ModelWithComputed(BaseModel): |
| 1119 | + value: int |
| 1120 | + |
| 1121 | + @computed_field |
| 1122 | + @property |
| 1123 | + def doubled(self) -> int: # pragma: no cover |
| 1124 | + return self.value * 2 |
| 1125 | + |
| 1126 | + def func_with_computed() -> ModelWithComputed: # pragma: no cover |
| 1127 | + return ModelWithComputed(value=3) |
| 1128 | + |
| 1129 | + meta = func_metadata(func_with_computed) |
| 1130 | + |
| 1131 | + assert meta.output_schema is not None |
| 1132 | + assert "doubled" in meta.output_schema["properties"] |
| 1133 | + |
| 1134 | + converted = meta.convert_result(ModelWithComputed(value=3)) |
| 1135 | + assert isinstance(converted, CallToolResult) |
| 1136 | + structured_content = converted.structured_content |
| 1137 | + assert structured_content is not None |
| 1138 | + assert structured_content == {"value": 3, "doubled": 6} |
| 1139 | + # Every serialized key must be advertised in the schema. |
| 1140 | + assert set(structured_content) <= set(meta.output_schema["properties"]) |
| 1141 | + |
| 1142 | + |
| 1143 | +def test_structured_output_serialization_alias(): |
| 1144 | + """A serialization_alias changes the dumped key, so the schema must use it too.""" |
| 1145 | + |
| 1146 | + class ModelWithSerializationAlias(BaseModel): |
| 1147 | + value: int = Field(serialization_alias="the_value") |
| 1148 | + |
| 1149 | + def func_with_serialization_alias() -> ModelWithSerializationAlias: # pragma: no cover |
| 1150 | + return ModelWithSerializationAlias(value=7) |
| 1151 | + |
| 1152 | + meta = func_metadata(func_with_serialization_alias) |
| 1153 | + |
| 1154 | + assert meta.output_schema is not None |
| 1155 | + assert "the_value" in meta.output_schema["properties"] |
| 1156 | + assert "value" not in meta.output_schema["properties"] |
| 1157 | + |
| 1158 | + converted = meta.convert_result(ModelWithSerializationAlias(value=7)) |
| 1159 | + assert isinstance(converted, CallToolResult) |
| 1160 | + structured_content = converted.structured_content |
| 1161 | + assert structured_content is not None |
| 1162 | + assert structured_content == {"the_value": 7} |
| 1163 | + |
| 1164 | + |
1115 | 1165 | def test_basemodel_reserved_names(): |
1116 | 1166 | """Test that functions with parameters named after BaseModel methods work correctly""" |
1117 | 1167 |
|
|
0 commit comments