diff --git a/src/mcp/server/fastmcp/server.py b/src/mcp/server/fastmcp/server.py index 719595916..31044cebc 100644 --- a/src/mcp/server/fastmcp/server.py +++ b/src/mcp/server/fastmcp/server.py @@ -88,7 +88,7 @@ class Settings(BaseSettings, Generic[LifespanResultT]): # Server settings debug: bool - log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] + log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None # HTTP settings host: str @@ -152,7 +152,7 @@ def __init__( # noqa: PLR0913 *, tools: list[Tool] | None = None, debug: bool = False, - log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO", + log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = "INFO", host: str = "127.0.0.1", port: int = 8000, mount_path: str = "/", @@ -224,8 +224,9 @@ def __init__( # noqa: PLR0913 # Set up MCP protocol handlers self._setup_handlers() - # Configure logging - configure_logging(self.settings.log_level) + if self.settings.log_level is not None: + # Configure logging + configure_logging(self.settings.log_level) @property def name(self) -> str: @@ -745,7 +746,7 @@ async def run_sse_async(self, mount_path: str | None = None) -> None: starlette_app, host=self.settings.host, port=self.settings.port, - log_level=self.settings.log_level.lower(), + **self._get_uvicorn_log_config(), ) server = uvicorn.Server(config) await server.serve() @@ -760,11 +761,17 @@ async def run_streamable_http_async(self) -> None: starlette_app, host=self.settings.host, port=self.settings.port, - log_level=self.settings.log_level.lower(), + **self._get_uvicorn_log_config(), ) server = uvicorn.Server(config) await server.serve() + def _get_uvicorn_log_config(self) -> dict[str, Any]: + """Map FastMCP log level to Uvicorn log level.""" + if self.settings.log_level is None: + return {"log_level": None, "log_config": None} + return {"log_level": self.settings.log_level.lower()} + def _normalize_path(self, mount_path: str, endpoint: str) -> str: """ Combine mount path and endpoint to return a normalized path. diff --git a/tests/server/fastmcp/test_server.py b/tests/server/fastmcp/test_server.py index 8caa3b1f6..cb7eef939 100644 --- a/tests/server/fastmcp/test_server.py +++ b/tests/server/fastmcp/test_server.py @@ -1,7 +1,7 @@ import base64 from pathlib import Path from typing import TYPE_CHECKING, Any -from unittest.mock import patch +from unittest.mock import AsyncMock, patch import pytest from pydantic import AnyUrl, BaseModel @@ -769,6 +769,37 @@ def get_data() -> str: assert resource.name == "test_get_data" assert resource.mimeType == "text/plain" + def test_server_no_log_reconfigure(self): + """Test that the server does not reconfigure logging if already configured.""" + with patch("mcp.server.fastmcp.server.configure_logging") as configure_logging_mock: + # First instantiation should configure logging + FastMCP(log_level=None) + assert configure_logging_mock.call_count == 0 + + def test_server_uvicorn_no_logging(self): + """Test that the server does not reconfigure logging if uvicorn logging is set.""" + with ( + patch("mcp.server.fastmcp.server.configure_logging") as configure_logging_mock, + patch("uvicorn.Config") as uvicorn_config_mock, + patch("uvicorn.Server") as uvicorn_server_mock, + ): + uvicorn_server_mock.return_value.serve = AsyncMock() + # First instantiation should configure logging + # Launch mock server + mcp = FastMCP(log_level=None) + mcp.run(transport="streamable-http") + # check that logging was not configured + assert configure_logging_mock.call_count == 0 + config_call_args = uvicorn_config_mock.call_args + # Verify that log_config and log_level are None on uvicorn Config + assert "log_config" in config_call_args.kwargs + assert config_call_args.kwargs["log_config"] is None + assert "log_level" in config_call_args.kwargs + assert config_call_args.kwargs["log_level"] is None + # Verify that the server was started + assert uvicorn_server_mock.call_count == 1 + assert uvicorn_server_mock.return_value.serve.call_count == 1 + class TestServerResourceTemplates: @pytest.mark.anyio