Repository navigation
Generated output schema uses Pydantic's validation shape while structured output uses its serialization shape #3100
Description
Initial Checks
- I confirm that I'm using the latest version of MCP Python SDK.
- I confirm that I searched existing issues and pull requests before opening this issue.
Description
For a Pydantic return model whose validation and serialization shapes differ, the generated outputSchema describes the validation shape while structuredContent uses the serialization shape. The SDK therefore publishes an output schema that rejects its own generated structured result.
This is related to, but not a duplicate of, #1073 / #1099. That change aligned ordinary field aliases by serializing structured output with aliases. Split validation_alias / serialization_alias values still expose different validation and serialization shapes, and serialization-only fields such as computed_field reveal the same underlying mismatch.
Example Code
from __future__ import annotations
import asyncio
import json
from jsonschema import Draft202012Validator
from pydantic import BaseModel, ConfigDict, Field, computed_field
try:
from mcp.server.mcpserver.tools.base import Tool
except ImportError: # MCP Python SDK 1.x
from mcp.server.fastmcp.tools.base import Tool
class AliasOutput(BaseModel):
model_config = ConfigDict(extra="forbid", populate_by_name=True)
value: int = Field(validation_alias="wireIn", serialization_alias="wireOut")
def alias_output() -> AliasOutput:
return AliasOutput(value=1)
class ComputedOutput(BaseModel):
model_config = ConfigDict(extra="forbid")
value: int
@computed_field
@property
def doubled(self) -> int:
return self.value * 2
def computed_output() -> ComputedOutput:
return ComputedOutput(value=1)
async def check(function: object) -> None:
tool = Tool.from_function(function)
converted = await tool.run({}, None, convert_result=True)
structured = converted[1] if isinstance(converted, tuple) else converted.structured_content
errors = [
error.message
for error in Draft202012Validator(tool.output_schema).iter_errors(structured)
]
print(function.__name__)
print("schema:", json.dumps(tool.output_schema, sort_keys=True))
print("structured:", json.dumps(structured, sort_keys=True))
print("schema_errors:", errors)
async def main() -> None:
await check(alias_output)
await check(computed_output)
asyncio.run(main())Observed validator messages:
alias_output
schema: ... "wireIn" ...
structured: {"wireOut": 1}
schema_errors: ["Additional properties are not allowed ('wireOut' was unexpected)", "'wireIn' is a required property"]
computed_output
schema: ... "value" ...
structured: {"doubled": 2, "value": 1}
schema_errors: ["Additional properties are not allowed ('doubled' was unexpected)"]
Expected behavior
The generated outputSchema should describe the serialized structured output. Generating the output model schema in Pydantic serialization mode makes both witnesses conform: the alias schema uses wireOut, and the computed-field schema includes doubled.
Python & MCP Python SDK
- Python: 3.13.13
- MCP Python SDK: 1.28.1 (latest stable)
- Also reproduced on current
main:2713b53b127afc094dc97d6067df9f69b647661c(2.0.0b2) - Pydantic: 2.13.4
Activity
Kaif10 commentedon Jul 16, 2026
I'd like to work on this if you'll assign it / mark it ready for work.
The mismatch is between how the two shapes are produced in
src/mcp/server/fastmcp/utilities/func_metadata.py:outputSchemais generated withmodel.model_json_schema(...)(defaultmode="validation", so it reflectsvalidation_aliasand omitscomputed_fields), while the structured content is produced withvalidated.model_dump(mode="json", by_alias=True)(the serialization shape, usingserialization_aliasand including computed fields). When a model splitsvalidation_alias/serialization_alias, or has acomputed_fieldunderextra="forbid", the publishedoutputSchemathen rejects the SDK's ownstructuredContent.Proposed fix: generate
outputSchemafrom the serialization shape (model_json_schema(mode="serialization", ...)) so the schema andmodel_dump(by_alias=True)agree, including computed fields — while making sure this doesn't regress the earlier alias handling (#1073/#1099). I'd extendtests/server/fastmcp/test_func_metadata.pywith cases for split validation/serialization aliases and a computed field underextra="forbid".This is pure-Python and I can develop/test it CPU-only. Disclosure: I'll use AI assistance in preparing the change; there's a human (me) who understands the fix and will own it through review. Let me know if you'd prefer a different approach before I start.
IgorGanapolsky commentedon Jul 16, 2026
This reads as a schema contract mismatch, not a model quality issue.
When “generate schema” uses Pydantic’s validation shape but structured output uses the serialization shape, the agent/runtime can believe it has a valid schema while the actual tool/output path rejects or mis-parses fields (aliases, defaults, computed fields,
exclude, etc.).Useful isolation before swapping models/prompts:
If you can share that artifact (even redacted), the failure branch is usually obvious in one pass.
jayzuccarelli commentedon Oct 10, 2026
#3644 was closed as a duplicate of this, so moving my offer here. This still reproduces on v2: the advertised
outputSchemais generated in validation mode, so a model with a serialization alias or a computed field returns structured content that does not match its own schema. The fix I'd make is small: generate the output schema in serialization mode inFuncMetadata, and dump a tool-returnedCallToolResult's validated structured content withby_alias=Trueso both sides agree, with a regression test covering a serialization alias and a computed field. Could a maintainer assign this to me?inanna-malick commentedon Oct 14, 2026
Adding a small deterministic reproduction for the validation/serialization shape mismatch already described here. It reproduces with a serialization alias alone; no split validation alias or computed property is needed.
The SDK successfully emits
structured_content={"wire": 7}, but its published schema has propertyvalueand requiresvalue. The test explicitly checks the emitted content before comparing the schema, so the observed failure is the advertised schema.The intentionally failing test is available on a test-only branch. No production code was changed. Existing ordinary alias output tests pass.
Python 3.14.7; Pydantic 2.12.5; MCP 2.3.0 (tag v2.3.0,
2118f14f8a19bc158d8a1cf90af58d85d187f849). Also reproduced using main source at754254c08fe1c3eda18b69da3d336106fe8d72d6.AI-assisted investigation and test reproduction, with a human in the loop.