diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index 9dabc8735..8d7cf4ab6 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() - self.settings = fastmcp.settings.ServerSettings(**settings) - self._cache = TimedCache( - expiration=datetime.timedelta( - seconds=self.settings.cache_expiration_seconds + if settings: + # TODO: remove settings. Deprecated since 2.3.4 + warnings.warn( + "Passing transport-specific and other runtime settings as kwargs " + "to the FastMCP constructor is deprecated (as of 2.3.4), " + "including most transport settings. 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=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 21395ce7c..b8748b0d2 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 cce488b45..d30074190 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) diff --git a/tests/test_deprecated.py b/tests/test_deprecated.py index c5b2ecdcd..8085c6e3c 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 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")