From 2a93404e8cb8790b17a15b2b1c64e98a0f0af60f Mon Sep 17 00:00:00 2001 From: Shuying <46500487+ShuyingZhang@users.noreply.github.com> Date: Sun, 26 Jul 2026 13:43:41 -0500 Subject: [PATCH] fix: accept callable roots handlers (#4639) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Accept callable roots handlers 🤖 Generated with OpenAI Codex * fix: cover callable object roots handlers 🤖 Generated with OpenAI Codex --------- Co-authored-by: Shuying --- fastmcp_slim/fastmcp/client/roots.py | 2 +- .../fastmcp/server/providers/proxy.py | 7 +--- tests/client/test_roots.py | 39 +++++++++++++++++++ 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/fastmcp_slim/fastmcp/client/roots.py b/fastmcp_slim/fastmcp/client/roots.py index cb655c1fb..7db4b9d60 100644 --- a/fastmcp_slim/fastmcp/client/roots.py +++ b/fastmcp_slim/fastmcp/client/roots.py @@ -37,7 +37,7 @@ def create_roots_callback( if isinstance(handler, list): # TODO(ty): remove when ty supports isinstance union narrowing return _create_roots_callback_from_roots(handler) # type: ignore[arg-type] # ty:ignore[invalid-argument-type] - elif inspect.isfunction(handler): + elif callable(handler): return _create_roots_callback_from_fn(handler) else: raise ValueError(f"Invalid roots handler: {handler}") diff --git a/fastmcp_slim/fastmcp/server/providers/proxy.py b/fastmcp_slim/fastmcp/server/providers/proxy.py index 78f1b1f17..cd6d888b2 100644 --- a/fastmcp_slim/fastmcp/server/providers/proxy.py +++ b/fastmcp_slim/fastmcp/server/providers/proxy.py @@ -1327,12 +1327,7 @@ def _restore_request_context( def _make_restoring_handler(handler: Callable, rc_ref: list[Any]) -> Callable: - """Wrap a proxy handler to restore request_ctx before delegating. - - The wrapper is a plain ``async def`` so it passes - ``inspect.isfunction()`` checks in handler registration paths - (e.g., ``create_roots_callback``). - """ + """Wrap a proxy handler to restore request_ctx before delegating.""" async def wrapper(*args: Any, **kwargs: Any) -> Any: _restore_request_context(rc_ref) diff --git a/tests/client/test_roots.py b/tests/client/test_roots.py index e71f0c849..8874ced05 100644 --- a/tests/client/test_roots.py +++ b/tests/client/test_roots.py @@ -1,3 +1,5 @@ +import functools + import pytest from fastmcp import Client, Context, FastMCP @@ -43,3 +45,40 @@ class TestClientRoots: "file://x/y/z", "file://x/y/z", ] + + async def test_bound_method_roots_handler(self, fastmcp_server: FastMCP): + class RootsProvider: + async def get_roots(self, _context: object) -> list[str]: + return ["file:///bound-method"] + + provider = RootsProvider() + + async with Client( + fastmcp_server, mode="legacy", roots=provider.get_roots + ) as client: + result = await client.call_tool("list_roots", {}) + + assert result.data == ["file:///bound-method"] + + async def test_partial_roots_handler(self, fastmcp_server: FastMCP): + async def get_roots(prefix: str, _context: object) -> list[str]: + return [f"file:///{prefix}"] + + handler = functools.partial(get_roots, "partial") + + async with Client(fastmcp_server, mode="legacy", roots=handler) as client: + result = await client.call_tool("list_roots", {}) + + assert result.data == ["file:///partial"] + + async def test_callable_object_roots_handler(self, fastmcp_server: FastMCP): + class RootsProvider: + async def __call__(self, _context: object) -> list[str]: + return ["file:///callable-object"] + + async with Client( + fastmcp_server, mode="legacy", roots=RootsProvider() + ) as client: + result = await client.call_tool("list_roots", {}) + + assert result.data == ["file:///callable-object"]