Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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/mcpserver/utilities/func_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@ def _try_create_model_and_schema(
# If we successfully created a model, try to get its schema
# Use StrictJsonSchema to raise exceptions instead of warnings
try:
schema = model.model_json_schema(schema_generator=StrictJsonSchema)
schema = model.model_json_schema(schema_generator=StrictJsonSchema, mode="serialization")
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
except (
PydanticUserError,
TypeError,
Expand Down
52 changes: 51 additions & 1 deletion tests/server/mcpserver/test_func_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import pytest
from dirty_equals import IsPartialDict
from mcp_types import CallToolResult, InputRequiredResult
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, computed_field

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


def test_structured_output_computed_field():
"""A computed field is in the serialized output, so it must appear in the schema."""

class ModelWithComputed(BaseModel):
value: int

@computed_field
@property
def doubled(self) -> int: # pragma: no cover
return self.value * 2

def func_with_computed() -> ModelWithComputed: # pragma: no cover
return ModelWithComputed(value=3)

meta = func_metadata(func_with_computed)

assert meta.output_schema is not None
assert "doubled" in meta.output_schema["properties"]

converted = meta.convert_result(ModelWithComputed(value=3))
assert isinstance(converted, CallToolResult)
structured_content = converted.structured_content
assert structured_content is not None
assert structured_content == {"value": 3, "doubled": 6}
# Every serialized key must be advertised in the schema.
assert set(structured_content) <= set(meta.output_schema["properties"])


def test_structured_output_serialization_alias():
"""A serialization_alias changes the dumped key, so the schema must use it too."""

class ModelWithSerializationAlias(BaseModel):
value: int = Field(serialization_alias="the_value")

def func_with_serialization_alias() -> ModelWithSerializationAlias: # pragma: no cover
return ModelWithSerializationAlias(value=7)

meta = func_metadata(func_with_serialization_alias)

assert meta.output_schema is not None
assert "the_value" in meta.output_schema["properties"]
assert "value" not in meta.output_schema["properties"]

converted = meta.convert_result(ModelWithSerializationAlias(value=7))
assert isinstance(converted, CallToolResult)
structured_content = converted.structured_content
assert structured_content is not None
assert structured_content == {"the_value": 7}


def test_basemodel_reserved_names():
"""Test that functions with parameters named after BaseModel methods work correctly"""

Expand Down
Loading