Repository: jlowin/fastmcp
Pull request #424 (2025-05-12 19:42:55)

Deprecate passing settings to the FastMCP instance
Adds a deprecation notice but does not change behavior. In the future, transport-specific settings will not be provided via the FastMCP constructor; only settings related to the server's universal operation will be accepted. This is to avoid the reasonable confusion in cases like #419, and more broadly to avoid the apparent trend of FastMCP becoming a dumping group for configuration (this is definitely the case in 1.x, and many of these settings are holdovers from that approach, in addition to new ones like streamable http)


Patch:
diff --git a/src/fastmcp/client/__init__.py b/src/fastmcp/client/__init__.py
index f8b61cf..30a9a29 100644
--- a/src/fastmcp/client/__init__.py
+++ b/src/fastmcp/client/__init__.py
@@ -9,7 +9,6 @@ from .transports import (
     UvxStdioTransport,
     NpxStdioTransport,
     FastMCPTransport,
-    StreamableHttpTransport,
 )
 
 __all__ = [
@@ -23,5 +22,4 @@ __all__ = [
     "UvxStdioTransport",
     "NpxStdioTransport",
     "FastMCPTransport",
-    "StreamableHttpTransport",
 ]
diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py
index 9dabc87..b17b4ec 100644
--- a/src/fastmcp/server/server.py
+++ b/src/fastmcp/server/server.py
@@ -3,7 +3,6 @@
 from __future__ import annotations
 
 import datetime
-import inspect
 import warnings
 from collections.abc import AsyncIterator, Awaitable, Callable
 from contextlib import (
@@ -54,7 +53,7 @@ from fastmcp.tools import ToolManager
 from fastmcp.tools.tool import Tool
 from fastmcp.utilities.cache import TimedCache
 from fastmcp.utilities.decorators import DecoratedFunction
-from fastmcp.utilities.logging import configure_logging, get_logger
+from fastmcp.utilities.logging import get_logger
 
 if TYPE_CHECKING:
     from fastmcp.client import Client
@@ -63,6 +62,8 @@ if TYPE_CHECKING:
 
 logger = get_logger(__name__)
 
+DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
+
 
 @asynccontextmanager
 async def default_lifespan(server: FastMCP) -> AsyncIterator[Any]:
@@ -107,40 +108,52 @@ class FastMCP(Generic[LifespanResultT]):
             | None
         ) = None,
         tags: set[str] | None = None,
+        dependencies: list[str] | None = None,
         tool_serializer: Callable[[Any], str] | None = None,
+        cache_expiration_seconds: float | None = None,
+        on_duplicate_tools: DuplicateBehavior | None = None,
+        on_duplicate_resources: DuplicateBehavior | None = None,
+        on_duplicate_prompts: DuplicateBehavior | None = None,
         **settings: Any,
     ):
-        self.tags: set[str] = tags or set()
+        if settings:
+            # TODO: remove settings. Deprecated since 2.3.4
+            warnings.warn(
+                "Passing runtime and transport-specific settings as kwargs "
+                "to the FastMCP constructor is deprecated (as of 2.3.4), "
+                "including most transport settings. If possible, provide settings when calling "
+                "run() instead.",
+                DeprecationWarning,
+                stacklevel=2,
+            )
         self.settings = fastmcp.settings.ServerSettings(**settings)
+
+        self.tags: set[str] = tags or set()
+        self.dependencies = dependencies
         self._cache = TimedCache(
-            expiration=datetime.timedelta(
-                seconds=self.settings.cache_expiration_seconds
-            )
+            expiration=datetime.timedelta(seconds=cache_expiration_seconds or 0)
         )
-
         self._mounted_servers: dict[str, MountedServer] = {}
+        self._additional_http_routes: list[BaseRoute] = []
+        self._tool_manager = ToolManager(
+            duplicate_behavior=on_duplicate_tools,
+            serializer=tool_serializer,
+        )
+        self._resource_manager = ResourceManager(
+            duplicate_behavior=on_duplicate_resources
+        )
+        self._prompt_manager = PromptManager(duplicate_behavior=on_duplicate_prompts)
 
         if lifespan is None:
             self._has_lifespan = False
             lifespan = default_lifespan
         else:
             self._has_lifespan = True
-
         self._mcp_server = MCPServer[LifespanResultT](
             name=name or "FastMCP",
             instructions=instructions,
             lifespan=_lifespan_wrapper(self, lifespan),
         )
-        self._tool_manager = ToolManager(
-            duplicate_behavior=self.settings.on_duplicate_tools,
-            serializer=tool_serializer,
-        )
-        self._resource_manager = ResourceManager(
-            duplicate_behavior=self.settings.on_duplicate_resources
-        )
-        self._prompt_manager = PromptManager(
-            duplicate_behavior=self.settings.on_duplicate_prompts
-        )
 
         if (self.settings.auth is not None) != (auth_server_provider is not None):
             # TODO: after we support separate authorization servers (see
@@ -150,15 +163,9 @@ class FastMCP(Generic[LifespanResultT]):
             )
         self._auth_server_provider = auth_server_provider
 
-        self._additional_http_routes: list[BaseRoute] = []
-        self.dependencies = self.settings.dependencies
-
         # Set up MCP protocol handlers
         self._setup_handlers()
 
-        # Configure logging
-        configure_logging(self.settings.log_level)
-
     def __repr__(self) -> str:
         return f"{type(self).__name__}({self.name!r})"
 
@@ -764,15 +771,14 @@ class FastMCP(Generic[LifespanResultT]):
         uvicorn_config: dict | None = None,
     ) -> None:
         """Run the server using SSE transport."""
+
+        # Deprecated since 2.3.2
         warnings.warn(
-            inspect.cleandoc(
-                """
-                The run_sse_async method is deprecated. Use run_http_async for a
-                modern (non-SSE) alternative, or create an SSE app with
-                `fastmcp.server.http.create_sse_app` and run it directly.
-                """
-            ),
+            "The run_sse_async method is deprecated (as of 2.3.2). Use run_http_async for a "
+            "modern (non-SSE) alternative, or create an SSE app with "
+            "`fastmcp.server.http.create_sse_app` and run it directly.",
             DeprecationWarning,
+            stacklevel=2,
         )
         await self.run_http_async(
             transport="sse",
@@ -797,14 +803,12 @@ class FastMCP(Generic[LifespanResultT]):
             message_path: The path to the message endpoint
             middleware: A list of middleware to apply to the app
         """
+        # Deprecated since 2.3.2
         warnings.warn(
-            inspect.cleandoc(
-                """
-                The sse_app method is deprecated. Use http_app as a modern (non-SSE)
-                alternative, or call `fastmcp.server.http.create_sse_app` directly.
-                """
-            ),
+            "The sse_app method is deprecated (as of 2.3.2). Use http_app as a modern (non-SSE) "
+            "alternative, or call `fastmcp.server.http.create_sse_app` directly.",
             DeprecationWarning,
+            stacklevel=2,
         )
         return create_sse_app(
             server=self,
@@ -829,9 +833,11 @@ class FastMCP(Generic[LifespanResultT]):
             path: The path to the StreamableHTTP endpoint
             middleware: A list of middleware to apply to the app
         """
+        # Deprecated since 2.3.2
         warnings.warn(
-            "The streamable_http_app method is deprecated. Use http_app() instead.",
+            "The streamable_http_app method is deprecated (as of 2.3.2). Use http_app() instead.",
             DeprecationWarning,
+            stacklevel=2,
         )
         return self.http_app(path=path, middleware=middleware)
 
@@ -886,9 +892,12 @@ class FastMCP(Generic[LifespanResultT]):
         path: str | None = None,
         uvicorn_config: dict | None = None,
     ) -> None:
+        # Deprecated since 2.3.2
         warnings.warn(
-            "The run_streamable_http_async method is deprecated. Use run_http_async instead.",
+            "The run_streamable_http_async method is deprecated (as of 2.3.2). "
+            "Use run_http_async instead.",
             DeprecationWarning,
+            stacklevel=2,
         )
         await self.run_http_async(
             transport="streamable-http",
diff --git a/src/fastmcp/settings.py b/src/fastmcp/settings.py
index 21395ce..b8748b0 100644
--- a/src/fastmcp/settings.py
+++ b/src/fastmcp/settings.py
@@ -3,8 +3,9 @@ from __future__ import annotations as _annotations
 from typing import TYPE_CHECKING, Literal
 
 from mcp.server.auth.settings import AuthSettings
-from pydantic import Field
+from pydantic import Field, model_validator
 from pydantic_settings import BaseSettings, SettingsConfigDict
+from typing_extensions import Self
 
 if TYPE_CHECKING:
     pass
@@ -38,6 +39,15 @@ class Settings(BaseSettings):
         Defaults to False.""",
     )
 
+    @model_validator(mode="after")
+    def setup_logging(self) -> Self:
+        """Finalize the settings."""
+        from fastmcp.utilities.logging import configure_logging
+
+        configure_logging(self.log_level)
+
+        return self
+
 
 class ServerSettings(BaseSettings):
     """FastMCP server settings.
diff --git a/src/fastmcp/tools/tool.py b/src/fastmcp/tools/tool.py
index fd6cdca..7f122c2 100644
--- a/src/fastmcp/tools/tool.py
+++ b/src/fastmcp/tools/tool.py
@@ -143,7 +143,6 @@ class Tool(BaseModel):
 
             return _convert_to_content(result, serializer=self.serializer)
         except Exception as e:
-            logger.exception(f"Tool {self.name} failed")
             raise ToolError(f"Error executing tool {self.name}: {e}") from e
 
     def to_mcp_tool(self, **overrides: Any) -> MCPTool:
diff --git a/src/fastmcp/utilities/logging.py b/src/fastmcp/utilities/logging.py
index cce488b..d300741 100644
--- a/src/fastmcp/utilities/logging.py
+++ b/src/fastmcp/utilities/logging.py
@@ -21,22 +21,27 @@ def get_logger(name: str) -> logging.Logger:
 
 def configure_logging(
     level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | int = "INFO",
+    logger: logging.Logger | None = None,
 ) -> None:
-    """Configure logging for FastMCP.
+    """
+    Configure logging for FastMCP.
 
     Args:
+        logger: the logger to configure
         level: the log level to use
     """
+    if logger is None:
+        logger = logging.getLogger("FastMCP")
+
     # Only configure the FastMCP logger namespace
     handler = RichHandler(console=Console(stderr=True), rich_tracebacks=True)
     formatter = logging.Formatter("%(message)s")
     handler.setFormatter(formatter)
 
-    fastmcp_logger = logging.getLogger("FastMCP")
-    fastmcp_logger.setLevel(level)
+    logger.setLevel(level)
 
     # Remove any existing handlers to avoid duplicates on reconfiguration
-    for hdlr in fastmcp_logger.handlers[:]:
-        fastmcp_logger.removeHandler(hdlr)
+    for hdlr in logger.handlers[:]:
+        logger.removeHandler(hdlr)
 
-    fastmcp_logger.addHandler(handler)
+    logger.addHandler(handler)


Test patch:
diff --git a/tests/test_deprecated.py b/tests/test_deprecated.py
index c5b2ecd..a28bcc4 100644
--- a/tests/test_deprecated.py
+++ b/tests/test_deprecated.py
@@ -9,6 +9,17 @@ from starlette.applications import Starlette
 from fastmcp import FastMCP
 
 
+def test_fastmcp_kwargs_settings_deprecation_warning():
+    """Test that passing settings as kwargs to FastMCP raises a deprecation warning."""
+    with pytest.warns(
+        DeprecationWarning,
+        match="Passing runtime and transport-specific settings as kwargs to the FastMCP constructor is deprecated",
+    ):
+        server = FastMCP("TestServer", host="127.0.0.2", port=8001)
+        assert server.settings.host == "127.0.0.2"
+        assert server.settings.port == 8001
+
+
 def test_sse_app_deprecation_warning():
     """Test that sse_app raises a deprecation warning."""
     server = FastMCP("TestServer")