fastmcp 2.3.4__py3-none-any.whl → 2.4.0__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/client/client.py CHANGED
@@ -1,14 +1,21 @@
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
 
6
+ import anyio
6
7
  import mcp.types
7
8
  from exceptiongroup import catch
8
9
  from mcp import ClientSession
9
10
  from pydantic import AnyUrl
10
11
 
11
- from fastmcp.client.logging import LogHandler, MessageHandler
12
+ from fastmcp.client.logging import (
13
+ LogHandler,
14
+ MessageHandler,
15
+ create_log_callback,
16
+ default_log_handler,
17
+ )
18
+ from fastmcp.client.progress import ProgressHandler, default_progress_handler
12
19
  from fastmcp.client.roots import (
13
20
  RootsHandler,
14
21
  RootsList,
@@ -18,6 +25,7 @@ from fastmcp.client.sampling import SamplingHandler, create_sampling_callback
18
25
  from fastmcp.exceptions import ToolError
19
26
  from fastmcp.server import FastMCP
20
27
  from fastmcp.utilities.exceptions import get_catch_handlers
28
+ from fastmcp.utilities.mcp_config import MCPConfig
21
29
 
22
30
  from .transports import ClientTransport, SessionKwargs, infer_transport
23
31
 
@@ -28,6 +36,7 @@ __all__ = [
28
36
  "LogHandler",
29
37
  "MessageHandler",
30
38
  "SamplingHandler",
39
+ "ProgressHandler",
31
40
  ]
32
41
 
33
42
 
@@ -45,11 +54,13 @@ class Client:
45
54
  - FastMCP: In-process FastMCP server
46
55
  - AnyUrl | str: URL to connect to
47
56
  - Path: File path for local socket
57
+ - MCPConfig: MCP server configuration
48
58
  - dict: Transport configuration
49
59
  roots: Optional RootsList or RootsHandler for filesystem access
50
60
  sampling_handler: Optional handler for sampling requests
51
61
  log_handler: Optional handler for log messages
52
62
  message_handler: Optional handler for protocol messages
63
+ progress_handler: Optional handler for progress notifications
53
64
  timeout: Optional timeout for requests (seconds or timedelta)
54
65
 
55
66
  Examples:
@@ -68,18 +79,34 @@ class Client:
68
79
 
69
80
  def __init__(
70
81
  self,
71
- transport: ClientTransport | FastMCP | AnyUrl | Path | dict[str, Any] | str,
82
+ transport: ClientTransport
83
+ | FastMCP
84
+ | AnyUrl
85
+ | Path
86
+ | MCPConfig
87
+ | dict[str, Any]
88
+ | str,
72
89
  # Common args
73
90
  roots: RootsList | RootsHandler | None = None,
74
91
  sampling_handler: SamplingHandler | None = None,
75
92
  log_handler: LogHandler | None = None,
76
93
  message_handler: MessageHandler | None = None,
94
+ progress_handler: ProgressHandler | None = None,
77
95
  timeout: datetime.timedelta | float | int | None = None,
78
96
  ):
79
97
  self.transport = infer_transport(transport)
80
98
  self._session: ClientSession | None = None
81
99
  self._exit_stack: AsyncExitStack | None = None
82
100
  self._nesting_counter: int = 0
101
+ self._initialize_result: mcp.types.InitializeResult | None = None
102
+
103
+ if log_handler is None:
104
+ log_handler = default_log_handler
105
+
106
+ if progress_handler is None:
107
+ progress_handler = default_progress_handler
108
+
109
+ self._progress_handler = progress_handler
83
110
 
84
111
  if isinstance(timeout, int | float):
85
112
  timeout = datetime.timedelta(seconds=timeout)
@@ -87,7 +114,7 @@ class Client:
87
114
  self._session_kwargs: SessionKwargs = {
88
115
  "sampling_callback": None,
89
116
  "list_roots_callback": None,
90
- "logging_callback": log_handler,
117
+ "logging_callback": create_log_callback(log_handler),
91
118
  "message_handler": message_handler,
92
119
  "read_timeout_seconds": timeout,
93
120
  }
@@ -96,17 +123,28 @@ class Client:
96
123
  self.set_roots(roots)
97
124
 
98
125
  if sampling_handler is not None:
99
- self.set_sampling_callback(sampling_handler)
126
+ self._session_kwargs["sampling_callback"] = create_sampling_callback(
127
+ sampling_handler
128
+ )
100
129
 
101
130
  @property
102
131
  def session(self) -> ClientSession:
103
132
  """Get the current active session. Raises RuntimeError if not connected."""
104
133
  if self._session is None:
105
134
  raise RuntimeError(
106
- "Client is not connected. Use 'async with client:' context manager first."
135
+ "Client is not connected. Use the 'async with client:' context manager first."
107
136
  )
108
137
  return self._session
109
138
 
139
+ @property
140
+ def initialize_result(self) -> mcp.types.InitializeResult:
141
+ """Get the result of the initialization request."""
142
+ if self._initialize_result is None:
143
+ raise RuntimeError(
144
+ "Client is not connected. Use the 'async with client:' context manager first."
145
+ )
146
+ return self._initialize_result
147
+
110
148
  def set_roots(self, roots: RootsList | RootsHandler) -> None:
111
149
  """Set the roots for the client. This does not automatically call `send_roots_list_changed`."""
112
150
  self._session_kwargs["list_roots_callback"] = create_roots_callback(roots)
@@ -121,27 +159,37 @@ class Client:
121
159
  """Check if the client is currently connected."""
122
160
  return self._session is not None
123
161
 
162
+ @asynccontextmanager
163
+ async def _context_manager(self):
164
+ with catch(get_catch_handlers()):
165
+ async with self.transport.connect_session(
166
+ **self._session_kwargs
167
+ ) as session:
168
+ self._session = session
169
+ # Initialize the session
170
+ try:
171
+ with anyio.fail_after(1):
172
+ self._initialize_result = await self._session.initialize()
173
+ yield
174
+ except TimeoutError:
175
+ raise RuntimeError("Failed to initialize server session")
176
+ finally:
177
+ self._exit_stack = None
178
+ self._session = None
179
+ self._initialize_result = None
180
+
124
181
  async def __aenter__(self):
125
182
  if self._nesting_counter == 0:
126
183
  # Create exit stack to manage both context managers
127
184
  stack = AsyncExitStack()
128
185
  await stack.__aenter__()
129
186
 
130
- # Add the exception handling context
131
- stack.enter_context(catch(get_catch_handlers()))
187
+ await stack.enter_async_context(self._context_manager())
132
188
 
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
189
  self._exit_stack = stack
143
190
 
144
191
  self._nesting_counter += 1
192
+
145
193
  return self
146
194
 
147
195
  async def __aexit__(self, exc_type, exc_val, exc_tb):
@@ -154,7 +202,6 @@ class Client:
154
202
  await self._exit_stack.__aexit__(exc_type, exc_val, exc_tb)
155
203
  finally:
156
204
  self._exit_stack = None
157
- self._session = None
158
205
 
159
206
  # --- MCP Client Methods ---
160
207
 
@@ -168,9 +215,12 @@ class Client:
168
215
  progress_token: str | int,
169
216
  progress: float,
170
217
  total: float | None = None,
218
+ message: str | None = None,
171
219
  ) -> None:
172
220
  """Send a progress notification."""
173
- await self.session.send_progress_notification(progress_token, progress, total)
221
+ await self.session.send_progress_notification(
222
+ progress_token, progress, total, message
223
+ )
174
224
 
175
225
  async def set_logging_level(self, level: mcp.types.LoggingLevel) -> None:
176
226
  """Send a logging/setLevel request."""
@@ -430,6 +480,7 @@ class Client:
430
480
  self,
431
481
  name: str,
432
482
  arguments: dict[str, Any],
483
+ progress_handler: ProgressHandler | None = None,
433
484
  timeout: datetime.timedelta | float | int | None = None,
434
485
  ) -> mcp.types.CallToolResult:
435
486
  """Send a tools/call request and return the complete MCP protocol result.
@@ -441,6 +492,8 @@ class Client:
441
492
  name (str): The name of the tool to call.
442
493
  arguments (dict[str, Any]): Arguments to pass to the tool.
443
494
  timeout (datetime.timedelta | float | int | None, optional): The timeout for the tool call. Defaults to None.
495
+ progress_handler (ProgressHandler | None, optional): The progress handler to use for the tool call. Defaults to None.
496
+
444
497
  Returns:
445
498
  mcp.types.CallToolResult: The complete response object from the protocol,
446
499
  containing the tool result and any additional metadata.
@@ -452,7 +505,10 @@ class Client:
452
505
  if isinstance(timeout, int | float):
453
506
  timeout = datetime.timedelta(seconds=timeout)
454
507
  result = await self.session.call_tool(
455
- name=name, arguments=arguments, read_timeout_seconds=timeout
508
+ name=name,
509
+ arguments=arguments,
510
+ read_timeout_seconds=timeout,
511
+ progress_callback=progress_handler or self._progress_handler,
456
512
  )
457
513
  return result
458
514
 
@@ -461,6 +517,7 @@ class Client:
461
517
  name: str,
462
518
  arguments: dict[str, Any] | None = None,
463
519
  timeout: datetime.timedelta | float | int | None = None,
520
+ progress_handler: ProgressHandler | None = None,
464
521
  ) -> list[
465
522
  mcp.types.TextContent | mcp.types.ImageContent | mcp.types.EmbeddedResource
466
523
  ]:
@@ -471,6 +528,8 @@ class Client:
471
528
  Args:
472
529
  name (str): The name of the tool to call.
473
530
  arguments (dict[str, Any] | None, optional): Arguments to pass to the tool. Defaults to None.
531
+ timeout (datetime.timedelta | float | int | None, optional): The timeout for the tool call. Defaults to None.
532
+ progress_handler (ProgressHandler | None, optional): The progress handler to use for the tool call. Defaults to None.
474
533
 
475
534
  Returns:
476
535
  list[mcp.types.TextContent | mcp.types.ImageContent | mcp.types.EmbeddedResource]:
@@ -484,6 +543,7 @@ class Client:
484
543
  name=name,
485
544
  arguments=arguments or {},
486
545
  timeout=timeout,
546
+ progress_handler=progress_handler,
487
547
  )
488
548
  if result.isError:
489
549
  msg = cast(mcp.types.TextContent, result.content[0]).text
fastmcp/client/logging.py CHANGED
@@ -1,13 +1,27 @@
1
+ from collections.abc import Awaitable, Callable
1
2
  from typing import TypeAlias
2
3
 
3
- from mcp.client.session import (
4
- LoggingFnT,
5
- MessageHandlerFnT,
6
- )
4
+ from mcp.client.session import LoggingFnT, MessageHandlerFnT
7
5
  from mcp.types import LoggingMessageNotificationParams
8
6
 
7
+ from fastmcp.utilities.logging import get_logger
8
+
9
+ logger = get_logger(__name__)
10
+
9
11
  LogMessage: TypeAlias = LoggingMessageNotificationParams
10
- LogHandler: TypeAlias = LoggingFnT
12
+ LogHandler: TypeAlias = Callable[[LogMessage], Awaitable[None]]
11
13
  MessageHandler: TypeAlias = MessageHandlerFnT
12
14
 
13
- __all__ = ["LogMessage", "LogHandler", "MessageHandler"]
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
@@ -0,0 +1,38 @@
1
+ from typing import TypeAlias
2
+
3
+ from mcp.shared.session import ProgressFnT
4
+
5
+ from fastmcp.utilities.logging import get_logger
6
+
7
+ logger = get_logger(__name__)
8
+
9
+ ProgressHandler: TypeAlias = ProgressFnT
10
+
11
+
12
+ async def default_progress_handler(
13
+ progress: float, total: float | None, message: str | None
14
+ ) -> None:
15
+ """Default handler for progress notifications.
16
+
17
+ Logs progress updates at debug level, properly handling missing total or message values.
18
+
19
+ Args:
20
+ progress: Current progress value
21
+ total: Optional total expected value
22
+ message: Optional status message
23
+ """
24
+ if total is not None:
25
+ # We have both progress and total
26
+ percent = (progress / total) * 100
27
+ progress_str = f"{progress}/{total} ({percent:.1f}%)"
28
+ else:
29
+ # We only have progress
30
+ progress_str = f"{progress}"
31
+
32
+ # Include message if available
33
+ if message:
34
+ log_msg = f"Progress: {progress_str} - {message}"
35
+ else:
36
+ log_msg = f"Progress: {progress_str}"
37
+
38
+ logger.debug(log_msg)
@@ -1,14 +1,12 @@
1
1
  import abc
2
2
  import contextlib
3
3
  import datetime
4
- import inspect
5
4
  import os
6
5
  import shutil
7
6
  import sys
8
- import warnings
9
7
  from collections.abc import AsyncIterator
10
8
  from pathlib import Path
11
- from typing import Any, TypedDict, cast
9
+ from typing import TYPE_CHECKING, Any, TypedDict, cast
12
10
 
13
11
  from mcp import ClientSession, StdioServerParameters
14
12
  from mcp.client.session import (
@@ -26,6 +24,14 @@ from pydantic import AnyUrl
26
24
  from typing_extensions import Unpack
27
25
 
28
26
  from fastmcp.server import FastMCP as FastMCPServer
27
+ from fastmcp.server.server import FastMCP
28
+ from fastmcp.utilities.logging import get_logger
29
+ from fastmcp.utilities.mcp_config import MCPConfig, infer_transport_type_from_url
30
+
31
+ if TYPE_CHECKING:
32
+ from fastmcp.utilities.mcp_config import MCPConfig
33
+
34
+ logger = get_logger(__name__)
29
35
 
30
36
 
31
37
  class SessionKwargs(TypedDict, total=False):
@@ -44,6 +50,7 @@ class ClientTransport(abc.ABC):
44
50
 
45
51
  A Transport is responsible for establishing and managing connections
46
52
  to an MCP server, and providing a ClientSession within an async context.
53
+
47
54
  """
48
55
 
49
56
  @abc.abstractmethod
@@ -52,7 +59,9 @@ class ClientTransport(abc.ABC):
52
59
  self, **session_kwargs: Unpack[SessionKwargs]
53
60
  ) -> AsyncIterator[ClientSession]:
54
61
  """
55
- Establishes a connection and yields an active, initialized ClientSession.
62
+ Establishes a connection and yields an active ClientSession.
63
+
64
+ The ClientSession is *not* expected to be initialized in this context manager.
56
65
 
57
66
  The session is guaranteed to be valid only within the scope of the
58
67
  async context manager. Connection setup and teardown are handled
@@ -63,10 +72,10 @@ class ClientTransport(abc.ABC):
63
72
  constructor (e.g., callbacks, timeouts).
64
73
 
65
74
  Yields:
66
- An initialized mcp.ClientSession instance.
75
+ A mcp.ClientSession instance.
67
76
  """
68
77
  raise NotImplementedError
69
- yield None # type: ignore
78
+ yield # type: ignore
70
79
 
71
80
  def __repr__(self) -> str:
72
81
  # Basic representation for subclasses
@@ -92,7 +101,6 @@ class WSTransport(ClientTransport):
92
101
  async with ClientSession(
93
102
  read_stream, write_stream, **session_kwargs
94
103
  ) as session:
95
- await session.initialize() # Initialize after session creation
96
104
  yield session
97
105
 
98
106
  def __repr__(self) -> str:
@@ -141,7 +149,6 @@ class SSETransport(ClientTransport):
141
149
  async with ClientSession(
142
150
  read_stream, write_stream, **session_kwargs
143
151
  ) as session:
144
- await session.initialize()
145
152
  yield session
146
153
 
147
154
  def __repr__(self) -> str:
@@ -187,7 +194,6 @@ class StreamableHttpTransport(ClientTransport):
187
194
  async with ClientSession(
188
195
  read_stream, write_stream, **session_kwargs
189
196
  ) as session:
190
- await session.initialize()
191
197
  yield session
192
198
 
193
199
  def __repr__(self) -> str:
@@ -235,7 +241,6 @@ class StdioTransport(ClientTransport):
235
241
  async with ClientSession(
236
242
  read_stream, write_stream, **session_kwargs
237
243
  ) as session:
238
- await session.initialize()
239
244
  yield session
240
245
 
241
246
  def __repr__(self) -> str:
@@ -451,7 +456,7 @@ class FastMCPTransport(ClientTransport):
451
456
  """
452
457
 
453
458
  def __init__(self, mcp: FastMCPServer):
454
- self._fastmcp = mcp # Can be FastMCP or MCPServer
459
+ self.server = mcp # Can be FastMCP or MCPServer
455
460
 
456
461
  @contextlib.asynccontextmanager
457
462
  async def connect_session(
@@ -459,17 +464,105 @@ class FastMCPTransport(ClientTransport):
459
464
  ) -> AsyncIterator[ClientSession]:
460
465
  # create_connected_server_and_client_session manages the session lifecycle itself
461
466
  async with create_connected_server_and_client_session(
462
- server=self._fastmcp._mcp_server,
467
+ server=self.server._mcp_server,
463
468
  **session_kwargs,
464
469
  ) as session:
465
470
  yield session
466
471
 
467
472
  def __repr__(self) -> str:
468
- return f"<FastMCP(server='{self._fastmcp.name}')>"
473
+ return f"<FastMCP(server='{self.server.name}')>"
474
+
475
+
476
+ class MCPConfigTransport(ClientTransport):
477
+ """Transport for connecting to one or more MCP servers defined in an MCPConfig.
478
+
479
+ This transport provides a unified interface to multiple MCP servers defined in an MCPConfig
480
+ object or dictionary matching the MCPConfig schema. It supports two key scenarios:
481
+
482
+ 1. If the MCPConfig contains exactly one server, it creates a direct transport to that server.
483
+ 2. If the MCPConfig contains multiple servers, it creates a composite client by mounting
484
+ all servers on a single FastMCP instance, with each server's name used as its mounting prefix.
485
+
486
+ In the multi-server case, tools are accessible with the prefix pattern `{server_name}_{tool_name}`
487
+ and resources with the pattern `protocol://{server_name}/path/to/resource`.
488
+
489
+ This is particularly useful for creating clients that need to interact with multiple specialized
490
+ MCP servers through a single interface, simplifying client code.
491
+
492
+ Examples:
493
+ ```python
494
+ from fastmcp import Client
495
+ from fastmcp.utilities.mcp_config import MCPConfig
496
+
497
+ # Create a config with multiple servers
498
+ config = {
499
+ "mcpServers": {
500
+ "weather": {
501
+ "url": "https://weather-api.example.com/mcp",
502
+ "transport": "streamable-http"
503
+ },
504
+ "calendar": {
505
+ "url": "https://calendar-api.example.com/mcp",
506
+ "transport": "streamable-http"
507
+ }
508
+ }
509
+ }
510
+
511
+ # Create a client with the config
512
+ client = Client(config)
513
+
514
+ async with client:
515
+ # Access tools with prefixes
516
+ weather = await client.call_tool("weather_get_forecast", {"city": "London"})
517
+ events = await client.call_tool("calendar_list_events", {"date": "2023-06-01"})
518
+
519
+ # Access resources with prefixed URIs
520
+ icons = await client.read_resource("weather://weather/icons/sunny")
521
+ ```
522
+ """
523
+
524
+ def __init__(self, config: MCPConfig | dict):
525
+ from fastmcp.client.client import Client
526
+
527
+ if isinstance(config, dict):
528
+ config = MCPConfig.from_dict(config)
529
+ self.config = config
530
+
531
+ # if there's exactly one server, create a client for that server
532
+ if len(self.config.mcpServers) == 1:
533
+ self.transport = list(self.config.mcpServers.values())[0].to_transport()
534
+
535
+ # otherwise create a composite client
536
+ else:
537
+ composite_server = FastMCP()
538
+
539
+ for name, server in self.config.mcpServers.items():
540
+ server_client = Client(transport=server.to_transport())
541
+ composite_server.mount(
542
+ prefix=name, server=FastMCP.as_proxy(server_client)
543
+ )
544
+
545
+ self.transport = FastMCPTransport(mcp=composite_server)
546
+
547
+ @contextlib.asynccontextmanager
548
+ async def connect_session(
549
+ self, **session_kwargs: Unpack[SessionKwargs]
550
+ ) -> AsyncIterator[ClientSession]:
551
+ async with self.transport.connect_session(**session_kwargs) as session:
552
+ yield session
553
+
554
+ def __repr__(self) -> str:
555
+ return f"<MCPConfig(config='{self.config}')>"
469
556
 
470
557
 
471
558
  def infer_transport(
472
- transport: ClientTransport | FastMCPServer | AnyUrl | Path | dict[str, Any] | str,
559
+ transport: ClientTransport
560
+ | FastMCPServer
561
+ | AnyUrl
562
+ | Path
563
+ | MCPConfig
564
+ | dict[str, Any]
565
+ | str,
473
566
  ) -> ClientTransport:
474
567
  """
475
568
  Infer the appropriate transport type from the given transport argument.
@@ -478,80 +571,73 @@ def infer_transport(
478
571
  argument, handling various input types and converting them to the appropriate
479
572
  ClientTransport subclass.
480
573
 
574
+ The function supports these input types:
575
+ - ClientTransport: Used directly without modification
576
+ - FastMCPServer: Creates an in-memory FastMCPTransport
577
+ - Path or str (file path): Creates PythonStdioTransport (.py) or NodeStdioTransport (.js)
578
+ - AnyUrl or str (URL): Creates StreamableHttpTransport (default) or SSETransport (for /sse endpoints)
579
+ - MCPConfig or dict: Creates MCPConfigTransport, potentially connecting to multiple servers
580
+
481
581
  For HTTP URLs, they are assumed to be Streamable HTTP URLs unless they end in `/sse`.
582
+
583
+ For MCPConfig with multiple servers, a composite client is created where each server
584
+ is mounted with its name as prefix. This allows accessing tools and resources from multiple
585
+ servers through a single unified client interface, using naming patterns like
586
+ `servername_toolname` for tools and `protocol://servername/path` for resources.
587
+ If the MCPConfig contains only one server, a direct connection is established without prefixing.
588
+
589
+ Examples:
590
+ ```python
591
+ # Connect to a local Python script
592
+ transport = infer_transport("my_script.py")
593
+
594
+ # Connect to a remote server via HTTP
595
+ transport = infer_transport("http://example.com/mcp")
596
+
597
+ # Connect to multiple servers using MCPConfig
598
+ config = {
599
+ "mcpServers": {
600
+ "weather": {"url": "http://weather.example.com/mcp"},
601
+ "calendar": {"url": "http://calendar.example.com/mcp"}
602
+ }
603
+ }
604
+ transport = infer_transport(config)
605
+ ```
482
606
  """
607
+ from fastmcp.utilities.mcp_config import MCPConfig
608
+
483
609
  # the transport is already a ClientTransport
484
610
  if isinstance(transport, ClientTransport):
485
611
  return transport
486
612
 
487
613
  # the transport is a FastMCP server
488
614
  elif isinstance(transport, FastMCPServer):
489
- return FastMCPTransport(mcp=transport)
615
+ inferred_transport = FastMCPTransport(mcp=transport)
490
616
 
491
617
  # the transport is a path to a script
492
618
  elif isinstance(transport, Path | str) and Path(transport).exists():
493
619
  if str(transport).endswith(".py"):
494
- return PythonStdioTransport(script_path=transport)
620
+ inferred_transport = PythonStdioTransport(script_path=transport)
495
621
  elif str(transport).endswith(".js"):
496
- return NodeStdioTransport(script_path=transport)
622
+ inferred_transport = NodeStdioTransport(script_path=transport)
497
623
  else:
498
624
  raise ValueError(f"Unsupported script type: {transport}")
499
625
 
500
626
  # the transport is an http(s) URL
501
627
  elif isinstance(transport, AnyUrl | str) and str(transport).startswith("http"):
502
- if str(transport).rstrip("/").endswith("/sse"):
503
- warnings.warn(
504
- inspect.cleandoc(
505
- """
506
- As of FastMCP 2.3.0, HTTP URLs are inferred to use Streamable HTTP.
507
- The provided URL ends in `/sse`, so you may encounter unexpected behavior.
508
- If you intended to use SSE, please use the `SSETransport` class directly.
509
- """
510
- ),
511
- category=UserWarning,
512
- stacklevel=2,
513
- )
514
- return StreamableHttpTransport(url=transport)
515
-
516
- # the transport is a websocket URL
517
- elif isinstance(transport, AnyUrl | str) and str(transport).startswith("ws"):
518
- return WSTransport(url=transport)
519
-
520
- ## if the transport is a config dict
521
- elif isinstance(transport, dict):
522
- if "mcpServers" not in transport:
523
- raise ValueError("Invalid transport dictionary: missing 'mcpServers' key")
628
+ inferred_transport_type = infer_transport_type_from_url(transport)
629
+ if inferred_transport_type == "sse":
630
+ inferred_transport = SSETransport(url=transport)
524
631
  else:
525
- server = transport["mcpServers"]
526
- if len(list(server.keys())) > 1:
527
- raise ValueError(
528
- "Invalid transport dictionary: multiple servers found - only one expected"
529
- )
530
- server_name = list(server.keys())[0]
531
- # Stdio transport
532
- if "command" in server[server_name] and "args" in server[server_name]:
533
- return StdioTransport(
534
- command=server[server_name]["command"],
535
- args=server[server_name]["args"],
536
- env=server[server_name].get("env", None),
537
- cwd=server[server_name].get("cwd", None),
538
- )
539
-
540
- # HTTP transport
541
- elif "url" in server:
542
- return SSETransport(
543
- url=server["url"],
544
- headers=server.get("headers", None),
545
- )
546
-
547
- # WebSocket transport
548
- elif "ws_url" in server:
549
- return WSTransport(
550
- url=server["ws_url"],
551
- )
632
+ inferred_transport = StreamableHttpTransport(url=transport)
552
633
 
553
- raise ValueError("Cannot determine transport type from dictionary")
634
+ # if the transport is a config dict or MCPConfig
635
+ elif isinstance(transport, dict | MCPConfig):
636
+ inferred_transport = MCPConfigTransport(config=transport)
554
637
 
555
638
  # the transport is an unknown type
556
639
  else:
557
640
  raise ValueError(f"Could not infer a valid transport from: {transport}")
641
+
642
+ logger.debug(f"Inferred transport: {inferred_transport}")
643
+ return inferred_transport
fastmcp/server/context.py CHANGED
@@ -56,7 +56,7 @@ class Context:
56
56
  ctx.error("Error message")
57
57
 
58
58
  # Report progress
59
- ctx.report_progress(50, 100)
59
+ ctx.report_progress(50, 100, "Processing")
60
60
 
61
61
  # Access resources
62
62
  data = ctx.read_resource("resource://data")
@@ -96,7 +96,7 @@ class Context:
96
96
  return self.fastmcp._mcp_server.request_context
97
97
 
98
98
  async def report_progress(
99
- self, progress: float, total: float | None = None
99
+ self, progress: float, total: float | None = None, message: str | None = None
100
100
  ) -> None:
101
101
  """Report progress for the current operation.
102
102
 
@@ -115,7 +115,10 @@ class Context:
115
115
  return
116
116
 
117
117
  await self.request_context.session.send_progress_notification(
118
- progress_token=progress_token, progress=progress, total=total
118
+ progress_token=progress_token,
119
+ progress=progress,
120
+ total=total,
121
+ message=message,
119
122
  )
120
123
 
121
124
  async def read_resource(self, uri: str | AnyUrl) -> list[ReadResourceContents]: