From 94537319b5d9c61ca41e5db34128cfb3793af329 Mon Sep 17 00:00:00 2001 From: Max Isbey <224885523+maxisbey@users.noreply.github.com> Date: Mon, 10 Nov 2025 20:04:47 +0000 Subject: [PATCH] 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+. --- .../server/fastmcp/utilities/func_metadata.py | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/mcp/server/fastmcp/utilities/func_metadata.py b/src/mcp/server/fastmcp/utilities/func_metadata.py index 873b1ae19..5943af4c3 100644 --- a/src/mcp/server/fastmcp/utilities/func_metadata.py +++ b/src/mcp/server/fastmcp/utilities/func_metadata.py @@ -239,10 +239,27 @@ def func_metadata( WithJsonSchema({"title": param.name, "type": "string"}), ] - field_info = FieldInfo.from_annotated_attribute( - _get_typed_annotation(annotation, globalns), - param.default if param.default is not inspect.Parameter.empty else PydanticUndefined, - ) + # Check if annotation contains Field with a default + # This is necessary for compatibility with pydantic 2.12+ where + # FieldInfo.from_annotated_attribute() overwrites Field defaults with PydanticUndefined + has_field_default = False + if get_origin(annotation) is Annotated: + args = get_args(annotation) + for arg in args[1:]: # Skip the first arg (the actual type) + if isinstance(arg, FieldInfo) and arg.default is not PydanticUndefined: + has_field_default = True + break + + # Use appropriate method based on whether Field has default + if has_field_default: + # Use from_annotation to preserve the default from Field() + field_info = FieldInfo.from_annotation(_get_typed_annotation(annotation, globalns)) + else: + # Use from_annotated_attribute to combine annotation and parameter default + field_info = FieldInfo.from_annotated_attribute( + _get_typed_annotation(annotation, globalns), + param.default if param.default is not inspect.Parameter.empty else PydanticUndefined, + ) # Check if the parameter name conflicts with BaseModel attributes # This is necessary because Pydantic warns about shadowing parent attributes