Skip to content

Commit d3054e0

Browse files
committed
fix(server): generate tool output schema in serialization mode
the output schema is generated with model_json_schema(), which defaults to validation mode, but structured content is serialized with model_dump(mode="json", by_alias=True). the two disagree: computed fields are absent from the schema, and serialization_alias fields are advertised under the validation name. generate the schema in serialization mode so the declared outputSchema matches what the server actually sends.
1 parent 3a6f299 commit d3054e0

2 files changed

Lines changed: 52 additions & 2 deletions

File tree

src/mcp/server/mcpserver/utilities/func_metadata.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,7 @@ def _try_create_model_and_schema(
445445
# If we successfully created a model, try to get its schema
446446
# Use StrictJsonSchema to raise exceptions instead of warnings
447447
try:
448-
schema = model.model_json_schema(schema_generator=StrictJsonSchema)
448+
schema = model.model_json_schema(schema_generator=StrictJsonSchema, mode="serialization")
449449
except (
450450
PydanticUserError,
451451
TypeError,

tests/server/mcpserver/test_func_metadata.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import pytest
1212
from dirty_equals import IsPartialDict
1313
from mcp_types import CallToolResult, InputRequiredResult
14-
from pydantic import BaseModel, Field
14+
from pydantic import BaseModel, Field, computed_field
1515

1616
from mcp.server.mcpserver.exceptions import InvalidSignature
1717
from mcp.server.mcpserver.utilities.func_metadata import func_metadata
@@ -1112,6 +1112,56 @@ def func_with_aliases() -> ModelWithAliases: # pragma: no cover
11121112
assert structured_content_defaults["second"] is None
11131113

11141114

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+
11151165
def test_basemodel_reserved_names():
11161166
"""Test that functions with parameters named after BaseModel methods work correctly"""
11171167

0 commit comments

Comments
 (0)