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/client/sampling/handlers/anthropic.py b/fastmcp_slim/fastmcp/client/sampling/handlers/anthropic.py index 747d1ab69..0ee24ef8d 100644 --- a/fastmcp_slim/fastmcp/client/sampling/handlers/anthropic.py +++ b/fastmcp_slim/fastmcp/client/sampling/handlers/anthropic.py @@ -75,7 +75,7 @@ class AnthropicSamplingHandler: Example: ```python from anthropic import AsyncAnthropic - from fastmcp import FastMCP + from fastmcp import Client from fastmcp.client.sampling.handlers.anthropic import AnthropicSamplingHandler handler = AnthropicSamplingHandler( @@ -83,7 +83,9 @@ class AnthropicSamplingHandler: client=AsyncAnthropic(), ) - server = FastMCP(sampling_handler=handler) + # Sampling is a server-initiated request, so it only exists on the + # handshake era; pass `mode="legacy"` to answer one. + client = Client(server_url, sampling_handler=handler, mode="legacy") ``` """ diff --git a/fastmcp_slim/fastmcp/client/sampling/handlers/google_genai.py b/fastmcp_slim/fastmcp/client/sampling/handlers/google_genai.py index 7bfbbb626..c49c77635 100644 --- a/fastmcp_slim/fastmcp/client/sampling/handlers/google_genai.py +++ b/fastmcp_slim/fastmcp/client/sampling/handlers/google_genai.py @@ -60,18 +60,20 @@ class GoogleGenaiSamplingHandler: Example: ```python - from google.genai import Client - from fastmcp import FastMCP + from google.genai import Client as GoogleGenaiClient + from fastmcp import Client as FastMCPClient from fastmcp.client.sampling.handlers.google_genai import ( GoogleGenaiSamplingHandler, ) handler = GoogleGenaiSamplingHandler( default_model="gemini-2.0-flash", - client=Client(), + client=GoogleGenaiClient(), ) - server = FastMCP(sampling_handler=handler) + # Sampling is a server-initiated request, so it only exists on the + # handshake era; pass `mode="legacy"` to answer one. + client = FastMCPClient(server_url, sampling_handler=handler, mode="legacy") ``` """ diff --git a/fastmcp_slim/fastmcp/server/providers/proxy.py b/fastmcp_slim/fastmcp/server/providers/proxy.py index 60c71e961..fe62202e5 100644 --- a/fastmcp_slim/fastmcp/server/providers/proxy.py +++ b/fastmcp_slim/fastmcp/server/providers/proxy.py @@ -1348,12 +1348,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 9d87202c0..10fd47632 100644 --- a/tests/client/test_roots.py +++ b/tests/client/test_roots.py @@ -1,3 +1,5 @@ +import functools + import pytest from mcp_types import Root @@ -65,3 +67,40 @@ class TestClientRoots: assert len(calls) == 1 assert result.data == ["file://from/handler"] + + 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"]