diff --git a/docs/clients/client.mdx b/docs/clients/client.mdx
index 3b086ab5c..02900b373 100644
--- a/docs/clients/client.mdx
+++ b/docs/clients/client.mdx
@@ -209,11 +209,19 @@ Available raw MCP methods:
These methods are especially useful for debugging or when you need to access metadata or fields that aren't exposed by the simplified methods.
-### Advanced Features
+### Additional Features
-MCP allows servers to interact with clients in order to provide additional capabilities. The `Client` constructor accepts additional configuration to handle these server requests.
+#### Pinging the server
-#### Timeout Control
+The client can be used to ping the server to verify connectivity.
+
+```python
+async with client:
+ await client.ping()
+ print("Server is reachable")
+```
+
+#### Timeouts
@@ -253,154 +261,7 @@ Timeout behavior varies between transport types:
For consistent behavior across all transports, we recommend explicitly setting timeouts at the individual tool call level when needed, rather than relying on client-level timeouts.
-#### Progress Tracking
-
-
-
-MCP servers can report progress during long-running operations. The client can set a progress handler to receive and process these updates.
-
-```python
-from fastmcp import Client
-from fastmcp.client.progress import ProgressHandler
-
-# A simple progress handler that prints progress updates
-async def my_progress_handler(
- progress: float,
- total: float | None,
- message: str | None
-) -> None:
- """Handle progress updates from the server."""
- if total is not None:
- percent = (progress / total) * 100
- print(f"Progress: {percent:.1f}% ({progress}/{total})")
- else:
- print(f"Progress: {progress}")
-
- if message:
- print(f"Message: {message}")
-
-# Set the progress handler at client level
-client = Client(
- my_mcp_server,
- progress_handler=my_progress_handler
-)
-```
-
-By default, FastMCP uses a handler that logs progress updates at the debug level. This default handler properly handles cases where `total` or `message` might be None.
-
-You can override the progress handler for specific tool calls:
-
-```python
-# Client uses the default debug logger for progress
-client = Client(my_mcp_server)
-
-async with client:
- # Use default progress handler (debug logging)
- result1 = await client.call_tool("long_task", {"param": "value"})
-
- # Override with custom progress handler just for this call
- result2 = await client.call_tool(
- "another_task",
- {"param": "value"},
- progress_handler=my_progress_handler
- )
-```
-
-A typical progress update includes:
-- Current progress value (e.g., 2 of 5 steps completed)
-- Total expected value (may be None)
-- Status message (may be None)
-
-#### LLM Sampling
-
-MCP Servers can request LLM completions from clients. The client can provide a `sampling_handler` to handle these requests. The sampling handler receives a list of messages and other parameters from the server, and should return a string completion.
-
-The following example uses the `marvin` library to generate a completion:
-
-```python {8-17, 21}
-import marvin
-from fastmcp import Client
-from fastmcp.client.sampling import (
- SamplingMessage,
- SamplingParams,
- RequestContext,
-)
-
-async def sampling_handler(
- messages: list[SamplingMessage],
- params: SamplingParams,
- context: RequestContext
-) -> str:
- return await marvin.say_async(
- message=[m.content.text for m in messages],
- instructions=params.systemPrompt,
- )
-
-client = Client(
- ...,
- sampling_handler=sampling_handler,
-)
-```
-
-#### Logging
-
-MCP servers can emit logs to clients. The client can set a logging callback to receive these logs.
-
-```python {4-5, 9}
-from fastmcp import Client
-from fastmcp.client.logging import LogHandler, LogMessage
-
-async def my_log_handler(params: LogMessage):
- print(f"[Server Log - {params.level.upper()}] {params.logger or 'default'}: {params.data}")
-
-client_with_logging = Client(
- ...,
- log_handler=my_log_handler,
-)
-```
-
-#### Roots
-
-Roots are a way for clients to inform servers about the resources they have access to or certain boundaries on their access. The server can use this information to adjust behavior or provide more accurate responses.
-
-Servers can request roots from clients, and clients can notify servers when their roots change.
-
-To set the roots when creating a client, users can either provide a list of roots (which can be a list of strings) or an async function that returns a list of roots.
-
-
-```python Static Roots {5}
-from fastmcp import Client
-
-client = Client(
- ...,
- roots=["/path/to/root1", "/path/to/root2"],
-)
-```
-```python Dynamic Roots Callback {4-6, 10}
-from fastmcp import Client
-from fastmcp.client.roots import RequestContext
-
-async def roots_callback(context: RequestContext) -> list[str]:
- print(f"Server requested roots (Request ID: {context.request_id})")
- return ["/path/to/root1", "/path/to/root2"]
-
-client = Client(
- ...,
- roots=roots_callback,
-)
-```
-
-### Utility Methods
-
-* **`ping()`**: Sends a ping request to the server to verify connectivity.
- ```python
- async def check_connection():
- async with client:
- await client.ping()
- print("Server is reachable")
- ```
-
-### Error Handling
+#### Error Handling
When a `call_tool` request results in an error on the server (e.g., the tool function raised an exception), the `client.call_tool()` method will raise a `fastmcp.client.ClientError`.
diff --git a/docs/clients/features.mdx b/docs/clients/features.mdx
new file mode 100644
index 000000000..cee3c461b
--- /dev/null
+++ b/docs/clients/features.mdx
@@ -0,0 +1,152 @@
+---
+title: Advanced Features
+sidebarTitle: Advanced Features
+description: Learn about the advanced features of the FastMCP Client.
+icon: stars
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+In addition to basic server interaction, FastMCP clients can also handle more advanced features and server interaction patterns. The `Client` constructor accepts additional configuration to handle these server requests.
+
+
+To enable many of these features, you must provide an appropriate handler or callback function. For example. In most cases, if you do not provide a handler, FastMCP's default handler will emit a `DEBUG` level log.
+
+
+## Logging and Notifications
+
+
+MCP servers can emit logs to clients. To process these logs, you can provide a `log_handler` to the client.
+
+The `log_handler` must be an async function that accepts a single argument, which is an instance of `fastmcp.client.logging.LogMessage`. This has attributes like `level`, `logger`, and `data`.
+
+```python {2, 12}
+from fastmcp import Client
+from fastmcp.client.logging import LogMessage
+
+async def log_handler(message: LogMessage):
+ level = message.level.upper()
+ logger = message.logger or 'default'
+ data = message.data
+ print(f"[Server Log - {level}] {logger}: {data}")
+
+client_with_logging = Client(
+ ...,
+ log_handler=log_handler,
+)
+```
+## Progress Monitoring
+
+
+
+MCP servers can report progress during long-running operations. The client can set a progress handler to receive and process these updates.
+
+```python {2, 13}
+from fastmcp import Client
+from fastmcp.client.progress import ProgressHandler
+
+async def my_progress_handler(
+ progress: float,
+ total: float | None,
+ message: str | None
+) -> None:
+ print(f"Progress: {progress} / {total} ({message})")
+
+client = Client(
+ ...,
+ progress_handler=my_progress_handler
+)
+```
+
+By default, FastMCP uses a handler that logs progress updates at the debug level. This default handler properly handles cases where `total` or `message` might be None.
+
+You can override the progress handler for specific tool calls:
+
+```python
+# Client uses the default debug logger for progress
+client = Client(...)
+
+async with client:
+ # Use default progress handler (debug logging)
+ result1 = await client.call_tool("long_task", {"param": "value"})
+
+ # Override with custom progress handler just for this call
+ result2 = await client.call_tool(
+ "another_task",
+ {"param": "value"},
+ progress_handler=my_progress_handler
+ )
+```
+
+A typical progress update includes:
+- Current progress value (e.g., 2 of 5 steps completed)
+- Total expected value (may be None)
+- Status message (may be None)
+
+## LLM Sampling
+
+
+
+MCP Servers can request LLM completions from clients. The client can provide a `sampling_handler` to handle these requests. The sampling handler receives a list of messages and other parameters from the server, and should return a string completion.
+
+The following example uses the `marvin` library to generate a completion:
+
+```python {8-17, 21}
+import marvin
+from fastmcp import Client
+from fastmcp.client.sampling import (
+ SamplingMessage,
+ SamplingParams,
+ RequestContext,
+)
+
+async def sampling_handler(
+ messages: list[SamplingMessage],
+ params: SamplingParams,
+ context: RequestContext
+) -> str:
+ return await marvin.say_async(
+ message=[m.content.text for m in messages],
+ instructions=params.systemPrompt,
+ )
+
+client = Client(
+ ...,
+ sampling_handler=sampling_handler,
+)
+```
+
+
+## Roots
+
+
+
+Roots are a way for clients to inform servers about the resources they have access to or certain boundaries on their access. The server can use this information to adjust behavior or provide more accurate responses.
+
+Servers can request roots from clients, and clients can notify servers when their roots change.
+
+To set the roots when creating a client, users can either provide a list of roots (which can be a list of strings) or an async function that returns a list of roots.
+
+
+```python Static Roots {5}
+from fastmcp import Client
+
+client = Client(
+ ...,
+ roots=["/path/to/root1", "/path/to/root2"],
+)
+```
+```python Dynamic Roots Callback {4-6, 10}
+from fastmcp import Client
+from fastmcp.client.roots import RequestContext
+
+async def roots_callback(context: RequestContext) -> list[str]:
+ print(f"Server requested roots (Request ID: {context.request_id})")
+ return ["/path/to/root1", "/path/to/root2"]
+
+client = Client(
+ ...,
+ roots=roots_callback,
+)
+```
+
\ No newline at end of file
diff --git a/docs/docs.json b/docs/docs.json
index 8498554b4..27e4a506c 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -67,6 +67,7 @@
"group": "Clients",
"pages": [
"clients/client",
+ "clients/features",
"clients/transports"
]
},
diff --git a/src/fastmcp/client/client.py b/src/fastmcp/client/client.py
index 01cfc287c..dcb4ff013 100644
--- a/src/fastmcp/client/client.py
+++ b/src/fastmcp/client/client.py
@@ -8,7 +8,12 @@ from exceptiongroup import catch
from mcp import ClientSession
from pydantic import AnyUrl
-from fastmcp.client.logging import LogHandler, MessageHandler, default_log_handler
+from fastmcp.client.logging import (
+ LogHandler,
+ MessageHandler,
+ create_log_callback,
+ default_log_handler,
+)
from fastmcp.client.progress import ProgressHandler, default_progress_handler
from fastmcp.client.roots import (
RootsHandler,
@@ -100,7 +105,7 @@ class Client:
self._session_kwargs: SessionKwargs = {
"sampling_callback": None,
"list_roots_callback": None,
- "logging_callback": log_handler,
+ "logging_callback": create_log_callback(log_handler),
"message_handler": message_handler,
"read_timeout_seconds": timeout,
}
diff --git a/src/fastmcp/client/logging.py b/src/fastmcp/client/logging.py
index 826eb0d28..d309a0674 100644
--- a/src/fastmcp/client/logging.py
+++ b/src/fastmcp/client/logging.py
@@ -1,9 +1,7 @@
+from collections.abc import Awaitable, Callable
from typing import TypeAlias
-from mcp.client.session import (
- LoggingFnT,
- MessageHandlerFnT,
-)
+from mcp.client.session import LoggingFnT, MessageHandlerFnT
from mcp.types import LoggingMessageNotificationParams
from fastmcp.utilities.logging import get_logger
@@ -11,11 +9,19 @@ from fastmcp.utilities.logging import get_logger
logger = get_logger(__name__)
LogMessage: TypeAlias = LoggingMessageNotificationParams
-LogHandler: TypeAlias = LoggingFnT
+LogHandler: TypeAlias = Callable[[LogMessage], Awaitable[None]]
MessageHandler: TypeAlias = MessageHandlerFnT
-__all__ = ["LogMessage", "LogHandler", "MessageHandler"]
+
+async def default_log_handler(message: LogMessage) -> None:
+ logger.debug(f"Log received: {message}")
-async def default_log_handler(params: LogMessage) -> None:
- logger.debug(f"Log received: {params}")
+def create_log_callback(handler: LogHandler | None = None) -> LoggingFnT:
+ if handler is None:
+ handler = default_log_handler
+
+ async def log_callback(params: LoggingMessageNotificationParams) -> None:
+ await handler(params)
+
+ return log_callback
diff --git a/tests/client/test_logs.py b/tests/client/test_logs.py
index 99b5f3db5..93f7720a1 100644
--- a/tests/client/test_logs.py
+++ b/tests/client/test_logs.py
@@ -9,8 +9,8 @@ class LogHandler:
def __init__(self):
self.logs: list[LogMessage] = []
- async def handle_log(self, params: LogMessage) -> None:
- self.logs.append(params)
+ async def handle_log(self, message: LogMessage) -> None:
+ self.logs.append(message)
@pytest.fixture