Jeremiah Lowin commited on
Commit
b05b338
·
1 Parent(s): 33f7965

Improve client documentation

Browse files
docs/clients/client.mdx CHANGED
@@ -209,11 +209,19 @@ Available raw MCP methods:
209
 
210
  These methods are especially useful for debugging or when you need to access metadata or fields that aren't exposed by the simplified methods.
211
 
212
- ### Advanced Features
213
 
214
- MCP allows servers to interact with clients in order to provide additional capabilities. The `Client` constructor accepts additional configuration to handle these server requests.
215
 
216
- #### Timeout Control
 
 
 
 
 
 
 
 
217
 
218
  <VersionBadge version="2.3.4" />
219
 
@@ -253,154 +261,7 @@ Timeout behavior varies between transport types:
253
  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.
254
  </Warning>
255
 
256
- #### Progress Tracking
257
-
258
- <VersionBadge version="2.3.5" />
259
-
260
- MCP servers can report progress during long-running operations. The client can set a progress handler to receive and process these updates.
261
-
262
- ```python
263
- from fastmcp import Client
264
- from fastmcp.client.progress import ProgressHandler
265
-
266
- # A simple progress handler that prints progress updates
267
- async def my_progress_handler(
268
- progress: float,
269
- total: float | None,
270
- message: str | None
271
- ) -> None:
272
- """Handle progress updates from the server."""
273
- if total is not None:
274
- percent = (progress / total) * 100
275
- print(f"Progress: {percent:.1f}% ({progress}/{total})")
276
- else:
277
- print(f"Progress: {progress}")
278
-
279
- if message:
280
- print(f"Message: {message}")
281
-
282
- # Set the progress handler at client level
283
- client = Client(
284
- my_mcp_server,
285
- progress_handler=my_progress_handler
286
- )
287
- ```
288
-
289
- 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.
290
-
291
- You can override the progress handler for specific tool calls:
292
-
293
- ```python
294
- # Client uses the default debug logger for progress
295
- client = Client(my_mcp_server)
296
-
297
- async with client:
298
- # Use default progress handler (debug logging)
299
- result1 = await client.call_tool("long_task", {"param": "value"})
300
-
301
- # Override with custom progress handler just for this call
302
- result2 = await client.call_tool(
303
- "another_task",
304
- {"param": "value"},
305
- progress_handler=my_progress_handler
306
- )
307
- ```
308
-
309
- A typical progress update includes:
310
- - Current progress value (e.g., 2 of 5 steps completed)
311
- - Total expected value (may be None)
312
- - Status message (may be None)
313
-
314
- #### LLM Sampling
315
-
316
- 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.
317
-
318
- The following example uses the `marvin` library to generate a completion:
319
-
320
- ```python {8-17, 21}
321
- import marvin
322
- from fastmcp import Client
323
- from fastmcp.client.sampling import (
324
- SamplingMessage,
325
- SamplingParams,
326
- RequestContext,
327
- )
328
-
329
- async def sampling_handler(
330
- messages: list[SamplingMessage],
331
- params: SamplingParams,
332
- context: RequestContext
333
- ) -> str:
334
- return await marvin.say_async(
335
- message=[m.content.text for m in messages],
336
- instructions=params.systemPrompt,
337
- )
338
-
339
- client = Client(
340
- ...,
341
- sampling_handler=sampling_handler,
342
- )
343
- ```
344
-
345
- #### Logging
346
-
347
- MCP servers can emit logs to clients. The client can set a logging callback to receive these logs.
348
-
349
- ```python {4-5, 9}
350
- from fastmcp import Client
351
- from fastmcp.client.logging import LogHandler, LogMessage
352
-
353
- async def my_log_handler(params: LogMessage):
354
- print(f"[Server Log - {params.level.upper()}] {params.logger or 'default'}: {params.data}")
355
-
356
- client_with_logging = Client(
357
- ...,
358
- log_handler=my_log_handler,
359
- )
360
- ```
361
-
362
- #### Roots
363
-
364
- 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.
365
-
366
- Servers can request roots from clients, and clients can notify servers when their roots change.
367
-
368
- 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.
369
-
370
- <CodeGroup>
371
- ```python Static Roots {5}
372
- from fastmcp import Client
373
-
374
- client = Client(
375
- ...,
376
- roots=["/path/to/root1", "/path/to/root2"],
377
- )
378
- ```
379
- ```python Dynamic Roots Callback {4-6, 10}
380
- from fastmcp import Client
381
- from fastmcp.client.roots import RequestContext
382
-
383
- async def roots_callback(context: RequestContext) -> list[str]:
384
- print(f"Server requested roots (Request ID: {context.request_id})")
385
- return ["/path/to/root1", "/path/to/root2"]
386
-
387
- client = Client(
388
- ...,
389
- roots=roots_callback,
390
- )
391
- ```
392
- </CodeGroup>
393
- ### Utility Methods
394
-
395
- * **`ping()`**: Sends a ping request to the server to verify connectivity.
396
- ```python
397
- async def check_connection():
398
- async with client:
399
- await client.ping()
400
- print("Server is reachable")
401
- ```
402
-
403
- ### Error Handling
404
 
405
  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`.
406
 
 
209
 
210
  These methods are especially useful for debugging or when you need to access metadata or fields that aren't exposed by the simplified methods.
211
 
212
+ ### Additional Features
213
 
214
+ #### Pinging the server
215
 
216
+ The client can be used to ping the server to verify connectivity.
217
+
218
+ ```python
219
+ async with client:
220
+ await client.ping()
221
+ print("Server is reachable")
222
+ ```
223
+
224
+ #### Timeouts
225
 
226
  <VersionBadge version="2.3.4" />
227
 
 
261
  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.
262
  </Warning>
263
 
264
+ #### Error Handling
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
 
266
  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`.
267
 
docs/clients/features.mdx ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Advanced Features
3
+ sidebarTitle: Advanced Features
4
+ description: Learn about the advanced features of the FastMCP Client.
5
+ icon: stars
6
+ ---
7
+
8
+ import { VersionBadge } from '/snippets/version-badge.mdx'
9
+
10
+ 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.
11
+
12
+ <Tip>
13
+ 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.
14
+ </Tip>
15
+
16
+ ## Logging and Notifications
17
+
18
+ <VersionBadge version="2.0.0" />
19
+ MCP servers can emit logs to clients. To process these logs, you can provide a `log_handler` to the client.
20
+
21
+ 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`.
22
+
23
+ ```python {2, 12}
24
+ from fastmcp import Client
25
+ from fastmcp.client.logging import LogMessage
26
+
27
+ async def log_handler(message: LogMessage):
28
+ level = message.level.upper()
29
+ logger = message.logger or 'default'
30
+ data = message.data
31
+ print(f"[Server Log - {level}] {logger}: {data}")
32
+
33
+ client_with_logging = Client(
34
+ ...,
35
+ log_handler=log_handler,
36
+ )
37
+ ```
38
+ ## Progress Monitoring
39
+
40
+ <VersionBadge version="2.3.5" />
41
+
42
+ MCP servers can report progress during long-running operations. The client can set a progress handler to receive and process these updates.
43
+
44
+ ```python {2, 13}
45
+ from fastmcp import Client
46
+ from fastmcp.client.progress import ProgressHandler
47
+
48
+ async def my_progress_handler(
49
+ progress: float,
50
+ total: float | None,
51
+ message: str | None
52
+ ) -> None:
53
+ print(f"Progress: {progress} / {total} ({message})")
54
+
55
+ client = Client(
56
+ ...,
57
+ progress_handler=my_progress_handler
58
+ )
59
+ ```
60
+
61
+ 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.
62
+
63
+ You can override the progress handler for specific tool calls:
64
+
65
+ ```python
66
+ # Client uses the default debug logger for progress
67
+ client = Client(...)
68
+
69
+ async with client:
70
+ # Use default progress handler (debug logging)
71
+ result1 = await client.call_tool("long_task", {"param": "value"})
72
+
73
+ # Override with custom progress handler just for this call
74
+ result2 = await client.call_tool(
75
+ "another_task",
76
+ {"param": "value"},
77
+ progress_handler=my_progress_handler
78
+ )
79
+ ```
80
+
81
+ A typical progress update includes:
82
+ - Current progress value (e.g., 2 of 5 steps completed)
83
+ - Total expected value (may be None)
84
+ - Status message (may be None)
85
+
86
+ ## LLM Sampling
87
+
88
+ <VersionBadge version="2.0.0" />
89
+
90
+ 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.
91
+
92
+ The following example uses the `marvin` library to generate a completion:
93
+
94
+ ```python {8-17, 21}
95
+ import marvin
96
+ from fastmcp import Client
97
+ from fastmcp.client.sampling import (
98
+ SamplingMessage,
99
+ SamplingParams,
100
+ RequestContext,
101
+ )
102
+
103
+ async def sampling_handler(
104
+ messages: list[SamplingMessage],
105
+ params: SamplingParams,
106
+ context: RequestContext
107
+ ) -> str:
108
+ return await marvin.say_async(
109
+ message=[m.content.text for m in messages],
110
+ instructions=params.systemPrompt,
111
+ )
112
+
113
+ client = Client(
114
+ ...,
115
+ sampling_handler=sampling_handler,
116
+ )
117
+ ```
118
+
119
+
120
+ ## Roots
121
+
122
+ <VersionBadge version="2.0.0" />
123
+
124
+ 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.
125
+
126
+ Servers can request roots from clients, and clients can notify servers when their roots change.
127
+
128
+ 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.
129
+
130
+ <CodeGroup>
131
+ ```python Static Roots {5}
132
+ from fastmcp import Client
133
+
134
+ client = Client(
135
+ ...,
136
+ roots=["/path/to/root1", "/path/to/root2"],
137
+ )
138
+ ```
139
+ ```python Dynamic Roots Callback {4-6, 10}
140
+ from fastmcp import Client
141
+ from fastmcp.client.roots import RequestContext
142
+
143
+ async def roots_callback(context: RequestContext) -> list[str]:
144
+ print(f"Server requested roots (Request ID: {context.request_id})")
145
+ return ["/path/to/root1", "/path/to/root2"]
146
+
147
+ client = Client(
148
+ ...,
149
+ roots=roots_callback,
150
+ )
151
+ ```
152
+ </CodeGroup>
docs/docs.json CHANGED
@@ -67,6 +67,7 @@
67
  "group": "Clients",
68
  "pages": [
69
  "clients/client",
 
70
  "clients/transports"
71
  ]
72
  },
 
67
  "group": "Clients",
68
  "pages": [
69
  "clients/client",
70
+ "clients/features",
71
  "clients/transports"
72
  ]
73
  },
src/fastmcp/client/client.py CHANGED
@@ -8,7 +8,12 @@ from exceptiongroup import catch
8
  from mcp import ClientSession
9
  from pydantic import AnyUrl
10
 
11
- from fastmcp.client.logging import LogHandler, MessageHandler, default_log_handler
 
 
 
 
 
12
  from fastmcp.client.progress import ProgressHandler, default_progress_handler
13
  from fastmcp.client.roots import (
14
  RootsHandler,
@@ -100,7 +105,7 @@ class Client:
100
  self._session_kwargs: SessionKwargs = {
101
  "sampling_callback": None,
102
  "list_roots_callback": None,
103
- "logging_callback": log_handler,
104
  "message_handler": message_handler,
105
  "read_timeout_seconds": timeout,
106
  }
 
8
  from mcp import ClientSession
9
  from pydantic import AnyUrl
10
 
11
+ from fastmcp.client.logging import (
12
+ LogHandler,
13
+ MessageHandler,
14
+ create_log_callback,
15
+ default_log_handler,
16
+ )
17
  from fastmcp.client.progress import ProgressHandler, default_progress_handler
18
  from fastmcp.client.roots import (
19
  RootsHandler,
 
105
  self._session_kwargs: SessionKwargs = {
106
  "sampling_callback": None,
107
  "list_roots_callback": None,
108
+ "logging_callback": create_log_callback(log_handler),
109
  "message_handler": message_handler,
110
  "read_timeout_seconds": timeout,
111
  }
src/fastmcp/client/logging.py CHANGED
@@ -1,9 +1,7 @@
 
1
  from typing import TypeAlias
2
 
3
- from mcp.client.session import (
4
- LoggingFnT,
5
- MessageHandlerFnT,
6
- )
7
  from mcp.types import LoggingMessageNotificationParams
8
 
9
  from fastmcp.utilities.logging import get_logger
@@ -11,11 +9,19 @@ from fastmcp.utilities.logging import get_logger
11
  logger = get_logger(__name__)
12
 
13
  LogMessage: TypeAlias = LoggingMessageNotificationParams
14
- LogHandler: TypeAlias = LoggingFnT
15
  MessageHandler: TypeAlias = MessageHandlerFnT
16
 
17
- __all__ = ["LogMessage", "LogHandler", "MessageHandler"]
18
 
 
 
19
 
20
- async def default_log_handler(params: LogMessage) -> None:
21
- logger.debug(f"Log received: {params}")
 
 
 
 
 
 
 
 
1
+ from collections.abc import Awaitable, Callable
2
  from typing import TypeAlias
3
 
4
+ from mcp.client.session import LoggingFnT, MessageHandlerFnT
 
 
 
5
  from mcp.types import LoggingMessageNotificationParams
6
 
7
  from fastmcp.utilities.logging import get_logger
 
9
  logger = get_logger(__name__)
10
 
11
  LogMessage: TypeAlias = LoggingMessageNotificationParams
12
+ LogHandler: TypeAlias = Callable[[LogMessage], Awaitable[None]]
13
  MessageHandler: TypeAlias = MessageHandlerFnT
14
 
 
15
 
16
+ async def default_log_handler(message: LogMessage) -> None:
17
+ logger.debug(f"Log received: {message}")
18
 
19
+
20
+ def create_log_callback(handler: LogHandler | None = None) -> LoggingFnT:
21
+ if handler is None:
22
+ handler = default_log_handler
23
+
24
+ async def log_callback(params: LoggingMessageNotificationParams) -> None:
25
+ await handler(params)
26
+
27
+ return log_callback
tests/client/test_logs.py CHANGED
@@ -9,8 +9,8 @@ class LogHandler:
9
  def __init__(self):
10
  self.logs: list[LogMessage] = []
11
 
12
- async def handle_log(self, params: LogMessage) -> None:
13
- self.logs.append(params)
14
 
15
 
16
  @pytest.fixture
 
9
  def __init__(self):
10
  self.logs: list[LogMessage] = []
11
 
12
+ async def handle_log(self, message: LogMessage) -> None:
13
+ self.logs.append(message)
14
 
15
 
16
  @pytest.fixture