Merge remote-tracking branch 'origin/main' into modern-merge

# Conflicts:
#	tests/client/test_roots.py
This commit is contained in:
Jeremiah Lowin 2026-07-26 14:53:31 -04:00
commit 1593257f2a
No known key found for this signature in database
5 changed files with 51 additions and 13 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

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

View file

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

View file

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

View file

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