fix: accept callable roots handlers (#4639)

* Accept callable roots handlers

🤖 Generated with OpenAI Codex

* fix: cover callable object roots handlers

🤖 Generated with OpenAI Codex

---------

Co-authored-by: Shuying <zsy@u.northwestern.edu>
This commit is contained in:
Shuying 2026-07-26 13:43:41 -05:00 committed by GitHub
commit 2a93404e8c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 41 additions and 7 deletions

View file

@ -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}")

View file

@ -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)

View file

@ -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"]