nate nowack Claude commited on
Commit
1b5fe32
·
unverified ·
1 Parent(s): 13f7b73

Fix tool output schema generation to respect Pydantic serialization aliases (#1148)

Browse files
src/fastmcp/tools/tool.py CHANGED
@@ -399,7 +399,7 @@ class ParsedFunction:
399
 
400
  try:
401
  type_adapter = get_cached_typeadapter(clean_output_type)
402
- base_schema = type_adapter.json_schema()
403
 
404
  # Generate schema for wrapped type if it's non-object
405
  # because MCP requires that output schemas are objects
@@ -410,7 +410,7 @@ class ParsedFunction:
410
  # Use the wrapped result schema directly
411
  wrapped_type = _WrappedResult[clean_output_type]
412
  wrapped_adapter = get_cached_typeadapter(wrapped_type)
413
- output_schema = wrapped_adapter.json_schema()
414
  output_schema["x-fastmcp-wrap-result"] = True
415
  else:
416
  output_schema = base_schema
 
399
 
400
  try:
401
  type_adapter = get_cached_typeadapter(clean_output_type)
402
+ base_schema = type_adapter.json_schema(mode="serialization")
403
 
404
  # Generate schema for wrapped type if it's non-object
405
  # because MCP requires that output schemas are objects
 
410
  # Use the wrapped result schema directly
411
  wrapped_type = _WrappedResult[clean_output_type]
412
  wrapped_adapter = get_cached_typeadapter(wrapped_type)
413
+ output_schema = wrapped_adapter.json_schema(mode="serialization")
414
  output_schema["x-fastmcp-wrap-result"] = True
415
  else:
416
  output_schema = base_schema
tests/tools/test_tool.py CHANGED
@@ -1291,6 +1291,88 @@ class TestUnionReturnTypes:
1291
  assert result2.structured_content == {"result": "error occurred"}
1292
 
1293
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1294
  class TestToolTitle:
1295
  """Tests for tool title functionality."""
1296
 
 
1291
  assert result2.structured_content == {"result": "error occurred"}
1292
 
1293
 
1294
+ class TestSerializationAlias:
1295
+ """Tests for Pydantic field serialization alias support in tool output schemas."""
1296
+
1297
+ def test_output_schema_respects_serialization_alias(self):
1298
+ """Test that Tool.from_function generates output schema using serialization alias."""
1299
+ from pydantic import AliasChoices, BaseModel, Field
1300
+
1301
+ class Component(BaseModel):
1302
+ """Model with multiple validation aliases but specific serialization alias."""
1303
+
1304
+ component_id: str = Field(
1305
+ validation_alias=AliasChoices("id", "componentId"),
1306
+ serialization_alias="componentId",
1307
+ description="The ID of the component",
1308
+ )
1309
+
1310
+ async def get_component(
1311
+ component_id: str,
1312
+ ) -> Annotated[Component, Field(description="The component.")]:
1313
+ # API returns data with 'id' field
1314
+ api_data = {"id": component_id}
1315
+ return Component.model_validate(api_data)
1316
+
1317
+ tool = Tool.from_function(get_component, name="get-component")
1318
+
1319
+ # The output schema should use the serialization alias 'componentId'
1320
+ # not the first validation alias 'id'
1321
+ assert tool.output_schema is not None
1322
+
1323
+ # Check the wrapped result schema
1324
+ assert "properties" in tool.output_schema
1325
+ assert "result" in tool.output_schema["properties"]
1326
+ assert "$defs" in tool.output_schema
1327
+
1328
+ # Find the Component definition
1329
+ component_def = list(tool.output_schema["$defs"].values())[0]
1330
+
1331
+ # Should have 'componentId' not 'id' in properties
1332
+ assert "componentId" in component_def["properties"]
1333
+ assert "id" not in component_def["properties"]
1334
+
1335
+ # Should require 'componentId' not 'id'
1336
+ assert "componentId" in component_def["required"]
1337
+ assert "id" not in component_def.get("required", [])
1338
+
1339
+ async def test_tool_execution_with_serialization_alias(self):
1340
+ """Test that tool execution works correctly with serialization aliases."""
1341
+ from pydantic import AliasChoices, BaseModel, Field
1342
+
1343
+ from fastmcp import Client, FastMCP
1344
+
1345
+ class Component(BaseModel):
1346
+ """Model with multiple validation aliases but specific serialization alias."""
1347
+
1348
+ component_id: str = Field(
1349
+ validation_alias=AliasChoices("id", "componentId"),
1350
+ serialization_alias="componentId",
1351
+ description="The ID of the component",
1352
+ )
1353
+
1354
+ mcp = FastMCP("TestServer")
1355
+
1356
+ @mcp.tool
1357
+ async def get_component(
1358
+ component_id: str,
1359
+ ) -> Annotated[Component, Field(description="The component.")]:
1360
+ # API returns data with 'id' field
1361
+ api_data = {"id": component_id}
1362
+ return Component.model_validate(api_data)
1363
+
1364
+ async with Client(mcp) as client:
1365
+ # Execute the tool - this should work without validation errors
1366
+ result = await client.call_tool(
1367
+ "get_component", {"component_id": "test123"}
1368
+ )
1369
+
1370
+ # The result should contain the serialized form with 'componentId'
1371
+ assert result.structured_content is not None
1372
+ assert result.structured_content["result"]["componentId"] == "test123"
1373
+ assert "id" not in result.structured_content["result"]
1374
+
1375
+
1376
  class TestToolTitle:
1377
  """Tests for tool title functionality."""
1378