Skip to content

Commit 9453731

Browse files
committed
Fix compatibility with pydantic 2.12+ Field defaults in Annotated types
When using Annotated[T, Field(default)] without an explicit parameter default (= value), pydantic 2.12+ changed FieldInfo.from_annotated_attribute() to overwrite the Field's default with PydanticUndefined, incorrectly marking fields as required in the JSON schema. This fix checks if a Field with a default exists in the Annotated metadata and uses FieldInfo.from_annotation() to preserve that default, while still using from_annotated_attribute() for the standard case where parameter defaults take precedence. The fix maintains backward compatibility with pydantic 2.11 and earlier while ensuring correct behavior with 2.12+.
1 parent da4fce2 commit 9453731

File tree

1 file changed

+21
-4
lines changed

1 file changed

+21
-4
lines changed

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

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -239,10 +239,27 @@ def func_metadata(
239239
WithJsonSchema({"title": param.name, "type": "string"}),
240240
]
241241

242-
field_info = FieldInfo.from_annotated_attribute(
243-
_get_typed_annotation(annotation, globalns),
244-
param.default if param.default is not inspect.Parameter.empty else PydanticUndefined,
245-
)
242+
# Check if annotation contains Field with a default
243+
# This is necessary for compatibility with pydantic 2.12+ where
244+
# FieldInfo.from_annotated_attribute() overwrites Field defaults with PydanticUndefined
245+
has_field_default = False
246+
if get_origin(annotation) is Annotated:
247+
args = get_args(annotation)
248+
for arg in args[1:]: # Skip the first arg (the actual type)
249+
if isinstance(arg, FieldInfo) and arg.default is not PydanticUndefined:
250+
has_field_default = True
251+
break
252+
253+
# Use appropriate method based on whether Field has default
254+
if has_field_default:
255+
# Use from_annotation to preserve the default from Field()
256+
field_info = FieldInfo.from_annotation(_get_typed_annotation(annotation, globalns))
257+
else:
258+
# Use from_annotated_attribute to combine annotation and parameter default
259+
field_info = FieldInfo.from_annotated_attribute(
260+
_get_typed_annotation(annotation, globalns),
261+
param.default if param.default is not inspect.Parameter.empty else PydanticUndefined,
262+
)
246263

247264
# Check if the parameter name conflicts with BaseModel attributes
248265
# This is necessary because Pydantic warns about shadowing parent attributes

0 commit comments

Comments
 (0)