fastmcp 2.3.4__py3-none-any.whl → 2.3.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
fastmcp/cli/cli.py CHANGED
@@ -17,6 +17,7 @@ from typer import Context, Exit
17
17
 
18
18
  import fastmcp
19
19
  from fastmcp.cli import claude
20
+ from fastmcp.cli import run as run_module
20
21
  from fastmcp.utilities.logging import get_logger
21
22
 
22
23
  logger = get_logger("cli")
@@ -58,7 +59,7 @@ def _parse_env_var(env_var: str) -> tuple[str, str]:
58
59
 
59
60
 
60
61
  def _build_uv_command(
61
- file_spec: str,
62
+ server_spec: str,
62
63
  with_editable: Path | None = None,
63
64
  with_packages: list[str] | None = None,
64
65
  ) -> list[str]:
@@ -76,106 +77,10 @@ def _build_uv_command(
76
77
  cmd.extend(["--with", pkg])
77
78
 
78
79
  # Add mcp run command
79
- cmd.extend(["fastmcp", "run", file_spec])
80
+ cmd.extend(["fastmcp", "run", server_spec])
80
81
  return cmd
81
82
 
82
83
 
83
- def _parse_file_path(file_spec: str) -> tuple[Path, str | None]:
84
- """Parse a file path that may include a server object specification.
85
-
86
- Args:
87
- file_spec: Path to file, optionally with :object suffix
88
-
89
- Returns:
90
- Tuple of (file_path, server_object)
91
- """
92
- # First check if we have a Windows path (e.g., C:\...)
93
- has_windows_drive = len(file_spec) > 1 and file_spec[1] == ":"
94
-
95
- # Split on the last colon, but only if it's not part of the Windows drive letter
96
- # and there's actually another colon in the string after the drive letter
97
- if ":" in (file_spec[2:] if has_windows_drive else file_spec):
98
- file_str, server_object = file_spec.rsplit(":", 1)
99
- else:
100
- file_str, server_object = file_spec, None
101
-
102
- # Resolve the file path
103
- file_path = Path(file_str).expanduser().resolve()
104
- if not file_path.exists():
105
- logger.error(f"File not found: {file_path}")
106
- sys.exit(1)
107
- if not file_path.is_file():
108
- logger.error(f"Not a file: {file_path}")
109
- sys.exit(1)
110
-
111
- return file_path, server_object
112
-
113
-
114
- def _import_server(file: Path, server_object: str | None = None):
115
- """Import a MCP server from a file.
116
-
117
- Args:
118
- file: Path to the file
119
- server_object: Optional object name in format "module:object" or just "object"
120
-
121
- Returns:
122
- The server object
123
- """
124
- # Add parent directory to Python path so imports can be resolved
125
- file_dir = str(file.parent)
126
- if file_dir not in sys.path:
127
- sys.path.insert(0, file_dir)
128
-
129
- # Import the module
130
- spec = importlib.util.spec_from_file_location("server_module", file)
131
- if not spec or not spec.loader:
132
- logger.error("Could not load module", extra={"file": str(file)})
133
- sys.exit(1)
134
-
135
- module = importlib.util.module_from_spec(spec)
136
- spec.loader.exec_module(module)
137
-
138
- # If no object specified, try common server names
139
- if not server_object:
140
- # Look for the most common server object names
141
- for name in ["mcp", "server", "app"]:
142
- if hasattr(module, name):
143
- return getattr(module, name)
144
-
145
- logger.error(
146
- f"No server object found in {file}. Please either:\n"
147
- "1. Use a standard variable name (mcp, server, or app)\n"
148
- "2. Specify the object name with file:object syntax",
149
- extra={"file": str(file)},
150
- )
151
- sys.exit(1)
152
-
153
- # Handle module:object syntax
154
- if ":" in server_object:
155
- module_name, object_name = server_object.split(":", 1)
156
- try:
157
- server_module = importlib.import_module(module_name)
158
- server = getattr(server_module, object_name, None)
159
- except ImportError:
160
- logger.error(
161
- f"Could not import module '{module_name}'",
162
- extra={"file": str(file)},
163
- )
164
- sys.exit(1)
165
- else:
166
- # Just object name
167
- server = getattr(module, server_object, None)
168
-
169
- if server is None:
170
- logger.error(
171
- f"Server object '{server_object}' not found",
172
- extra={"file": str(file)},
173
- )
174
- sys.exit(1)
175
-
176
- return server
177
-
178
-
179
84
  @app.command()
180
85
  def version(ctx: Context):
181
86
  if ctx.resilient_parsing:
@@ -201,7 +106,7 @@ def version(ctx: Context):
201
106
 
202
107
  @app.command()
203
108
  def dev(
204
- file_spec: str = typer.Argument(
109
+ server_spec: str = typer.Argument(
205
110
  ...,
206
111
  help="Python file to run, optionally with :object suffix",
207
112
  ),
@@ -246,7 +151,7 @@ def dev(
246
151
  ] = None,
247
152
  ) -> None:
248
153
  """Run a MCP server with the MCP Inspector."""
249
- file, server_object = _parse_file_path(file_spec)
154
+ file, server_object = run_module.parse_file_path(server_spec)
250
155
 
251
156
  logger.debug(
252
157
  "Starting dev server",
@@ -262,8 +167,8 @@ def dev(
262
167
 
263
168
  try:
264
169
  # Import server to get dependencies
265
- server = _import_server(file, server_object)
266
- if hasattr(server, "dependencies"):
170
+ server = run_module.import_server(file, server_object)
171
+ if hasattr(server, "dependencies") and server.dependencies is not None:
267
172
  with_packages = list(set(with_packages + server.dependencies))
268
173
 
269
174
  env_vars = {}
@@ -285,7 +190,7 @@ def dev(
285
190
  if inspector_version:
286
191
  inspector_cmd += f"@{inspector_version}"
287
192
 
288
- uv_cmd = _build_uv_command(file_spec, with_editable, with_packages)
193
+ uv_cmd = _build_uv_command(server_spec, with_editable, with_packages)
289
194
 
290
195
  # Run the MCP Inspector command with shell=True on Windows
291
196
  shell = sys.platform == "win32"
@@ -318,9 +223,9 @@ def dev(
318
223
 
319
224
  @app.command()
320
225
  def run(
321
- file_spec: str = typer.Argument(
226
+ server_spec: str = typer.Argument(
322
227
  ...,
323
- help="Python file to run, optionally with :object suffix",
228
+ help="Python file, object specification (file:obj), or URL",
324
229
  ),
325
230
  transport: Annotated[
326
231
  str | None,
@@ -354,22 +259,20 @@ def run(
354
259
  ),
355
260
  ] = None,
356
261
  ) -> None:
357
- """Run a MCP server.
262
+ """Run a MCP server or connect to a remote one.
358
263
 
359
- The server can be specified in two ways:
360
- 1. Module approach: server.py - runs the module directly, expecting a server.run() call.\n
361
- 2. Import approach: server.py:app - imports and runs the specified server object.\n\n
264
+ The server can be specified in three ways:
265
+ 1. Module approach: server.py - runs the module directly, looking for an object named mcp/server/app.\n
266
+ 2. Import approach: server.py:app - imports and runs the specified server object.\n
267
+ 3. URL approach: http://server-url - connects to a remote server and creates a proxy.\n\n
362
268
 
363
269
  Note: This command runs the server directly. You are responsible for ensuring
364
270
  all dependencies are available.
365
271
  """
366
- file, server_object = _parse_file_path(file_spec)
367
-
368
272
  logger.debug(
369
- "Running server",
273
+ "Running server or client",
370
274
  extra={
371
- "file": str(file),
372
- "server_object": server_object,
275
+ "server_spec": server_spec,
373
276
  "transport": transport,
374
277
  "host": host,
375
278
  "port": port,
@@ -378,29 +281,18 @@ def run(
378
281
  )
379
282
 
380
283
  try:
381
- # Import and get server object
382
- server = _import_server(file, server_object)
383
-
384
- logger.info(f'Found server "{server.name}" in {file}')
385
-
386
- # Run the server
387
- kwargs = {}
388
- if transport:
389
- kwargs["transport"] = transport
390
- if host:
391
- kwargs["host"] = host
392
- if port:
393
- kwargs["port"] = port
394
- if log_level:
395
- kwargs["log_level"] = log_level
396
-
397
- server.run(**kwargs)
398
-
284
+ run_module.run_command(
285
+ server_spec=server_spec,
286
+ transport=transport,
287
+ host=host,
288
+ port=port,
289
+ log_level=log_level,
290
+ )
399
291
  except Exception as e:
400
292
  logger.error(
401
- f"Failed to run server: {e}",
293
+ f"Failed to run: {e}",
402
294
  extra={
403
- "file": str(file),
295
+ "server_spec": server_spec,
404
296
  "error": str(e),
405
297
  },
406
298
  )
@@ -409,7 +301,7 @@ def run(
409
301
 
410
302
  @app.command()
411
303
  def install(
412
- file_spec: str = typer.Argument(
304
+ server_spec: str = typer.Argument(
413
305
  ...,
414
306
  help="Python file to run, optionally with :object suffix",
415
307
  ),
@@ -466,7 +358,7 @@ def install(
466
358
  Environment variables are preserved once added and only updated if new values
467
359
  are explicitly provided.
468
360
  """
469
- file, server_object = _parse_file_path(file_spec)
361
+ file, server_object = run_module.parse_file_path(server_spec)
470
362
 
471
363
  logger.debug(
472
364
  "Installing server",
@@ -489,7 +381,7 @@ def install(
489
381
  server = None
490
382
  if not name:
491
383
  try:
492
- server = _import_server(file, server_object)
384
+ server = run_module.import_server(file, server_object)
493
385
  name = server.name
494
386
  except (ImportError, ModuleNotFoundError) as e:
495
387
  logger.debug(
@@ -526,7 +418,7 @@ def install(
526
418
  env_dict[key] = value
527
419
 
528
420
  if claude.update_claude_config(
529
- file_spec,
421
+ server_spec,
530
422
  name,
531
423
  with_editable=with_editable,
532
424
  with_packages=with_packages,
fastmcp/cli/run.py ADDED
@@ -0,0 +1,179 @@
1
+ """FastMCP run command implementation."""
2
+
3
+ import importlib.util
4
+ import re
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Any, Literal
8
+
9
+ from fastmcp.utilities.logging import get_logger
10
+
11
+ logger = get_logger("cli.run")
12
+
13
+ TransportType = Literal["stdio", "streamable-http", "sse"]
14
+
15
+
16
+ def is_url(path: str) -> bool:
17
+ """Check if a string is a URL."""
18
+ url_pattern = re.compile(r"^https?://")
19
+ return bool(url_pattern.match(path))
20
+
21
+
22
+ def parse_file_path(server_spec: str) -> tuple[Path, str | None]:
23
+ """Parse a file path that may include a server object specification.
24
+
25
+ Args:
26
+ server_spec: Path to file, optionally with :object suffix
27
+
28
+ Returns:
29
+ Tuple of (file_path, server_object)
30
+ """
31
+ # First check if we have a Windows path (e.g., C:\...)
32
+ has_windows_drive = len(server_spec) > 1 and server_spec[1] == ":"
33
+
34
+ # Split on the last colon, but only if it's not part of the Windows drive letter
35
+ # and there's actually another colon in the string after the drive letter
36
+ if ":" in (server_spec[2:] if has_windows_drive else server_spec):
37
+ file_str, server_object = server_spec.rsplit(":", 1)
38
+ else:
39
+ file_str, server_object = server_spec, None
40
+
41
+ # Resolve the file path
42
+ file_path = Path(file_str).expanduser().resolve()
43
+ if not file_path.exists():
44
+ logger.error(f"File not found: {file_path}")
45
+ sys.exit(1)
46
+ if not file_path.is_file():
47
+ logger.error(f"Not a file: {file_path}")
48
+ sys.exit(1)
49
+
50
+ return file_path, server_object
51
+
52
+
53
+ def import_server(file: Path, server_object: str | None = None) -> Any:
54
+ """Import a MCP server from a file.
55
+
56
+ Args:
57
+ file: Path to the file
58
+ server_object: Optional object name in format "module:object" or just "object"
59
+
60
+ Returns:
61
+ The server object
62
+ """
63
+ # Add parent directory to Python path so imports can be resolved
64
+ file_dir = str(file.parent)
65
+ if file_dir not in sys.path:
66
+ sys.path.insert(0, file_dir)
67
+
68
+ # Import the module
69
+ spec = importlib.util.spec_from_file_location("server_module", file)
70
+ if not spec or not spec.loader:
71
+ logger.error("Could not load module", extra={"file": str(file)})
72
+ sys.exit(1)
73
+
74
+ module = importlib.util.module_from_spec(spec)
75
+ spec.loader.exec_module(module)
76
+
77
+ # If no object specified, try common server names
78
+ if not server_object:
79
+ # Look for the most common server object names
80
+ for name in ["mcp", "server", "app"]:
81
+ if hasattr(module, name):
82
+ return getattr(module, name)
83
+
84
+ logger.error(
85
+ f"No server object found in {file}. Please either:\n"
86
+ "1. Use a standard variable name (mcp, server, or app)\n"
87
+ "2. Specify the object name with file:object syntax",
88
+ extra={"file": str(file)},
89
+ )
90
+ sys.exit(1)
91
+
92
+ # Handle module:object syntax
93
+ if ":" in server_object:
94
+ module_name, object_name = server_object.split(":", 1)
95
+ try:
96
+ server_module = importlib.import_module(module_name)
97
+ server = getattr(server_module, object_name, None)
98
+ except ImportError:
99
+ logger.error(
100
+ f"Could not import module '{module_name}'",
101
+ extra={"file": str(file)},
102
+ )
103
+ sys.exit(1)
104
+ else:
105
+ # Just object name
106
+ server = getattr(module, server_object, None)
107
+
108
+ if server is None:
109
+ logger.error(
110
+ f"Server object '{server_object}' not found",
111
+ extra={"file": str(file)},
112
+ )
113
+ sys.exit(1)
114
+
115
+ return server
116
+
117
+
118
+ def create_client_server(url: str) -> Any:
119
+ """Create a FastMCP server from a client URL.
120
+
121
+ Args:
122
+ url: The URL to connect to
123
+
124
+ Returns:
125
+ A FastMCP server instance
126
+ """
127
+ try:
128
+ import fastmcp
129
+
130
+ client = fastmcp.Client(url)
131
+ server = fastmcp.FastMCP.from_client(client)
132
+ return server
133
+ except Exception as e:
134
+ logger.error(f"Failed to create client for URL {url}: {e}")
135
+ sys.exit(1)
136
+
137
+
138
+ def run_command(
139
+ server_spec: str,
140
+ transport: str | None = None,
141
+ host: str | None = None,
142
+ port: int | None = None,
143
+ log_level: str | None = None,
144
+ ) -> None:
145
+ """Run a MCP server or connect to a remote one.
146
+
147
+ Args:
148
+ server_spec: Python file, object specification (file:obj), or URL
149
+ transport: Transport protocol to use
150
+ host: Host to bind to when using http transport
151
+ port: Port to bind to when using http transport
152
+ log_level: Log level
153
+ """
154
+ if is_url(server_spec):
155
+ # Handle URL case
156
+ server = create_client_server(server_spec)
157
+ logger.debug(f"Created client proxy server for {server_spec}")
158
+ else:
159
+ # Handle file case
160
+ file, server_object = parse_file_path(server_spec)
161
+ server = import_server(file, server_object)
162
+ logger.debug(f'Found server "{server.name}" in {file}')
163
+
164
+ # Run the server
165
+ kwargs = {}
166
+ if transport:
167
+ kwargs["transport"] = transport
168
+ if host:
169
+ kwargs["host"] = host
170
+ if port:
171
+ kwargs["port"] = port
172
+ if log_level:
173
+ kwargs["log_level"] = log_level
174
+
175
+ try:
176
+ server.run(**kwargs)
177
+ except Exception as e:
178
+ logger.error(f"Failed to run server: {e}")
179
+ sys.exit(1)
fastmcp/client/client.py CHANGED
@@ -1,5 +1,5 @@
1
1
  import datetime
2
- from contextlib import AsyncExitStack
2
+ from contextlib import AsyncExitStack, asynccontextmanager
3
3
  from pathlib import Path
4
4
  from typing import Any, cast
5
5
 
@@ -8,7 +8,8 @@ from exceptiongroup import catch
8
8
  from mcp import ClientSession
9
9
  from pydantic import AnyUrl
10
10
 
11
- from fastmcp.client.logging import LogHandler, MessageHandler
11
+ from fastmcp.client.logging import LogHandler, MessageHandler, default_log_handler
12
+ from fastmcp.client.progress import ProgressHandler, default_progress_handler
12
13
  from fastmcp.client.roots import (
13
14
  RootsHandler,
14
15
  RootsList,
@@ -28,6 +29,7 @@ __all__ = [
28
29
  "LogHandler",
29
30
  "MessageHandler",
30
31
  "SamplingHandler",
32
+ "ProgressHandler",
31
33
  ]
32
34
 
33
35
 
@@ -50,6 +52,7 @@ class Client:
50
52
  sampling_handler: Optional handler for sampling requests
51
53
  log_handler: Optional handler for log messages
52
54
  message_handler: Optional handler for protocol messages
55
+ progress_handler: Optional handler for progress notifications
53
56
  timeout: Optional timeout for requests (seconds or timedelta)
54
57
 
55
58
  Examples:
@@ -74,12 +77,22 @@ class Client:
74
77
  sampling_handler: SamplingHandler | None = None,
75
78
  log_handler: LogHandler | None = None,
76
79
  message_handler: MessageHandler | None = None,
80
+ progress_handler: ProgressHandler | None = None,
77
81
  timeout: datetime.timedelta | float | int | None = None,
78
82
  ):
79
83
  self.transport = infer_transport(transport)
80
84
  self._session: ClientSession | None = None
81
85
  self._exit_stack: AsyncExitStack | None = None
82
86
  self._nesting_counter: int = 0
87
+ self._initialize_result: mcp.types.InitializeResult | None = None
88
+
89
+ if log_handler is None:
90
+ log_handler = default_log_handler
91
+
92
+ if progress_handler is None:
93
+ progress_handler = default_progress_handler
94
+
95
+ self._progress_handler = progress_handler
83
96
 
84
97
  if isinstance(timeout, int | float):
85
98
  timeout = datetime.timedelta(seconds=timeout)
@@ -96,17 +109,28 @@ class Client:
96
109
  self.set_roots(roots)
97
110
 
98
111
  if sampling_handler is not None:
99
- self.set_sampling_callback(sampling_handler)
112
+ self._session_kwargs["sampling_callback"] = create_sampling_callback(
113
+ sampling_handler
114
+ )
100
115
 
101
116
  @property
102
117
  def session(self) -> ClientSession:
103
118
  """Get the current active session. Raises RuntimeError if not connected."""
104
119
  if self._session is None:
105
120
  raise RuntimeError(
106
- "Client is not connected. Use 'async with client:' context manager first."
121
+ "Client is not connected. Use the 'async with client:' context manager first."
107
122
  )
108
123
  return self._session
109
124
 
125
+ @property
126
+ def initialize_result(self) -> mcp.types.InitializeResult:
127
+ """Get the result of the initialization request."""
128
+ if self._initialize_result is None:
129
+ raise RuntimeError(
130
+ "Client is not connected. Use the 'async with client:' context manager first."
131
+ )
132
+ return self._initialize_result
133
+
110
134
  def set_roots(self, roots: RootsList | RootsHandler) -> None:
111
135
  """Set the roots for the client. This does not automatically call `send_roots_list_changed`."""
112
136
  self._session_kwargs["list_roots_callback"] = create_roots_callback(roots)
@@ -121,27 +145,35 @@ class Client:
121
145
  """Check if the client is currently connected."""
122
146
  return self._session is not None
123
147
 
148
+ @asynccontextmanager
149
+ async def _context_manager(self):
150
+ with catch(get_catch_handlers()):
151
+ async with self.transport.connect_session(
152
+ **self._session_kwargs
153
+ ) as session:
154
+ self._session = session
155
+ # Initialize the session
156
+ self._initialize_result = await self._session.initialize()
157
+
158
+ try:
159
+ yield
160
+ finally:
161
+ self._exit_stack = None
162
+ self._session = None
163
+ self._initialize_result = None
164
+
124
165
  async def __aenter__(self):
125
166
  if self._nesting_counter == 0:
126
167
  # Create exit stack to manage both context managers
127
168
  stack = AsyncExitStack()
128
169
  await stack.__aenter__()
129
170
 
130
- # Add the exception handling context
131
- stack.enter_context(catch(get_catch_handlers()))
171
+ await stack.enter_async_context(self._context_manager())
132
172
 
133
- # the above catch will only apply once this __aenter__ finishes so
134
- # we need to wrap the session creation in a new context in case it
135
- # raises errors itself
136
- with catch(get_catch_handlers()):
137
- # Create and enter the transport session using the exit stack
138
- session_cm = self.transport.connect_session(**self._session_kwargs)
139
- self._session = await stack.enter_async_context(session_cm)
140
-
141
- # Store the stack for cleanup in __aexit__
142
173
  self._exit_stack = stack
143
174
 
144
175
  self._nesting_counter += 1
176
+
145
177
  return self
146
178
 
147
179
  async def __aexit__(self, exc_type, exc_val, exc_tb):
@@ -154,7 +186,6 @@ class Client:
154
186
  await self._exit_stack.__aexit__(exc_type, exc_val, exc_tb)
155
187
  finally:
156
188
  self._exit_stack = None
157
- self._session = None
158
189
 
159
190
  # --- MCP Client Methods ---
160
191
 
@@ -168,9 +199,12 @@ class Client:
168
199
  progress_token: str | int,
169
200
  progress: float,
170
201
  total: float | None = None,
202
+ message: str | None = None,
171
203
  ) -> None:
172
204
  """Send a progress notification."""
173
- await self.session.send_progress_notification(progress_token, progress, total)
205
+ await self.session.send_progress_notification(
206
+ progress_token, progress, total, message
207
+ )
174
208
 
175
209
  async def set_logging_level(self, level: mcp.types.LoggingLevel) -> None:
176
210
  """Send a logging/setLevel request."""
@@ -430,6 +464,7 @@ class Client:
430
464
  self,
431
465
  name: str,
432
466
  arguments: dict[str, Any],
467
+ progress_handler: ProgressHandler | None = None,
433
468
  timeout: datetime.timedelta | float | int | None = None,
434
469
  ) -> mcp.types.CallToolResult:
435
470
  """Send a tools/call request and return the complete MCP protocol result.
@@ -441,6 +476,8 @@ class Client:
441
476
  name (str): The name of the tool to call.
442
477
  arguments (dict[str, Any]): Arguments to pass to the tool.
443
478
  timeout (datetime.timedelta | float | int | None, optional): The timeout for the tool call. Defaults to None.
479
+ progress_handler (ProgressHandler | None, optional): The progress handler to use for the tool call. Defaults to None.
480
+
444
481
  Returns:
445
482
  mcp.types.CallToolResult: The complete response object from the protocol,
446
483
  containing the tool result and any additional metadata.
@@ -452,7 +489,10 @@ class Client:
452
489
  if isinstance(timeout, int | float):
453
490
  timeout = datetime.timedelta(seconds=timeout)
454
491
  result = await self.session.call_tool(
455
- name=name, arguments=arguments, read_timeout_seconds=timeout
492
+ name=name,
493
+ arguments=arguments,
494
+ read_timeout_seconds=timeout,
495
+ progress_callback=progress_handler or self._progress_handler,
456
496
  )
457
497
  return result
458
498
 
@@ -461,6 +501,7 @@ class Client:
461
501
  name: str,
462
502
  arguments: dict[str, Any] | None = None,
463
503
  timeout: datetime.timedelta | float | int | None = None,
504
+ progress_handler: ProgressHandler | None = None,
464
505
  ) -> list[
465
506
  mcp.types.TextContent | mcp.types.ImageContent | mcp.types.EmbeddedResource
466
507
  ]:
@@ -471,6 +512,8 @@ class Client:
471
512
  Args:
472
513
  name (str): The name of the tool to call.
473
514
  arguments (dict[str, Any] | None, optional): Arguments to pass to the tool. Defaults to None.
515
+ timeout (datetime.timedelta | float | int | None, optional): The timeout for the tool call. Defaults to None.
516
+ progress_handler (ProgressHandler | None, optional): The progress handler to use for the tool call. Defaults to None.
474
517
 
475
518
  Returns:
476
519
  list[mcp.types.TextContent | mcp.types.ImageContent | mcp.types.EmbeddedResource]:
@@ -484,6 +527,7 @@ class Client:
484
527
  name=name,
485
528
  arguments=arguments or {},
486
529
  timeout=timeout,
530
+ progress_handler=progress_handler,
487
531
  )
488
532
  if result.isError:
489
533
  msg = cast(mcp.types.TextContent, result.content[0]).text
fastmcp/client/logging.py CHANGED
@@ -6,8 +6,16 @@ from mcp.client.session import (
6
6
  )
7
7
  from mcp.types import LoggingMessageNotificationParams
8
8
 
9
+ from fastmcp.utilities.logging import get_logger
10
+
11
+ logger = get_logger(__name__)
12
+
9
13
  LogMessage: TypeAlias = LoggingMessageNotificationParams
10
14
  LogHandler: TypeAlias = LoggingFnT
11
15
  MessageHandler: TypeAlias = MessageHandlerFnT
12
16
 
13
17
  __all__ = ["LogMessage", "LogHandler", "MessageHandler"]
18
+
19
+
20
+ async def default_log_handler(params: LogMessage) -> None:
21
+ logger.debug(f"Log received: {params}")