Skip to content

Commit f8fed3d

Browse files
committed
Generate tool outputSchema in serialization mode
outputSchema was built with pydantic's default validation mode, while structuredContent is dumped with by_alias=True. Serialization aliases and computed fields showed up in the content but not in the schema, so clients validating the result rejected it. Pass mode="serialization" to json_schema so the two agree.
1 parent 91941ed commit f8fed3d

2 files changed

Lines changed: 24 additions & 2 deletions

File tree

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,9 @@ class FuncMetadata(BaseModel):
127127
def model_post_init(self, context: Any, /) -> None:
128128
if self.output_model is not None and self.output_schema is None:
129129
# StrictJsonSchema raises instead of warning, so an unserializable return type fails construction.
130-
schema = self._output_adapter(self.output_model).json_schema(schema_generator=StrictJsonSchema)
130+
adapter = self._output_adapter(self.output_model)
131+
# Serialization mode, so the schema describes the aliased/computed fields structured_content carries.
132+
schema = adapter.json_schema(schema_generator=StrictJsonSchema, mode="serialization")
131133
self.output_schema = _inline_root_ref(schema)
132134

133135
def _output_adapter(self, output_model: Any) -> TypeAdapter[Any]:

‎tests/server/mcpserver/test_func_metadata.py‎

Lines changed: 21 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, ContentBlock, EmbeddedResource, InputRequiredResult, TextContent
14-
from pydantic import BaseModel, Field, ValidationError
14+
from pydantic import BaseModel, Field, ValidationError, computed_field
1515
from typing_extensions import NotRequired, ReadOnly, Required
1616

1717
from mcp import MCPDeprecationWarning
@@ -1520,3 +1520,23 @@ def fn() -> StepA | StepB: ... # pragma: no branch
15201520

15211521
meta = func_metadata(fn)
15221522
assert meta.output_schema is None
1523+
1524+
1525+
def test_output_schema_matches_structured_content_for_serialization_aliases_and_computed_fields():
1526+
class Out(BaseModel):
1527+
user_id: int = Field(serialization_alias="userId")
1528+
1529+
@computed_field
1530+
@property
1531+
def doubled(self) -> int:
1532+
return self.user_id * 2
1533+
1534+
def tool() -> Out: # pragma: no cover
1535+
return Out(user_id=3)
1536+
1537+
meta = func_metadata(tool)
1538+
assert meta.output_schema is not None
1539+
result = meta.convert_result(Out(user_id=3))
1540+
assert result.structured_content == {"userId": 3, "doubled": 6}
1541+
assert set(meta.output_schema["properties"]) == {"userId", "doubled"}
1542+
assert set(meta.output_schema["required"]) == {"userId", "doubled"}

0 commit comments

Comments
 (0)