Repository: jlowin/fastmcp

Issue:
[BUG] fastmcp service fails to bind port, mcp.run() hangs without error (typer 0.15.x dependency)
### Description

### [BUG] fastmcp service fails to bind port, mcp.run() hangs without error (typer 0.15.x dependency)

#### Environment Information:
- **OS:** macOS (please specify your version)
- **Python:** 3.10 (please specify your version)
- **fastmcp:** 2.3.3
- **typer:** 0.15.3
- **click:** 8.1.3
- **fastapi:** 0.115.12
- **uvicorn:** 0.34.2

---

#### Issue Description:
When running `fastmcp` 2.3.3 (or 2.2.0) with `typer` 0.15.x, executing the official minimal server example results in `mcp.run()` only outputting `Starting server ...`. However, the service does not bind to the port, does not start, and does not raise any errors or exceptions.

---

#### Steps to Reproduce:
1. Create a new virtual environment and install **fastmcp** and its dependencies (see `pip list` below).
2. Create the following script `server.py`:
    ```python
    from fastmcp import FastMCP

    mcp = FastMCP("Demo Server", host="0.0.0.0", port=8090)

    @mcp.tool()
    def add(a: int, b: int) -> int:
        return a + b

    if __name__ == "__main__":
        print("Starting MCP server ...")
        mcp.run()
        print("MCP server started on port 8090")
    ```
3. Run `python server.py`. Terminal output:
    ```
    Starting MCP server ...
    [05/12/25 20:53:39] INFO     Starting server "Demo Server"...
    ```
    However, there is no further output, and port 8090 is not bound (no process found with `lsof -i :8090`).

---

#### Expected Behavior:
`mcp.run()` should successfully start the service, bind to the port, and be accessible via `curl` or other clients.

---

#### Actual Behavior:
Only `Starting server ...` is printed. The service does not bind to the port, does not start, and does not throw any errors.

---

#### Additional Notes:
- A minimal **FastAPI+uvicorn** server can bind to the port, indicating that the environment and dependencies are fine.
- **fastmcp 2.x** requires **typer 0.15.2+**, but this combination fails to start the service locally.

---

#### Dependency List:
Here are the main dependencies from `pip list`:
- fastmcp==2.3.3
- typer==0.15.3
- click==8.1.3
- fastapi==0.115.12
- uvicorn==0.34.2

---

#### Request for Assistance:
Please help investigate compatibility issues or provide suggestions for a resolution.

### Example Code

```Python

```

### Version Information

```Text
$ fastmcp version
FastMCP version:                                                                   2.3.3
MCP version:                                                                       1.8.0
Python version:                                                                  3.10.17
Platform:                                                 macOS-15.4.1-x86_64-i386-64bit
FastMCP root path: /Users/wuwei7/opt/anaconda3/envs/mcp_env/lib/python3.10/site-packages
```

### Additional Context

_No response_

Discussion:
@jlowin: `mcp.run()` uses `stdio` transport by default, which does not have a host or port binding. You need to provide an alternative transport (like sse or streamable http) to use host/port settings. Please review https://gofastmcp.com/deployment/running-server. 

As a side note, I increasingly dislike passing any transport arguments to the FastMCP instance on startup because it creates confusion just like this -- probably we will remove them in the near future (though that would constitute a breaking change!)

Solution patch:
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/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")