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.
@@ -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,13 @@
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
9
  from typing import Any, TypedDict, cast
10
+ from urllib.parse import urlparse
12
11
 
13
12
  from mcp import ClientSession, StdioServerParameters
14
13
  from mcp.client.session import (
@@ -26,6 +25,9 @@ from pydantic import AnyUrl
26
25
  from typing_extensions import Unpack
27
26
 
28
27
  from fastmcp.server import FastMCP as FastMCPServer
28
+ from fastmcp.utilities.logging import get_logger
29
+
30
+ logger = get_logger(__name__)
29
31
 
30
32
 
31
33
  class SessionKwargs(TypedDict, total=False):
@@ -44,6 +46,7 @@ class ClientTransport(abc.ABC):
44
46
 
45
47
  A Transport is responsible for establishing and managing connections
46
48
  to an MCP server, and providing a ClientSession within an async context.
49
+
47
50
  """
48
51
 
49
52
  @abc.abstractmethod
@@ -52,7 +55,9 @@ class ClientTransport(abc.ABC):
52
55
  self, **session_kwargs: Unpack[SessionKwargs]
53
56
  ) -> AsyncIterator[ClientSession]:
54
57
  """
55
- Establishes a connection and yields an active, initialized ClientSession.
58
+ Establishes a connection and yields an active ClientSession.
59
+
60
+ The ClientSession is *not* expected to be initialized in this context manager.
56
61
 
57
62
  The session is guaranteed to be valid only within the scope of the
58
63
  async context manager. Connection setup and teardown are handled
@@ -63,7 +68,7 @@ class ClientTransport(abc.ABC):
63
68
  constructor (e.g., callbacks, timeouts).
64
69
 
65
70
  Yields:
66
- An initialized mcp.ClientSession instance.
71
+ A mcp.ClientSession instance.
67
72
  """
68
73
  raise NotImplementedError
69
74
  yield None # type: ignore
@@ -92,7 +97,6 @@ class WSTransport(ClientTransport):
92
97
  async with ClientSession(
93
98
  read_stream, write_stream, **session_kwargs
94
99
  ) as session:
95
- await session.initialize() # Initialize after session creation
96
100
  yield session
97
101
 
98
102
  def __repr__(self) -> str:
@@ -141,7 +145,6 @@ class SSETransport(ClientTransport):
141
145
  async with ClientSession(
142
146
  read_stream, write_stream, **session_kwargs
143
147
  ) as session:
144
- await session.initialize()
145
148
  yield session
146
149
 
147
150
  def __repr__(self) -> str:
@@ -187,7 +190,6 @@ class StreamableHttpTransport(ClientTransport):
187
190
  async with ClientSession(
188
191
  read_stream, write_stream, **session_kwargs
189
192
  ) as session:
190
- await session.initialize()
191
193
  yield session
192
194
 
193
195
  def __repr__(self) -> str:
@@ -235,7 +237,6 @@ class StdioTransport(ClientTransport):
235
237
  async with ClientSession(
236
238
  read_stream, write_stream, **session_kwargs
237
239
  ) as session:
238
- await session.initialize()
239
240
  yield session
240
241
 
241
242
  def __repr__(self) -> str:
@@ -486,36 +487,29 @@ def infer_transport(
486
487
 
487
488
  # the transport is a FastMCP server
488
489
  elif isinstance(transport, FastMCPServer):
489
- return FastMCPTransport(mcp=transport)
490
+ inferred_transport = FastMCPTransport(mcp=transport)
490
491
 
491
492
  # the transport is a path to a script
492
493
  elif isinstance(transport, Path | str) and Path(transport).exists():
493
494
  if str(transport).endswith(".py"):
494
- return PythonStdioTransport(script_path=transport)
495
+ inferred_transport = PythonStdioTransport(script_path=transport)
495
496
  elif str(transport).endswith(".js"):
496
- return NodeStdioTransport(script_path=transport)
497
+ inferred_transport = NodeStdioTransport(script_path=transport)
497
498
  else:
498
499
  raise ValueError(f"Unsupported script type: {transport}")
499
500
 
500
501
  # the transport is an http(s) URL
501
502
  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)
503
+ transport_str = str(transport)
504
+ # Parse out just the path portion to check for /sse
505
+ parsed_url = urlparse(transport_str)
506
+ path = parsed_url.path
507
+
508
+ # Check if path contains /sse/ or ends with /sse
509
+ if "/sse/" in path or path.rstrip("/").endswith("/sse"):
510
+ inferred_transport = SSETransport(url=transport)
511
+ else:
512
+ inferred_transport = StreamableHttpTransport(url=transport)
519
513
 
520
514
  ## if the transport is a config dict
521
515
  elif isinstance(transport, dict):
@@ -530,7 +524,7 @@ def infer_transport(
530
524
  server_name = list(server.keys())[0]
531
525
  # Stdio transport
532
526
  if "command" in server[server_name] and "args" in server[server_name]:
533
- return StdioTransport(
527
+ inferred_transport = StdioTransport(
534
528
  command=server[server_name]["command"],
535
529
  args=server[server_name]["args"],
536
530
  env=server[server_name].get("env", None),
@@ -539,19 +533,16 @@ def infer_transport(
539
533
 
540
534
  # HTTP transport
541
535
  elif "url" in server:
542
- return SSETransport(
536
+ inferred_transport = SSETransport(
543
537
  url=server["url"],
544
538
  headers=server.get("headers", None),
545
539
  )
546
540
 
547
- # WebSocket transport
548
- elif "ws_url" in server:
549
- return WSTransport(
550
- url=server["ws_url"],
551
- )
552
-
553
541
  raise ValueError("Cannot determine transport type from dictionary")
554
542
 
555
543
  # the transport is an unknown type
556
544
  else:
557
545
  raise ValueError(f"Could not infer a valid transport from: {transport}")
546
+
547
+ logger.debug(f"Inferred transport: {inferred_transport}")
548
+ 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]:
fastmcp/server/http.py CHANGED
@@ -10,9 +10,16 @@ from mcp.server.auth.middleware.bearer_auth import (
10
10
  BearerAuthBackend,
11
11
  RequireAuthMiddleware,
12
12
  )
13
- from mcp.server.auth.provider import OAuthAuthorizationServerProvider
13
+ from mcp.server.auth.provider import (
14
+ AccessTokenT,
15
+ AuthorizationCodeT,
16
+ OAuthAuthorizationServerProvider,
17
+ RefreshTokenT,
18
+ )
14
19
  from mcp.server.auth.routes import create_auth_routes
15
20
  from mcp.server.auth.settings import AuthSettings
21
+ from mcp.server.lowlevel.server import LifespanResultT
22
+ from mcp.server.sse import SseServerTransport
16
23
  from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
17
24
  from starlette.applications import Starlette
18
25
  from starlette.middleware import Middleware
@@ -20,9 +27,8 @@ from starlette.middleware.authentication import AuthenticationMiddleware
20
27
  from starlette.requests import Request
21
28
  from starlette.responses import Response
22
29
  from starlette.routing import BaseRoute, Mount, Route
23
- from starlette.types import Receive, Scope, Send
30
+ from starlette.types import Lifespan, Receive, Scope, Send
24
31
 
25
- from fastmcp.low_level.sse_server_transport import SseServerTransport
26
32
  from fastmcp.utilities.logging import get_logger
27
33
 
28
34
  if TYPE_CHECKING:
@@ -30,12 +36,19 @@ if TYPE_CHECKING:
30
36
 
31
37
  logger = get_logger(__name__)
32
38
 
39
+
33
40
  _current_http_request: ContextVar[Request | None] = ContextVar(
34
41
  "http_request",
35
42
  default=None,
36
43
  )
37
44
 
38
45
 
46
+ class StarletteWithLifespan(Starlette):
47
+ @property
48
+ def lifespan(self) -> Lifespan:
49
+ return self.router.lifespan_context
50
+
51
+
39
52
  @contextmanager
40
53
  def set_http_request(request: Request) -> Generator[Request, None, None]:
41
54
  token = _current_http_request.set(request)
@@ -62,7 +75,10 @@ class RequestContextMiddleware:
62
75
 
63
76
 
64
77
  def setup_auth_middleware_and_routes(
65
- auth_server_provider: OAuthAuthorizationServerProvider | None,
78
+ auth_server_provider: OAuthAuthorizationServerProvider[
79
+ AuthorizationCodeT, RefreshTokenT, AccessTokenT
80
+ ]
81
+ | None,
66
82
  auth_settings: AuthSettings | None,
67
83
  ) -> tuple[list[Middleware], list[BaseRoute], list[str]]:
68
84
  """Set up authentication middleware and routes if auth is enabled.
@@ -112,7 +128,7 @@ def create_base_app(
112
128
  middleware: list[Middleware],
113
129
  debug: bool = False,
114
130
  lifespan: Callable | None = None,
115
- ) -> Starlette:
131
+ ) -> StarletteWithLifespan:
116
132
  """Create a base Starlette app with common middleware and routes.
117
133
 
118
134
  Args:
@@ -127,7 +143,7 @@ def create_base_app(
127
143
  # Always add RequestContextMiddleware as the outermost middleware
128
144
  middleware.append(Middleware(RequestContextMiddleware))
129
145
 
130
- return Starlette(
146
+ return StarletteWithLifespan(
131
147
  routes=routes,
132
148
  middleware=middleware,
133
149
  debug=debug,
@@ -136,15 +152,18 @@ def create_base_app(
136
152
 
137
153
 
138
154
  def create_sse_app(
139
- server: FastMCP,
155
+ server: FastMCP[LifespanResultT],
140
156
  message_path: str,
141
157
  sse_path: str,
142
- auth_server_provider: OAuthAuthorizationServerProvider | None = None,
158
+ auth_server_provider: OAuthAuthorizationServerProvider[
159
+ AuthorizationCodeT, RefreshTokenT, AccessTokenT
160
+ ]
161
+ | None = None,
143
162
  auth_settings: AuthSettings | None = None,
144
163
  debug: bool = False,
145
164
  routes: list[BaseRoute] | None = None,
146
165
  middleware: list[Middleware] | None = None,
147
- ) -> Starlette:
166
+ ) -> StarletteWithLifespan:
148
167
  """Return an instance of the SSE server app.
149
168
 
150
169
  Args:
@@ -228,25 +247,33 @@ def create_sse_app(
228
247
  server_middleware.extend(middleware)
229
248
 
230
249
  # Create and return the app
231
- return create_base_app(
250
+ app = create_base_app(
232
251
  routes=server_routes,
233
252
  middleware=server_middleware,
234
253
  debug=debug,
235
254
  )
255
+ # Store the FastMCP server instance on the Starlette app state
256
+ app.state.fastmcp_server = server
257
+ app.state.path = sse_path
258
+
259
+ return app
236
260
 
237
261
 
238
262
  def create_streamable_http_app(
239
- server: FastMCP,
263
+ server: FastMCP[LifespanResultT],
240
264
  streamable_http_path: str,
241
265
  event_store: None = None,
242
- auth_server_provider: OAuthAuthorizationServerProvider | None = None,
266
+ auth_server_provider: OAuthAuthorizationServerProvider[
267
+ AuthorizationCodeT, RefreshTokenT, AccessTokenT
268
+ ]
269
+ | None = None,
243
270
  auth_settings: AuthSettings | None = None,
244
271
  json_response: bool = False,
245
272
  stateless_http: bool = False,
246
273
  debug: bool = False,
247
274
  routes: list[BaseRoute] | None = None,
248
275
  middleware: list[Middleware] | None = None,
249
- ) -> Starlette:
276
+ ) -> StarletteWithLifespan:
250
277
  """Return an instance of the StreamableHTTP server app.
251
278
 
252
279
  Args:
@@ -322,9 +349,15 @@ def create_streamable_http_app(
322
349
  yield
323
350
 
324
351
  # Create and return the app with lifespan
325
- return create_base_app(
352
+ app = create_base_app(
326
353
  routes=server_routes,
327
354
  middleware=server_middleware,
328
355
  debug=debug,
329
356
  lifespan=lifespan,
330
357
  )
358
+ # Store the FastMCP server instance on the Starlette app state
359
+ app.state.fastmcp_server = server
360
+
361
+ app.state.path = streamable_http_path
362
+
363
+ return app
fastmcp/server/server.py CHANGED
@@ -11,6 +11,7 @@ from contextlib import (
11
11
  asynccontextmanager,
12
12
  )
13
13
  from functools import partial
14
+ from pathlib import Path
14
15
  from typing import TYPE_CHECKING, Any, Generic, Literal
15
16
 
16
17
  import anyio
@@ -35,7 +36,6 @@ from mcp.types import Resource as MCPResource
35
36
  from mcp.types import ResourceTemplate as MCPResourceTemplate
36
37
  from mcp.types import Tool as MCPTool
37
38
  from pydantic import AnyUrl
38
- from starlette.applications import Starlette
39
39
  from starlette.middleware import Middleware
40
40
  from starlette.requests import Request
41
41
  from starlette.responses import Response
@@ -48,7 +48,11 @@ from fastmcp.prompts import Prompt, PromptManager
48
48
  from fastmcp.prompts.prompt import PromptResult
49
49
  from fastmcp.resources import Resource, ResourceManager
50
50
  from fastmcp.resources.template import ResourceTemplate
51
- from fastmcp.server.http import create_sse_app
51
+ from fastmcp.server.http import (
52
+ StarletteWithLifespan,
53
+ create_sse_app,
54
+ create_streamable_http_app,
55
+ )
52
56
  from fastmcp.tools import ToolManager
53
57
  from fastmcp.tools.tool import Tool
54
58
  from fastmcp.utilities.cache import TimedCache
@@ -57,16 +61,16 @@ from fastmcp.utilities.logging import get_logger
57
61
 
58
62
  if TYPE_CHECKING:
59
63
  from fastmcp.client import Client
64
+ from fastmcp.client.transports import ClientTransport
60
65
  from fastmcp.server.openapi import FastMCPOpenAPI
61
66
  from fastmcp.server.proxy import FastMCPProxy
62
-
63
67
  logger = get_logger(__name__)
64
68
 
65
69
  DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
66
70
 
67
71
 
68
72
  @asynccontextmanager
69
- async def default_lifespan(server: FastMCP) -> AsyncIterator[Any]:
73
+ async def default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[Any]:
70
74
  """Default lifespan context manager that does nothing.
71
75
 
72
76
  Args:
@@ -79,8 +83,10 @@ async def default_lifespan(server: FastMCP) -> AsyncIterator[Any]:
79
83
 
80
84
 
81
85
  def _lifespan_wrapper(
82
- app: FastMCP,
83
- lifespan: Callable[[FastMCP], AbstractAsyncContextManager[LifespanResultT]],
86
+ app: FastMCP[LifespanResultT],
87
+ lifespan: Callable[
88
+ [FastMCP[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
89
+ ],
84
90
  ) -> Callable[
85
91
  [MCPServer[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
86
92
  ]:
@@ -189,15 +195,13 @@ class FastMCP(Generic[LifespanResultT]):
189
195
  """
190
196
  if transport is None:
191
197
  transport = "stdio"
192
- if transport not in ["stdio", "streamable-http", "sse"]:
198
+ if transport not in {"stdio", "streamable-http", "sse"}:
193
199
  raise ValueError(f"Unknown transport: {transport}")
194
200
 
195
201
  if transport == "stdio":
196
202
  await self.run_stdio_async(**transport_kwargs)
197
- elif transport == "streamable-http":
198
- await self.run_http_async(transport="streamable-http", **transport_kwargs)
199
- elif transport == "sse":
200
- await self.run_http_async(transport="sse", **transport_kwargs)
203
+ elif transport in {"streamable-http", "sse"}:
204
+ await self.run_http_async(transport=transport, **transport_kwargs)
201
205
  else:
202
206
  raise ValueError(f"Unknown transport: {transport}")
203
207
 
@@ -211,7 +215,6 @@ class FastMCP(Generic[LifespanResultT]):
211
215
  Args:
212
216
  transport: Transport protocol to use ("stdio", "sse", or "streamable-http")
213
217
  """
214
- logger.info(f'Starting server "{self.name}"...')
215
218
 
216
219
  anyio.run(partial(self.run_async, transport, **transport_kwargs))
217
220
 
@@ -228,7 +231,7 @@ class FastMCP(Generic[LifespanResultT]):
228
231
  async def get_tools(self) -> dict[str, Tool]:
229
232
  """Get all registered tools, indexed by registered key."""
230
233
  if (tools := self._cache.get("tools")) is self._cache.NOT_FOUND:
231
- tools = {}
234
+ tools: dict[str, Tool] = {}
232
235
  for server in self._mounted_servers.values():
233
236
  server_tools = await server.get_tools()
234
237
  tools.update(server_tools)
@@ -239,7 +242,7 @@ class FastMCP(Generic[LifespanResultT]):
239
242
  async def get_resources(self) -> dict[str, Resource]:
240
243
  """Get all registered resources, indexed by registered key."""
241
244
  if (resources := self._cache.get("resources")) is self._cache.NOT_FOUND:
242
- resources = {}
245
+ resources: dict[str, Resource] = {}
243
246
  for server in self._mounted_servers.values():
244
247
  server_resources = await server.get_resources()
245
248
  resources.update(server_resources)
@@ -252,7 +255,7 @@ class FastMCP(Generic[LifespanResultT]):
252
255
  if (
253
256
  templates := self._cache.get("resource_templates")
254
257
  ) is self._cache.NOT_FOUND:
255
- templates = {}
258
+ templates: dict[str, ResourceTemplate] = {}
256
259
  for server in self._mounted_servers.values():
257
260
  server_templates = await server.get_resource_templates()
258
261
  templates.update(server_templates)
@@ -265,7 +268,7 @@ class FastMCP(Generic[LifespanResultT]):
265
268
  List all available prompts.
266
269
  """
267
270
  if (prompts := self._cache.get("prompts")) is self._cache.NOT_FOUND:
268
- prompts = {}
271
+ prompts: dict[str, Prompt] = {}
269
272
  for server in self._mounted_servers.values():
270
273
  server_prompts = await server.get_prompts()
271
274
  prompts.update(server_prompts)
@@ -728,6 +731,7 @@ class FastMCP(Generic[LifespanResultT]):
728
731
  async def run_stdio_async(self) -> None:
729
732
  """Run the server using stdio transport."""
730
733
  async with stdio_server() as (read_stream, write_stream):
734
+ logger.info(f"Starting MCP server {self.name!r} with transport 'stdio'")
731
735
  await self._mcp_server.run(
732
736
  read_stream,
733
737
  write_stream,
@@ -743,7 +747,8 @@ class FastMCP(Generic[LifespanResultT]):
743
747
  port: int | None = None,
744
748
  log_level: str | None = None,
745
749
  path: str | None = None,
746
- uvicorn_config: dict | None = None,
750
+ uvicorn_config: dict[str, Any] | None = None,
751
+ middleware: list[Middleware] | None = None,
747
752
  ) -> None:
748
753
  """Run the server using HTTP transport.
749
754
 
@@ -755,21 +760,29 @@ class FastMCP(Generic[LifespanResultT]):
755
760
  path: Path for the endpoint (defaults to settings.streamable_http_path or settings.sse_path)
756
761
  uvicorn_config: Additional configuration for the Uvicorn server
757
762
  """
758
- uvicorn_config = uvicorn_config or {}
759
- uvicorn_config.setdefault("timeout_graceful_shutdown", 0)
760
- # lifespan is required for streamable http
761
- uvicorn_config["lifespan"] = "on"
762
-
763
- app = self.http_app(path=path, transport=transport)
764
-
765
- config = uvicorn.Config(
766
- app,
767
- host=host or self.settings.host,
768
- port=port or self.settings.port,
769
- log_level=log_level or self.settings.log_level.lower(),
770
- **uvicorn_config,
771
- )
763
+ host = host or self.settings.host
764
+ port = port or self.settings.port
765
+ default_log_level_to_use = log_level or self.settings.log_level.lower()
766
+
767
+ app = self.http_app(path=path, transport=transport, middleware=middleware)
768
+
769
+ _uvicorn_config_from_user = uvicorn_config or {}
770
+
771
+ config_kwargs: dict[str, Any] = {
772
+ "timeout_graceful_shutdown": 0,
773
+ "lifespan": "on",
774
+ }
775
+ config_kwargs.update(_uvicorn_config_from_user)
776
+
777
+ if "log_config" not in config_kwargs and "log_level" not in config_kwargs:
778
+ config_kwargs["log_level"] = default_log_level_to_use
779
+
780
+ config = uvicorn.Config(app, host=host, port=port, **config_kwargs)
772
781
  server = uvicorn.Server(config)
782
+ path = app.state.path.lstrip("/") # type: ignore
783
+ logger.info(
784
+ f"Starting MCP server {self.name!r} with transport {transport!r} on http://{host}:{port}/{path}"
785
+ )
773
786
  await server.serve()
774
787
 
775
788
  async def run_sse_async(
@@ -779,7 +792,7 @@ class FastMCP(Generic[LifespanResultT]):
779
792
  log_level: str | None = None,
780
793
  path: str | None = None,
781
794
  message_path: str | None = None,
782
- uvicorn_config: dict | None = None,
795
+ uvicorn_config: dict[str, Any] | None = None,
783
796
  ) -> None:
784
797
  """Run the server using SSE transport."""
785
798
 
@@ -805,7 +818,7 @@ class FastMCP(Generic[LifespanResultT]):
805
818
  path: str | None = None,
806
819
  message_path: str | None = None,
807
820
  middleware: list[Middleware] | None = None,
808
- ) -> Starlette:
821
+ ) -> StarletteWithLifespan:
809
822
  """
810
823
  Create a Starlette app for the SSE server.
811
824
 
@@ -836,7 +849,7 @@ class FastMCP(Generic[LifespanResultT]):
836
849
  self,
837
850
  path: str | None = None,
838
851
  middleware: list[Middleware] | None = None,
839
- ) -> Starlette:
852
+ ) -> StarletteWithLifespan:
840
853
  """
841
854
  Create a Starlette app for the StreamableHTTP server.
842
855
 
@@ -857,7 +870,7 @@ class FastMCP(Generic[LifespanResultT]):
857
870
  path: str | None = None,
858
871
  middleware: list[Middleware] | None = None,
859
872
  transport: Literal["streamable-http", "sse"] = "streamable-http",
860
- ) -> Starlette:
873
+ ) -> StarletteWithLifespan:
861
874
  """Create a Starlette app using the specified HTTP transport.
862
875
 
863
876
  Args:
@@ -868,7 +881,6 @@ class FastMCP(Generic[LifespanResultT]):
868
881
  Returns:
869
882
  A Starlette application configured with the specified transport
870
883
  """
871
- from fastmcp.server.http import create_streamable_http_app
872
884
 
873
885
  if transport == "streamable-http":
874
886
  return create_streamable_http_app(
@@ -901,7 +913,7 @@ class FastMCP(Generic[LifespanResultT]):
901
913
  port: int | None = None,
902
914
  log_level: str | None = None,
903
915
  path: str | None = None,
904
- uvicorn_config: dict | None = None,
916
+ uvicorn_config: dict[str, Any] | None = None,
905
917
  ) -> None:
906
918
  # Deprecated since 2.3.2
907
919
  warnings.warn(
@@ -1028,9 +1040,6 @@ class FastMCP(Generic[LifespanResultT]):
1028
1040
  - The prompts are imported with prefixed names using the
1029
1041
  prompt_separator Example: If server has a prompt named
1030
1042
  "weather_prompt", it will be available as "weather_weather_prompt"
1031
- - The mounted server's lifespan will be executed when the parent
1032
- server's lifespan runs, ensuring that any setup needed by the mounted
1033
- server is performed
1034
1043
 
1035
1044
  Args:
1036
1045
  prefix: The prefix to use for the mounted server server: The FastMCP
@@ -1102,14 +1111,48 @@ class FastMCP(Generic[LifespanResultT]):
1102
1111
  openapi_spec=app.openapi(), client=client, name=name, **settings
1103
1112
  )
1104
1113
 
1114
+ @classmethod
1115
+ def as_proxy(
1116
+ cls,
1117
+ backend: Client
1118
+ | ClientTransport
1119
+ | FastMCP[Any]
1120
+ | AnyUrl
1121
+ | Path
1122
+ | dict[str, Any]
1123
+ | str,
1124
+ **settings: Any,
1125
+ ) -> FastMCPProxy:
1126
+ """Create a FastMCP proxy server for the given backend.
1127
+
1128
+ The ``backend`` argument can be either an existing :class:`~fastmcp.client.Client`
1129
+ instance or any value accepted as the ``transport`` argument of
1130
+ :class:`~fastmcp.client.Client`. This mirrors the convenience of the
1131
+ ``Client`` constructor.
1132
+ """
1133
+ from fastmcp.client.client import Client
1134
+ from fastmcp.server.proxy import FastMCPProxy
1135
+
1136
+ if isinstance(backend, Client):
1137
+ client = backend
1138
+ else:
1139
+ client = Client(backend)
1140
+
1141
+ return FastMCPProxy(client=client, **settings)
1142
+
1105
1143
  @classmethod
1106
1144
  def from_client(cls, client: Client, **settings: Any) -> FastMCPProxy:
1107
1145
  """
1108
1146
  Create a FastMCP proxy server from a FastMCP client.
1109
1147
  """
1110
- from fastmcp.server.proxy import FastMCPProxy
1148
+ # Deprecated since 2.3.5
1149
+ warnings.warn(
1150
+ "FastMCP.from_client() is deprecated; use FastMCP.as_proxy() instead.",
1151
+ DeprecationWarning,
1152
+ stacklevel=2,
1153
+ )
1111
1154
 
1112
- return FastMCPProxy(client=client, **settings)
1155
+ return cls.as_proxy(client, **settings)
1113
1156
 
1114
1157
 
1115
1158
  def _validate_resource_prefix(prefix: str) -> None:
@@ -1128,7 +1171,7 @@ class MountedServer:
1128
1171
  def __init__(
1129
1172
  self,
1130
1173
  prefix: str,
1131
- server: FastMCP,
1174
+ server: FastMCP[LifespanResultT],
1132
1175
  tool_separator: str | None = None,
1133
1176
  resource_separator: str | None = None,
1134
1177
  prompt_separator: str | None = None,