fastmcp 2.5.1__py3-none-any.whl → 2.5.2__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
@@ -50,9 +50,7 @@ def _get_npx_command():
50
50
  def _parse_env_var(env_var: str) -> tuple[str, str]:
51
51
  """Parse environment variable string in format KEY=VALUE."""
52
52
  if "=" not in env_var:
53
- logger.error(
54
- f"Invalid environment variable format: {env_var}. Must be KEY=VALUE"
55
- )
53
+ logger.error("Invalid environment variable format. Must be KEY=VALUE")
56
54
  sys.exit(1)
57
55
  key, value = env_var.split("=", 1)
58
56
  return key.strip(), value.strip()
fastmcp/client/client.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import datetime
2
2
  from contextlib import AsyncExitStack, asynccontextmanager
3
3
  from pathlib import Path
4
- from typing import Any, cast
4
+ from typing import Any, Generic, cast, overload
5
5
 
6
6
  import anyio
7
7
  import mcp.types
@@ -9,6 +9,7 @@ from exceptiongroup import catch
9
9
  from mcp import ClientSession
10
10
  from pydantic import AnyUrl
11
11
 
12
+ import fastmcp
12
13
  from fastmcp.client.logging import (
13
14
  LogHandler,
14
15
  MessageHandler,
@@ -27,7 +28,18 @@ from fastmcp.server import FastMCP
27
28
  from fastmcp.utilities.exceptions import get_catch_handlers
28
29
  from fastmcp.utilities.mcp_config import MCPConfig
29
30
 
30
- from .transports import ClientTransport, SessionKwargs, infer_transport
31
+ from .transports import (
32
+ ClientTransportT,
33
+ FastMCP1Server,
34
+ FastMCPTransport,
35
+ MCPConfigTransport,
36
+ NodeStdioTransport,
37
+ PythonStdioTransport,
38
+ SessionKwargs,
39
+ SSETransport,
40
+ StreamableHttpTransport,
41
+ infer_transport,
42
+ )
31
43
 
32
44
  __all__ = [
33
45
  "Client",
@@ -40,13 +52,13 @@ __all__ = [
40
52
  ]
41
53
 
42
54
 
43
- class Client:
55
+ class Client(Generic[ClientTransportT]):
44
56
  """
45
57
  MCP client that delegates connection management to a Transport instance.
46
58
 
47
59
  The Client class is responsible for MCP protocol logic, while the Transport
48
- handles connection establishment and management. Client provides methods
49
- for working with resources, prompts, tools and other MCP capabilities.
60
+ handles connection establishment and management. Client provides methods for
61
+ working with resources, prompts, tools and other MCP capabilities.
50
62
 
51
63
  Args:
52
64
  transport: Connection source specification, which can be:
@@ -62,24 +74,60 @@ class Client:
62
74
  message_handler: Optional handler for protocol messages
63
75
  progress_handler: Optional handler for progress notifications
64
76
  timeout: Optional timeout for requests (seconds or timedelta)
77
+ init_timeout: Optional timeout for initial connection (seconds or timedelta).
78
+ Set to 0 to disable. If None, uses the value in the FastMCP global settings.
65
79
 
66
80
  Examples:
67
- ```python
68
- # Connect to FastMCP server
69
- client = Client("http://localhost:8080")
81
+ ```python # Connect to FastMCP server client =
82
+ Client("http://localhost:8080")
70
83
 
71
84
  async with client:
72
- # List available resources
73
- resources = await client.list_resources()
85
+ # List available resources resources = await client.list_resources()
74
86
 
75
- # Call a tool
76
- result = await client.call_tool("my_tool", {"param": "value"})
87
+ # Call a tool result = await client.call_tool("my_tool", {"param":
88
+ "value"})
77
89
  ```
78
90
  """
79
91
 
92
+ @overload
93
+ def __new__(
94
+ cls,
95
+ transport: ClientTransportT,
96
+ **kwargs: Any,
97
+ ) -> "Client[ClientTransportT]": ...
98
+
99
+ @overload
100
+ def __new__(
101
+ cls, transport: AnyUrl, **kwargs
102
+ ) -> "Client[SSETransport|StreamableHttpTransport]": ...
103
+
104
+ @overload
105
+ def __new__(
106
+ cls, transport: FastMCP | FastMCP1Server, **kwargs
107
+ ) -> "Client[FastMCPTransport]": ...
108
+
109
+ @overload
110
+ def __new__(
111
+ cls, transport: Path, **kwargs
112
+ ) -> "Client[PythonStdioTransport|NodeStdioTransport]": ...
113
+
114
+ @overload
115
+ def __new__(
116
+ cls, transport: MCPConfig | dict[str, Any], **kwargs
117
+ ) -> "Client[MCPConfigTransport]": ...
118
+
119
+ @overload
120
+ def __new__(
121
+ cls, transport: str, **kwargs
122
+ ) -> "Client[PythonStdioTransport|NodeStdioTransport|SSETransport|StreamableHttpTransport]": ...
123
+
124
+ def __new__(cls, transport, **kwargs) -> "Client":
125
+ instance = super().__new__(cls)
126
+ return instance
127
+
80
128
  def __init__(
81
129
  self,
82
- transport: ClientTransport
130
+ transport: ClientTransportT
83
131
  | FastMCP
84
132
  | AnyUrl
85
133
  | Path
@@ -93,8 +141,9 @@ class Client:
93
141
  message_handler: MessageHandler | None = None,
94
142
  progress_handler: ProgressHandler | None = None,
95
143
  timeout: datetime.timedelta | float | int | None = None,
144
+ init_timeout: datetime.timedelta | float | int | None = None,
96
145
  ):
97
- self.transport = infer_transport(transport)
146
+ self.transport = cast(ClientTransportT, infer_transport(transport))
98
147
  self._session: ClientSession | None = None
99
148
  self._exit_stack: AsyncExitStack | None = None
100
149
  self._nesting_counter: int = 0
@@ -111,6 +160,17 @@ class Client:
111
160
  if isinstance(timeout, int | float):
112
161
  timeout = datetime.timedelta(seconds=timeout)
113
162
 
163
+ # handle init handshake timeout
164
+ if init_timeout is None:
165
+ init_timeout = fastmcp.settings.settings.client_init_timeout
166
+ if isinstance(init_timeout, datetime.timedelta):
167
+ init_timeout = init_timeout.total_seconds()
168
+ elif not init_timeout:
169
+ init_timeout = None
170
+ else:
171
+ init_timeout = float(init_timeout)
172
+ self._init_timeout = init_timeout
173
+
114
174
  self._session_kwargs: SessionKwargs = {
115
175
  "sampling_callback": None,
116
176
  "list_roots_callback": None,
@@ -134,6 +194,7 @@ class Client:
134
194
  raise RuntimeError(
135
195
  "Client is not connected. Use the 'async with client:' context manager first."
136
196
  )
197
+
137
198
  return self._session
138
199
 
139
200
  @property
@@ -168,9 +229,11 @@ class Client:
168
229
  self._session = session
169
230
  # Initialize the session
170
231
  try:
171
- with anyio.fail_after(1):
232
+ with anyio.fail_after(self._init_timeout):
172
233
  self._initialize_result = await self._session.initialize()
173
234
  yield
235
+ except anyio.ClosedResourceError:
236
+ raise RuntimeError("Server session was closed unexpectedly")
174
237
  except TimeoutError:
175
238
  raise RuntimeError("Failed to initialize server session")
176
239
  finally:
@@ -203,6 +266,11 @@ class Client:
203
266
  finally:
204
267
  self._exit_stack = None
205
268
 
269
+ async def close(self):
270
+ await self.transport.close()
271
+ self._session = None
272
+ self._initialize_result = None
273
+
206
274
  # --- MCP Client Methods ---
207
275
 
208
276
  async def ping(self) -> bool:
@@ -1,12 +1,14 @@
1
1
  import abc
2
+ import asyncio
2
3
  import contextlib
3
4
  import datetime
4
5
  import os
5
6
  import shutil
6
7
  import sys
8
+ import warnings
7
9
  from collections.abc import AsyncIterator
8
10
  from pathlib import Path
9
- from typing import TYPE_CHECKING, Any, TypedDict, cast
11
+ from typing import TYPE_CHECKING, Any, TypedDict, TypeVar, cast, overload
10
12
 
11
13
  from mcp import ClientSession, StdioServerParameters
12
14
  from mcp.client.session import (
@@ -35,6 +37,9 @@ if TYPE_CHECKING:
35
37
 
36
38
  logger = get_logger(__name__)
37
39
 
40
+ # TypeVar for preserving specific ClientTransport subclass types
41
+ ClientTransportT = TypeVar("ClientTransportT", bound="ClientTransport")
42
+
38
43
 
39
44
  class SessionKwargs(TypedDict, total=False):
40
45
  """Keyword arguments for the MCP ClientSession constructor."""
@@ -83,11 +88,21 @@ class ClientTransport(abc.ABC):
83
88
  # Basic representation for subclasses
84
89
  return f"<{self.__class__.__name__}>"
85
90
 
91
+ async def close(self):
92
+ """Close the transport."""
93
+ pass
94
+
86
95
 
87
96
  class WSTransport(ClientTransport):
88
97
  """Transport implementation that connects to an MCP server via WebSockets."""
89
98
 
90
99
  def __init__(self, url: str | AnyUrl):
100
+ # we never really used this transport, so it can be removed at any time
101
+ warnings.warn(
102
+ "WSTransport is a deprecated MCP transport and will be removed in a future version. Use StreamableHttpTransport instead.",
103
+ DeprecationWarning,
104
+ stacklevel=2,
105
+ )
91
106
  if isinstance(url, AnyUrl):
92
107
  url = str(url)
93
108
  if not isinstance(url, str) or not url.startswith("ws"):
@@ -224,6 +239,7 @@ class StdioTransport(ClientTransport):
224
239
  args: list[str],
225
240
  env: dict[str, str] | None = None,
226
241
  cwd: str | None = None,
242
+ keep_alive: bool | None = None,
227
243
  ):
228
244
  """
229
245
  Initialize a Stdio transport.
@@ -233,25 +249,90 @@ class StdioTransport(ClientTransport):
233
249
  args: The arguments to pass to the command
234
250
  env: Environment variables to set for the subprocess
235
251
  cwd: Current working directory for the subprocess
252
+ keep_alive: Whether to keep the subprocess alive between connections.
253
+ Defaults to True. When True, the subprocess remains active
254
+ after the connection context exits, allowing reuse in
255
+ subsequent connections.
236
256
  """
237
257
  self.command = command
238
258
  self.args = args
239
259
  self.env = env
240
260
  self.cwd = cwd
261
+ if keep_alive is None:
262
+ keep_alive = True
263
+ self.keep_alive = keep_alive
264
+
265
+ self._session: ClientSession | None = None
266
+ self._connect_task: asyncio.Task | None = None
267
+ self._ready_event = asyncio.Event()
268
+ self._stop_event = asyncio.Event()
241
269
 
242
270
  @contextlib.asynccontextmanager
243
271
  async def connect_session(
244
272
  self, **session_kwargs: Unpack[SessionKwargs]
245
273
  ) -> AsyncIterator[ClientSession]:
246
- server_params = StdioServerParameters(
247
- command=self.command, args=self.args, env=self.env, cwd=self.cwd
248
- )
249
- async with stdio_client(server_params) as transport:
250
- read_stream, write_stream = transport
251
- async with ClientSession(
252
- read_stream, write_stream, **session_kwargs
253
- ) as session:
254
- yield session
274
+ try:
275
+ await self.connect(**session_kwargs)
276
+ assert self._session is not None
277
+ yield self._session
278
+ finally:
279
+ if not self.keep_alive:
280
+ await self.disconnect()
281
+ else:
282
+ logger.debug("Stdio transport has keep_alive=True, not disconnecting")
283
+
284
+ async def connect(
285
+ self, **session_kwargs: Unpack[SessionKwargs]
286
+ ) -> ClientSession | None:
287
+ if self._connect_task is not None:
288
+ return
289
+
290
+ async def _connect_task():
291
+ async with contextlib.AsyncExitStack() as stack:
292
+ try:
293
+ server_params = StdioServerParameters(
294
+ command=self.command, args=self.args, env=self.env, cwd=self.cwd
295
+ )
296
+ transport = await stack.enter_async_context(
297
+ stdio_client(server_params)
298
+ )
299
+ read_stream, write_stream = transport
300
+ self._session = await stack.enter_async_context(
301
+ ClientSession(read_stream, write_stream, **session_kwargs)
302
+ )
303
+
304
+ logger.debug("Stdio transport connected")
305
+ self._ready_event.set()
306
+
307
+ # Wait until disconnect is requested (stop_event is set)
308
+ await self._stop_event.wait()
309
+ finally:
310
+ # Clean up client on exit
311
+ self._session = None
312
+ logger.debug("Stdio transport disconnected")
313
+
314
+ # start the connection task
315
+ self._connect_task = asyncio.create_task(_connect_task())
316
+ # wait for the client to be ready before returning
317
+ await self._ready_event.wait()
318
+
319
+ async def disconnect(self):
320
+ if self._connect_task is None:
321
+ return
322
+
323
+ # signal the connection task to stop
324
+ self._stop_event.set()
325
+
326
+ # wait for the connection task to finish cleanly
327
+ await self._connect_task
328
+
329
+ # reset variables and events for potential future reconnects
330
+ self._connect_task = None
331
+ self._stop_event = asyncio.Event()
332
+ self._ready_event = asyncio.Event()
333
+
334
+ async def close(self):
335
+ await self.disconnect()
255
336
 
256
337
  def __repr__(self) -> str:
257
338
  return (
@@ -269,6 +350,7 @@ class PythonStdioTransport(StdioTransport):
269
350
  env: dict[str, str] | None = None,
270
351
  cwd: str | None = None,
271
352
  python_cmd: str = sys.executable,
353
+ keep_alive: bool | None = None,
272
354
  ):
273
355
  """
274
356
  Initialize a Python transport.
@@ -279,6 +361,10 @@ class PythonStdioTransport(StdioTransport):
279
361
  env: Environment variables to set for the subprocess
280
362
  cwd: Current working directory for the subprocess
281
363
  python_cmd: Python command to use (default: "python")
364
+ keep_alive: Whether to keep the subprocess alive between connections.
365
+ Defaults to True. When True, the subprocess remains active
366
+ after the connection context exits, allowing reuse in
367
+ subsequent connections.
282
368
  """
283
369
  script_path = Path(script_path).resolve()
284
370
  if not script_path.is_file():
@@ -290,7 +376,13 @@ class PythonStdioTransport(StdioTransport):
290
376
  if args:
291
377
  full_args.extend(args)
292
378
 
293
- super().__init__(command=python_cmd, args=full_args, env=env, cwd=cwd)
379
+ super().__init__(
380
+ command=python_cmd,
381
+ args=full_args,
382
+ env=env,
383
+ cwd=cwd,
384
+ keep_alive=keep_alive,
385
+ )
294
386
  self.script_path = script_path
295
387
 
296
388
 
@@ -303,6 +395,7 @@ class FastMCPStdioTransport(StdioTransport):
303
395
  args: list[str] | None = None,
304
396
  env: dict[str, str] | None = None,
305
397
  cwd: str | None = None,
398
+ keep_alive: bool | None = None,
306
399
  ):
307
400
  script_path = Path(script_path).resolve()
308
401
  if not script_path.is_file():
@@ -311,7 +404,11 @@ class FastMCPStdioTransport(StdioTransport):
311
404
  raise ValueError(f"Not a Python script: {script_path}")
312
405
 
313
406
  super().__init__(
314
- command="fastmcp", args=["run", str(script_path)], env=env, cwd=cwd
407
+ command="fastmcp",
408
+ args=["run", str(script_path)],
409
+ env=env,
410
+ cwd=cwd,
411
+ keep_alive=keep_alive,
315
412
  )
316
413
  self.script_path = script_path
317
414
 
@@ -326,6 +423,7 @@ class NodeStdioTransport(StdioTransport):
326
423
  env: dict[str, str] | None = None,
327
424
  cwd: str | None = None,
328
425
  node_cmd: str = "node",
426
+ keep_alive: bool | None = None,
329
427
  ):
330
428
  """
331
429
  Initialize a Node transport.
@@ -336,6 +434,10 @@ class NodeStdioTransport(StdioTransport):
336
434
  env: Environment variables to set for the subprocess
337
435
  cwd: Current working directory for the subprocess
338
436
  node_cmd: Node.js command to use (default: "node")
437
+ keep_alive: Whether to keep the subprocess alive between connections.
438
+ Defaults to True. When True, the subprocess remains active
439
+ after the connection context exits, allowing reuse in
440
+ subsequent connections.
339
441
  """
340
442
  script_path = Path(script_path).resolve()
341
443
  if not script_path.is_file():
@@ -347,7 +449,9 @@ class NodeStdioTransport(StdioTransport):
347
449
  if args:
348
450
  full_args.extend(args)
349
451
 
350
- super().__init__(command=node_cmd, args=full_args, env=env, cwd=cwd)
452
+ super().__init__(
453
+ command=node_cmd, args=full_args, env=env, cwd=cwd, keep_alive=keep_alive
454
+ )
351
455
  self.script_path = script_path
352
456
 
353
457
 
@@ -363,6 +467,7 @@ class UvxStdioTransport(StdioTransport):
363
467
  with_packages: list[str] | None = None,
364
468
  from_package: str | None = None,
365
469
  env_vars: dict[str, str] | None = None,
470
+ keep_alive: bool | None = None,
366
471
  ):
367
472
  """
368
473
  Initialize a Uvx transport.
@@ -375,6 +480,10 @@ class UvxStdioTransport(StdioTransport):
375
480
  with_packages: Additional packages to include
376
481
  from_package: Package to install the tool from
377
482
  env_vars: Additional environment variables
483
+ keep_alive: Whether to keep the subprocess alive between connections.
484
+ Defaults to True. When True, the subprocess remains active
485
+ after the connection context exits, allowing reuse in
486
+ subsequent connections.
378
487
  """
379
488
  # Basic validation
380
489
  if project_directory and not Path(project_directory).exists():
@@ -402,7 +511,13 @@ class UvxStdioTransport(StdioTransport):
402
511
  env = os.environ.copy()
403
512
  env.update(env_vars)
404
513
 
405
- super().__init__(command="uvx", args=uvx_args, env=env, cwd=project_directory)
514
+ super().__init__(
515
+ command="uvx",
516
+ args=uvx_args,
517
+ env=env,
518
+ cwd=project_directory,
519
+ keep_alive=keep_alive,
520
+ )
406
521
  self.tool_name = tool_name
407
522
 
408
523
 
@@ -416,6 +531,7 @@ class NpxStdioTransport(StdioTransport):
416
531
  project_directory: str | None = None,
417
532
  env_vars: dict[str, str] | None = None,
418
533
  use_package_lock: bool = True,
534
+ keep_alive: bool | None = None,
419
535
  ):
420
536
  """
421
537
  Initialize an Npx transport.
@@ -426,6 +542,10 @@ class NpxStdioTransport(StdioTransport):
426
542
  project_directory: Project directory with package.json
427
543
  env_vars: Additional environment variables
428
544
  use_package_lock: Whether to use package-lock.json (--prefer-offline)
545
+ keep_alive: Whether to keep the subprocess alive between connections.
546
+ Defaults to True. When True, the subprocess remains active
547
+ after the connection context exits, allowing reuse in
548
+ subsequent connections.
429
549
  """
430
550
  # verify npx is installed
431
551
  if shutil.which("npx") is None:
@@ -453,7 +573,13 @@ class NpxStdioTransport(StdioTransport):
453
573
  env = os.environ.copy()
454
574
  env.update(env_vars)
455
575
 
456
- super().__init__(command="npx", args=npx_args, env=env, cwd=project_directory)
576
+ super().__init__(
577
+ command="npx",
578
+ args=npx_args,
579
+ env=env,
580
+ cwd=project_directory,
581
+ keep_alive=keep_alive,
582
+ )
457
583
  self.package = package
458
584
 
459
585
 
@@ -575,6 +701,44 @@ class MCPConfigTransport(ClientTransport):
575
701
  return f"<MCPConfig(config='{self.config}')>"
576
702
 
577
703
 
704
+ @overload
705
+ def infer_transport(transport: ClientTransportT) -> ClientTransportT: ...
706
+
707
+
708
+ @overload
709
+ def infer_transport(transport: FastMCPServer) -> FastMCPTransport: ...
710
+
711
+
712
+ @overload
713
+ def infer_transport(transport: FastMCP1Server) -> FastMCPTransport: ...
714
+
715
+
716
+ @overload
717
+ def infer_transport(transport: MCPConfig) -> MCPConfigTransport: ...
718
+
719
+
720
+ @overload
721
+ def infer_transport(transport: dict[str, Any]) -> MCPConfigTransport: ...
722
+
723
+
724
+ @overload
725
+ def infer_transport(
726
+ transport: AnyUrl,
727
+ ) -> SSETransport | StreamableHttpTransport: ...
728
+
729
+
730
+ @overload
731
+ def infer_transport(
732
+ transport: str,
733
+ ) -> (
734
+ PythonStdioTransport | NodeStdioTransport | SSETransport | StreamableHttpTransport
735
+ ): ...
736
+
737
+
738
+ @overload
739
+ def infer_transport(transport: Path) -> PythonStdioTransport | NodeStdioTransport: ...
740
+
741
+
578
742
  def infer_transport(
579
743
  transport: ClientTransport
580
744
  | FastMCPServer
@@ -148,7 +148,7 @@ class ResourceTemplate(BaseModel):
148
148
  f"URI parameters {uri_params} must be a subset of the function arguments: {func_params}"
149
149
  )
150
150
 
151
- description = description or fn.__doc__ or ""
151
+ description = description or fn.__doc__
152
152
 
153
153
  if not inspect.isroutine(fn):
154
154
  fn = fn.__call__
@@ -48,12 +48,23 @@ def get_http_headers(include_all: bool = False) -> dict[str, str]:
48
48
  if include_all:
49
49
  exclude_headers = set()
50
50
  else:
51
- exclude_headers = {"content-length"}
52
-
53
- # ensure all lowercase!
54
- # (just in case)
55
- exclude_headers = {h.lower() for h in exclude_headers}
56
-
51
+ exclude_headers = {
52
+ "host",
53
+ "content-length",
54
+ "connection",
55
+ "transfer-encoding",
56
+ "upgrade",
57
+ "te",
58
+ "keep-alive",
59
+ "expect",
60
+ # Proxy-related headers
61
+ "proxy-authenticate",
62
+ "proxy-authorization",
63
+ "proxy-connection",
64
+ }
65
+ # (just in case)
66
+ if not all(h.lower() == h for h in exclude_headers):
67
+ raise ValueError("Excluded headers must be lowercase")
57
68
  headers = {}
58
69
 
59
70
  try:
fastmcp/server/server.py CHANGED
@@ -62,7 +62,7 @@ from fastmcp.utilities.mcp_config import MCPConfig
62
62
 
63
63
  if TYPE_CHECKING:
64
64
  from fastmcp.client import Client
65
- from fastmcp.client.transports import ClientTransport
65
+ from fastmcp.client.transports import ClientTransport, ClientTransportT
66
66
  from fastmcp.server.openapi import ComponentFn as OpenAPIComponentFn
67
67
  from fastmcp.server.openapi import FastMCPOpenAPI, RouteMap
68
68
  from fastmcp.server.openapi import RouteMapFn as OpenAPIRouteMapFn
@@ -257,9 +257,15 @@ class FastMCP(Generic[LifespanResultT]):
257
257
  """Get all registered tools, indexed by registered key."""
258
258
  if (tools := self._cache.get("tools")) is self._cache.NOT_FOUND:
259
259
  tools: dict[str, Tool] = {}
260
- for server in self._mounted_servers.values():
261
- server_tools = await server.get_tools()
262
- tools.update(server_tools)
260
+ for prefix, server in self._mounted_servers.items():
261
+ try:
262
+ server_tools = await server.get_tools()
263
+ tools.update(server_tools)
264
+ except Exception as e:
265
+ logger.warning(
266
+ f"Failed to get tools from mounted server '{prefix}': {e}"
267
+ )
268
+ continue
263
269
  tools.update(self._tool_manager.get_tools())
264
270
  self._cache.set("tools", tools)
265
271
  return tools
@@ -268,9 +274,15 @@ class FastMCP(Generic[LifespanResultT]):
268
274
  """Get all registered resources, indexed by registered key."""
269
275
  if (resources := self._cache.get("resources")) is self._cache.NOT_FOUND:
270
276
  resources: dict[str, Resource] = {}
271
- for server in self._mounted_servers.values():
272
- server_resources = await server.get_resources()
273
- resources.update(server_resources)
277
+ for prefix, server in self._mounted_servers.items():
278
+ try:
279
+ server_resources = await server.get_resources()
280
+ resources.update(server_resources)
281
+ except Exception as e:
282
+ logger.warning(
283
+ f"Failed to get resources from mounted server '{prefix}': {e}"
284
+ )
285
+ continue
274
286
  resources.update(self._resource_manager.get_resources())
275
287
  self._cache.set("resources", resources)
276
288
  return resources
@@ -281,9 +293,16 @@ class FastMCP(Generic[LifespanResultT]):
281
293
  templates := self._cache.get("resource_templates")
282
294
  ) is self._cache.NOT_FOUND:
283
295
  templates: dict[str, ResourceTemplate] = {}
284
- for server in self._mounted_servers.values():
285
- server_templates = await server.get_resource_templates()
286
- templates.update(server_templates)
296
+ for prefix, server in self._mounted_servers.items():
297
+ try:
298
+ server_templates = await server.get_resource_templates()
299
+ templates.update(server_templates)
300
+ except Exception as e:
301
+ logger.warning(
302
+ "Failed to get resource templates from mounted server "
303
+ f"'{prefix}': {e}"
304
+ )
305
+ continue
287
306
  templates.update(self._resource_manager.get_templates())
288
307
  self._cache.set("resource_templates", templates)
289
308
  return templates
@@ -294,9 +313,15 @@ class FastMCP(Generic[LifespanResultT]):
294
313
  """
295
314
  if (prompts := self._cache.get("prompts")) is self._cache.NOT_FOUND:
296
315
  prompts: dict[str, Prompt] = {}
297
- for server in self._mounted_servers.values():
298
- server_prompts = await server.get_prompts()
299
- prompts.update(server_prompts)
316
+ for prefix, server in self._mounted_servers.items():
317
+ try:
318
+ server_prompts = await server.get_prompts()
319
+ prompts.update(server_prompts)
320
+ except Exception as e:
321
+ logger.warning(
322
+ f"Failed to get prompts from mounted server '{prefix}': {e}"
323
+ )
324
+ continue
300
325
  prompts.update(self._prompt_manager.get_prompts())
301
326
  self._cache.set("prompts", prompts)
302
327
  return prompts
@@ -802,7 +827,7 @@ class FastMCP(Generic[LifespanResultT]):
802
827
  """
803
828
  host = host or self.settings.host
804
829
  port = port or self.settings.port
805
- default_log_level_to_use = log_level or self.settings.log_level.lower()
830
+ default_log_level_to_use = (log_level or self.settings.log_level).lower()
806
831
 
807
832
  app = self.http_app(path=path, transport=transport, middleware=middleware)
808
833
 
@@ -1263,7 +1288,7 @@ class FastMCP(Generic[LifespanResultT]):
1263
1288
  @classmethod
1264
1289
  def as_proxy(
1265
1290
  cls,
1266
- backend: Client
1291
+ backend: Client[ClientTransportT]
1267
1292
  | ClientTransport
1268
1293
  | FastMCP[Any]
1269
1294
  | AnyUrl
@@ -1291,7 +1316,9 @@ class FastMCP(Generic[LifespanResultT]):
1291
1316
  return FastMCPProxy(client=client, **settings)
1292
1317
 
1293
1318
  @classmethod
1294
- def from_client(cls, client: Client, **settings: Any) -> FastMCPProxy:
1319
+ def from_client(
1320
+ cls, client: Client[ClientTransportT], **settings: Any
1321
+ ) -> FastMCPProxy:
1295
1322
  """
1296
1323
  Create a FastMCP proxy server from a FastMCP client.
1297
1324
  """
fastmcp/settings.py CHANGED
@@ -87,6 +87,13 @@ class Settings(BaseSettings):
87
87
  ),
88
88
  ] = False
89
89
 
90
+ client_init_timeout: Annotated[
91
+ float | None,
92
+ Field(
93
+ description="The timeout for the client's initialization handshake, in seconds. Set to None or 0 to disable.",
94
+ ),
95
+ ] = None
96
+
90
97
  @model_validator(mode="after")
91
98
  def setup_logging(self) -> Self:
92
99
  """Finalize the settings."""
fastmcp/tools/tool.py CHANGED
@@ -36,7 +36,9 @@ class Tool(BaseModel):
36
36
 
37
37
  fn: Callable[..., Any]
38
38
  name: str = Field(description="Name of the tool")
39
- description: str = Field(description="Description of what the tool does")
39
+ description: str | None = Field(
40
+ default=None, description="Description of what the tool does"
41
+ )
40
42
  parameters: dict[str, Any] = Field(description="JSON schema for tool parameters")
41
43
  tags: Annotated[set[str], BeforeValidator(_convert_set_defaults)] = Field(
42
44
  default_factory=set, description="Tags for the tool"
@@ -74,7 +76,7 @@ class Tool(BaseModel):
74
76
  if func_name == "<lambda>":
75
77
  raise ValueError("You must provide a name for lambda functions")
76
78
 
77
- func_doc = description or fn.__doc__ or ""
79
+ func_doc = description or fn.__doc__
78
80
 
79
81
  # if the fn is a callable class, we need to get the __call__ method from here out
80
82
  if not inspect.isroutine(fn):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastmcp
3
- Version: 2.5.1
3
+ Version: 2.5.2
4
4
  Summary: The fast, Pythonic way to build MCP servers.
5
5
  Project-URL: Homepage, https://gofastmcp.com
6
6
  Project-URL: Repository, https://github.com/jlowin/fastmcp
@@ -1,19 +1,19 @@
1
1
  fastmcp/__init__.py,sha256=yTAqLZORsPqbr7AE0ayw6zIYBeMlxQlI-3HE2WqbvHk,435
2
2
  fastmcp/exceptions.py,sha256=YvaKqOT3w0boXF9ylIoaSIzW9XiQ1qLFG1LZq6B60H8,680
3
3
  fastmcp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- fastmcp/settings.py,sha256=ES59HUoZGLCtBiCKjf4ioVXjPSZtKLJrXhBN4OH_1N4,5356
4
+ fastmcp/settings.py,sha256=aVOLK-QfhGr_0mPLVzBmeUxyS9_w8gSHAjMmRqEoEow,5577
5
5
  fastmcp/cli/__init__.py,sha256=Ii284TNoG5lxTP40ETMGhHEq3lQZWxu9m9JuU57kUpQ,87
6
6
  fastmcp/cli/claude.py,sha256=IAlcZ4qZKBBj09jZUMEx7EANZE_IR3vcu7zOBJmMOuU,4567
7
- fastmcp/cli/cli.py,sha256=eRZ4tpne7dj_rhjREwiNRN5i9A1T8-ptxg1lYaHfS5o,12401
7
+ fastmcp/cli/cli.py,sha256=CQxpRTXgnQQynGJLEV5g1FnLMaiWoiUgefnMZ7VxS4o,12367
8
8
  fastmcp/cli/run.py,sha256=o7Ge6JZKXYwlY2vYdMNoVX8agBchAaeU_73iPndojIM,5351
9
9
  fastmcp/client/__init__.py,sha256=Ri8GFHolIKOZnXaMzIc3VpkLcEqAmOoYGCKgmSk6NnE,550
10
10
  fastmcp/client/base.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
- fastmcp/client/client.py,sha256=QJsb8PHheUTn4UPG9kxzgn8M10g0cUkFttnGj-OgPdk,20847
11
+ fastmcp/client/client.py,sha256=JxFC_YUOrDPZwiDbD0JgCHQsOy1F8Rup7hnege96OIc,23021
12
12
  fastmcp/client/logging.py,sha256=hOPRailZUp89RUck6V4HPaWVZinVrNY8HD4hD0dd-fE,822
13
13
  fastmcp/client/progress.py,sha256=WjLLDbUKMsx8DK-fqO7AGsXb83ak-6BMrLvzzznGmcI,1043
14
14
  fastmcp/client/roots.py,sha256=IxI_bHwHTmg6c2H-s1av1ZgrRnNDieHtYwdGFbzXT5c,2471
15
15
  fastmcp/client/sampling.py,sha256=UlDHxnd6k_HoU8RA3ob0g8-e6haJBc9u27N_v291QoI,1698
16
- fastmcp/client/transports.py,sha256=Ooh1YCYcdy61Qa4Ugl2wEWTc-uy05FBApoXofWdVpk4,24376
16
+ fastmcp/client/transports.py,sha256=G1MQ7bHkmQbbni4ZWwMJs-opbPdqpyKYdg7TYkkjLbU,29986
17
17
  fastmcp/contrib/README.md,sha256=rKknYSI1T192UvSszqwwDlQ2eYQpxywrNTLoj177SYU,878
18
18
  fastmcp/contrib/bulk_tool_caller/README.md,sha256=5aUUY1TSFKtz1pvTLSDqkUCkGkuqMfMZNsLeaNqEgAc,1960
19
19
  fastmcp/contrib/bulk_tool_caller/__init__.py,sha256=xvGSSaUXTQrc31erBoi1Gh7BikgOliETDiYVTP3rLxY,75
@@ -31,17 +31,17 @@ fastmcp/prompts/prompt_manager.py,sha256=qptEhZHMwc8XxQd5lTQg8iIb5MiTZVsNaux_XLv
31
31
  fastmcp/resources/__init__.py,sha256=t0x1j8lc74rjUKtXe9H5Gs4fpQt82K4NgBK6Y7A0xTg,467
32
32
  fastmcp/resources/resource.py,sha256=Rx1My_fi1f-oqnQ9R_v7ejopAk4BJDfbB75-s4d31dM,2492
33
33
  fastmcp/resources/resource_manager.py,sha256=nsgCR3lo9t4Q0QR6txPfAas2upqIb8P8ZlqWAfV9Qc0,11344
34
- fastmcp/resources/template.py,sha256=mdejT0ofACYrn32Jw3wdJ7bJcVbW_4VMQEwMZLDR3zM,7529
34
+ fastmcp/resources/template.py,sha256=u0_-yNMmZfnl5DqtSRndGbGBrm7JgbzBU8IUd0hrEWE,7523
35
35
  fastmcp/resources/types.py,sha256=5fUFvzRlekNjtfihtq8S-fT0alKoNfclzrugqeM5JRE,6366
36
36
  fastmcp/server/__init__.py,sha256=bMD4aQD4yJqLz7-mudoNsyeV8UgQfRAg3PRwPvwTEds,119
37
37
  fastmcp/server/context.py,sha256=yN1e0LsnCl7cEpr9WlbvFhSf8oE56kKb-20m8h2SsBY,10171
38
- fastmcp/server/dependencies.py,sha256=A1A2dKAyZ2GcAA2RQ6KA5SaHuLU3LSbZaGkzncgcX2E,1722
38
+ fastmcp/server/dependencies.py,sha256=4kdJLvWn-lMU7uPIJ-Np1RHBwvkbU7Dc31ZdsGTA9_I,2093
39
39
  fastmcp/server/http.py,sha256=wZWUrLvKITlvkxQoggJ9RyvynCUMEJqqMMsvX7Hmb9o,12807
40
40
  fastmcp/server/openapi.py,sha256=9qXSuEl671sT1F7nSM3SiD5KANGqHUhiL1BBdCnuCcU,39153
41
41
  fastmcp/server/proxy.py,sha256=mt3eM6TQWfnZD5XehmTXisskZ4CBbsWyjRPjprlTjBY,9653
42
- fastmcp/server/server.py,sha256=C0VEnHuSoYt-qef1jm4REPJkcM2stQ2Zp_rT_sELr2Y,57141
42
+ fastmcp/server/server.py,sha256=TGC8ysEtA-fpuFzaqfvCstfWYJks5ClrCQl6zyYfLZM,58237
43
43
  fastmcp/tools/__init__.py,sha256=ocw-SFTtN6vQ8fgnlF8iNAOflRmh79xS1xdO0Bc3QPE,96
44
- fastmcp/tools/tool.py,sha256=id2DCxPOtgFa-CRZvCBmCOz16nWyJXcq2ubIjmtOxPg,7803
44
+ fastmcp/tools/tool.py,sha256=Qx1sQ-D_llZIETPea8KoRn_vOjYgyriDqi0hpd_pRP8,7832
45
45
  fastmcp/tools/tool_manager.py,sha256=785vKYlJ9B2B5ThXFhuXYB4VNY4h0283-_AAdy1hEfk,4430
46
46
  fastmcp/utilities/__init__.py,sha256=-imJ8S-rXmbXMWeDamldP-dHDqAPg_wwmPVz-LNX14E,31
47
47
  fastmcp/utilities/cache.py,sha256=aV3oZ-ZhMgLSM9iAotlUlEy5jFvGXrVo0Y5Bj4PBtqY,707
@@ -53,8 +53,8 @@ fastmcp/utilities/mcp_config.py,sha256=_wY3peaFDEgyOBkJ_Tb8sETk3mtdwtw1053q7ry0z
53
53
  fastmcp/utilities/openapi.py,sha256=QQos4vP59HQ8vPDTKftWOIVv_zmW30mNxYSXVU7JUbY,38441
54
54
  fastmcp/utilities/tests.py,sha256=teyHcl3j7WGfYJ6m42VuQYB_IVpGvPdFqIpC-UxsN78,3369
55
55
  fastmcp/utilities/types.py,sha256=6CcqAQ1QqCO2HGSFlPS6FO5JRWnacjCcO2-EhyEnZV0,4400
56
- fastmcp-2.5.1.dist-info/METADATA,sha256=_gulwPGdZ2oVtGOs186YsSYcJw0KChjfP1jngkHnbWk,16510
57
- fastmcp-2.5.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
58
- fastmcp-2.5.1.dist-info/entry_points.txt,sha256=ff8bMtKX1JvXyurMibAacMSKbJEPmac9ffAKU9mLnM8,44
59
- fastmcp-2.5.1.dist-info/licenses/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
60
- fastmcp-2.5.1.dist-info/RECORD,,
56
+ fastmcp-2.5.2.dist-info/METADATA,sha256=EU1bb1c0HO8jLeLAlTo-UCPA619yiw1kiAiFNA8bMMk,16510
57
+ fastmcp-2.5.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
58
+ fastmcp-2.5.2.dist-info/entry_points.txt,sha256=ff8bMtKX1JvXyurMibAacMSKbJEPmac9ffAKU9mLnM8,44
59
+ fastmcp-2.5.2.dist-info/licenses/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
60
+ fastmcp-2.5.2.dist-info/RECORD,,