From 58e25ccf47429b295e6325b06e27dacf785ae5f8 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sat, 7 Mar 2026 12:10:01 -0500 Subject: [PATCH] Preserve kw-only defaults when rebuilding functions for resolved annotations (#3429) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Preserve kw-only defaults in cloned adapters (🤖 GPT-5.2-Codex) * Fix ruff format violation in test_types.py --- src/fastmcp/utilities/types.py | 3 +++ tests/utilities/test_types.py | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/fastmcp/utilities/types.py b/src/fastmcp/utilities/types.py index 91377df45..f8e4f91af 100644 --- a/src/fastmcp/utilities/types.py +++ b/src/fastmcp/utilities/types.py @@ -88,12 +88,14 @@ def get_cached_typeadapter(cls: T) -> TypeAdapter[T]: globals_dict = actual_func.__globals__ # ty: ignore[unresolved-attribute] name = actual_func.__name__ # ty: ignore[unresolved-attribute] defaults = actual_func.__defaults__ # ty: ignore[unresolved-attribute] + kwdefaults = actual_func.__kwdefaults__ # ty: ignore[unresolved-attribute] closure = actual_func.__closure__ # ty: ignore[unresolved-attribute] else: code = cls.__code__ globals_dict = cls.__globals__ name = cls.__name__ defaults = cls.__defaults__ + kwdefaults = cls.__kwdefaults__ closure = cls.__closure__ new_func = types.FunctionType( @@ -107,6 +109,7 @@ def get_cached_typeadapter(cls: T) -> TypeAdapter[T]: new_func.__module__ = cls.__module__ new_func.__qualname__ = getattr(cls, "__qualname__", cls.__name__) new_func.__annotations__ = processed_hints + new_func.__kwdefaults__ = kwdefaults if inspect.ismethod(cls): new_method = types.MethodType(new_func, cls.__self__) diff --git a/tests/utilities/test_types.py b/tests/utilities/test_types.py index 7901caff9..1f349ebdb 100644 --- a/tests/utilities/test_types.py +++ b/tests/utilities/test_types.py @@ -671,3 +671,21 @@ class TestAnnotationStringDescriptions: # Should keep the Field description assert schema["properties"]["name"]["description"] == "Field desc" + + def test_kwonly_defaults_preserved_when_annotations_are_processed(self): + """Keyword-only defaults should survive function cloning during annotation processing.""" + + def func(*, limit: Annotated[int, "Maximum number of results"] = 5) -> int: + return limit + + adapter = get_cached_typeadapter(func) + schema = adapter.json_schema() + + assert "required" not in schema + assert schema["properties"]["limit"]["default"] == 5 + assert ( + schema["properties"]["limit"]["description"] == "Maximum number of results" + ) + + validated = adapter.validate_python({}) + assert validated == 5