From c326aa0778d7db095ea267fb36ce8dc3f5239c65 Mon Sep 17 00:00:00 2001 From: Genmin Date: Fri, 1 May 2026 08:26:45 -0700 Subject: [PATCH 1/5] fix: keep FastMCP from configuring app logging --- src/mcp/cli/cli.py | 16 ++++++++-- src/mcp/server/mcpserver/server.py | 5 +--- tests/cli/test_utils.py | 42 ++++++++++++++++++++++++++- tests/server/mcpserver/test_server.py | 13 +++++++++ 4 files changed, 69 insertions(+), 7 deletions(-) diff --git a/src/mcp/cli/cli.py b/src/mcp/cli/cli.py index 62334a4a2..1de96cdfa 100644 --- a/src/mcp/cli/cli.py +++ b/src/mcp/cli/cli.py @@ -6,7 +6,7 @@ import subprocess import sys from pathlib import Path -from typing import Annotated, Any +from typing import Annotated, Any, Literal from mcp.server import MCPServer from mcp.server import Server as LowLevelServer @@ -19,7 +19,7 @@ try: from mcp.cli import claude - from mcp.server.mcpserver.utilities.logging import get_logger + from mcp.server.mcpserver.utilities.logging import configure_logging, get_logger except ImportError: # pragma: no cover print("Error: mcp.server is not installed or not in PYTHONPATH") sys.exit(1) @@ -31,6 +31,8 @@ logger = get_logger("cli") +LogLevel = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] + app = typer.Typer( name="mcp", help="MCP development tools", @@ -116,6 +118,14 @@ def _parse_file_path(file_spec: str) -> tuple[Path, str | None]: return file_path, server_object +def _get_server_log_level(server: Any) -> LogLevel: + settings = getattr(server, "settings", None) + log_level = getattr(settings, "log_level", "INFO") + if log_level in ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"): + return log_level + return "INFO" + + def _import_server(file: Path, server_object: str | None = None): # pragma: no cover """Import an MCP server from a file. @@ -340,6 +350,8 @@ def run( # Import and get server object server = _import_server(file, server_object) + configure_logging(_get_server_log_level(server)) + # Run the server kwargs = {} if transport: diff --git a/src/mcp/server/mcpserver/server.py b/src/mcp/server/mcpserver/server.py index be77705da..d243aef86 100644 --- a/src/mcp/server/mcpserver/server.py +++ b/src/mcp/server/mcpserver/server.py @@ -36,7 +36,7 @@ from mcp.server.mcpserver.resources import FunctionResource, Resource, ResourceManager from mcp.server.mcpserver.tools import Tool, ToolManager from mcp.server.mcpserver.utilities.context_injection import find_context_parameter -from mcp.server.mcpserver.utilities.logging import configure_logging, get_logger +from mcp.server.mcpserver.utilities.logging import get_logger from mcp.server.sse import SseServerTransport from mcp.server.stdio import stdio_server from mcp.server.streamable_http import EventStore @@ -203,9 +203,6 @@ def __init__( self._token_verifier = ProviderTokenVerifier(auth_server_provider) self._custom_starlette_routes: list[Route] = [] - # Configure logging - configure_logging(self.settings.log_level) - @property def name(self) -> str: return self._lowlevel_server.name diff --git a/tests/cli/test_utils.py b/tests/cli/test_utils.py index 44f4ab4d3..0c9fab414 100644 --- a/tests/cli/test_utils.py +++ b/tests/cli/test_utils.py @@ -5,7 +5,12 @@ import pytest -from mcp.cli.cli import _build_uv_command, _get_npx_command, _parse_file_path # type: ignore[reportPrivateUsage] +from mcp.cli.cli import ( # type: ignore[reportPrivateUsage] + _build_uv_command, + _get_npx_command, + _parse_file_path, + run, +) @pytest.mark.parametrize( @@ -38,6 +43,41 @@ def test_parse_file_exit_on_dir(tmp_path: Path): _parse_file_path(str(dir_path)) +def test_run_configures_logging_at_cli_entrypoint(monkeypatch: pytest.MonkeyPatch, tmp_path: Path): + """The CLI owns process logging setup; importing or constructing the server does not.""" + file = tmp_path / "server.py" + file.write_text("server = object()") + calls: list[tuple[str, Any]] = [] + + class Settings: + log_level = "DEBUG" + + class ImportedServer: + settings = Settings() + + def run(self, **kwargs: Any) -> None: + calls.append(("run", kwargs)) + + def fake_import_server(*_args: Any) -> ImportedServer: + return ImportedServer() + + def fake_configure_logging(level: str) -> None: + calls.append(("logging", level)) + + monkeypatch.setattr("mcp.cli.cli._import_server", fake_import_server) + monkeypatch.setattr( + "mcp.cli.cli.configure_logging", + fake_configure_logging, + ) + + run(f"{file}:server", transport="stdio") + + assert calls == [ + ("logging", "DEBUG"), + ("run", {"transport": "stdio"}), + ] + + def test_build_uv_command_minimal(): """Should emit core command when no extras specified.""" cmd = _build_uv_command("foo.py") diff --git a/tests/server/mcpserver/test_server.py b/tests/server/mcpserver/test_server.py index 3457ec944..5341446d3 100644 --- a/tests/server/mcpserver/test_server.py +++ b/tests/server/mcpserver/test_server.py @@ -1,4 +1,5 @@ import base64 +import logging from pathlib import Path from typing import Any from unittest.mock import AsyncMock, MagicMock, patch @@ -46,6 +47,18 @@ class TestServer: + def test_create_server_does_not_configure_application_logging(self, monkeypatch: pytest.MonkeyPatch): + calls: list[tuple[tuple[Any, ...], dict[str, Any]]] = [] + + def fake_basic_config(*args: Any, **kwargs: Any) -> None: + calls.append((args, kwargs)) + + monkeypatch.setattr(logging, "basicConfig", fake_basic_config) + + MCPServer("test") + + assert calls == [] + async def test_create_server(self): mcp = MCPServer( title="MCPServer Server", From 01f3c0a0065728fe496b3dba6af7d37230aa2314 Mon Sep 17 00:00:00 2001 From: Genmin Date: Fri, 1 May 2026 15:13:27 -0700 Subject: [PATCH 2/5] test: cover mcpserver logging utility --- .../mcpserver/utilities/test_logging.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 tests/server/mcpserver/utilities/test_logging.py diff --git a/tests/server/mcpserver/utilities/test_logging.py b/tests/server/mcpserver/utilities/test_logging.py new file mode 100644 index 000000000..0c35cbee8 --- /dev/null +++ b/tests/server/mcpserver/utilities/test_logging.py @@ -0,0 +1,34 @@ +import logging +from typing import Any + +import pytest + +from mcp.server.mcpserver.utilities.logging import configure_logging, get_logger + + +def test_get_logger_returns_named_logger(): + logger = get_logger("mcp.test") + + assert logger is logging.getLogger("mcp.test") + + +def test_configure_logging_uses_rich_handler(monkeypatch: pytest.MonkeyPatch): + calls: list[dict[str, Any]] = [] + + def fake_basic_config(**kwargs: Any) -> None: + calls.append(kwargs) + + monkeypatch.setattr(logging, "basicConfig", fake_basic_config) + + configure_logging("WARNING") + + handlers = calls[0]["handlers"] + assert calls == [ + { + "level": "WARNING", + "format": "%(message)s", + "handlers": handlers, + } + ] + assert len(handlers) == 1 + assert isinstance(handlers[0], logging.Handler) From 4bc0e488569dfd1692b027722dd4d78af79ba267 Mon Sep 17 00:00:00 2001 From: Genmin Date: Fri, 1 May 2026 15:25:57 -0700 Subject: [PATCH 3/5] test: close logging coverage gaps --- tests/cli/test_utils.py | 11 +++++++++++ tests/server/mcpserver/test_server.py | 10 +++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/tests/cli/test_utils.py b/tests/cli/test_utils.py index 0c9fab414..400194c9c 100644 --- a/tests/cli/test_utils.py +++ b/tests/cli/test_utils.py @@ -8,6 +8,7 @@ from mcp.cli.cli import ( # type: ignore[reportPrivateUsage] _build_uv_command, _get_npx_command, + _get_server_log_level, _parse_file_path, run, ) @@ -78,6 +79,16 @@ def fake_configure_logging(level: str) -> None: ] +def test_get_server_log_level_defaults_invalid_setting(): + class Settings: + log_level = "VERBOSE" + + class Server: + settings = Settings() + + assert _get_server_log_level(Server()) == "INFO" + + def test_build_uv_command_minimal(): """Should emit core command when no extras specified.""" cmd = _build_uv_command("foo.py") diff --git a/tests/server/mcpserver/test_server.py b/tests/server/mcpserver/test_server.py index 5341446d3..986c52c7f 100644 --- a/tests/server/mcpserver/test_server.py +++ b/tests/server/mcpserver/test_server.py @@ -48,16 +48,12 @@ class TestServer: def test_create_server_does_not_configure_application_logging(self, monkeypatch: pytest.MonkeyPatch): - calls: list[tuple[tuple[Any, ...], dict[str, Any]]] = [] - - def fake_basic_config(*args: Any, **kwargs: Any) -> None: - calls.append((args, kwargs)) - - monkeypatch.setattr(logging, "basicConfig", fake_basic_config) + basic_config = MagicMock() + monkeypatch.setattr(logging, "basicConfig", basic_config) MCPServer("test") - assert calls == [] + basic_config.assert_not_called() async def test_create_server(self): mcp = MCPServer( From 4ac189ed59d84b107ddd3e15e998099836de7d4c Mon Sep 17 00:00:00 2001 From: Genmin Date: Fri, 1 May 2026 15:41:54 -0700 Subject: [PATCH 4/5] test: narrow cli coverage pragma --- src/mcp/cli/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mcp/cli/cli.py b/src/mcp/cli/cli.py index 1de96cdfa..664fde607 100644 --- a/src/mcp/cli/cli.py +++ b/src/mcp/cli/cli.py @@ -324,7 +324,7 @@ def run( help="Transport protocol to use (stdio or sse)", ), ] = None, -) -> None: # pragma: no cover +) -> None: """Run an MCP server. The server can be specified in two ways: @@ -359,7 +359,7 @@ def run( server.run(**kwargs) - except Exception: + except Exception: # pragma: no cover logger.exception( "Failed to run server", extra={ From 99e235f763717b474e613be4963c09b1bd1a363c Mon Sep 17 00:00:00 2001 From: Genmin Date: Fri, 1 May 2026 15:44:35 -0700 Subject: [PATCH 5/5] test: cover cli run without transport --- tests/cli/test_utils.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/tests/cli/test_utils.py b/tests/cli/test_utils.py index 400194c9c..0fd489c96 100644 --- a/tests/cli/test_utils.py +++ b/tests/cli/test_utils.py @@ -44,7 +44,19 @@ def test_parse_file_exit_on_dir(tmp_path: Path): _parse_file_path(str(dir_path)) -def test_run_configures_logging_at_cli_entrypoint(monkeypatch: pytest.MonkeyPatch, tmp_path: Path): +@pytest.mark.parametrize( + ("transport", "expected_kwargs"), + [ + (None, {}), + ("stdio", {"transport": "stdio"}), + ], +) +def test_run_configures_logging_at_cli_entrypoint( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + transport: str | None, + expected_kwargs: dict[str, str], +): """The CLI owns process logging setup; importing or constructing the server does not.""" file = tmp_path / "server.py" file.write_text("server = object()") @@ -71,11 +83,11 @@ def fake_configure_logging(level: str) -> None: fake_configure_logging, ) - run(f"{file}:server", transport="stdio") + run(f"{file}:server", transport=transport) assert calls == [ ("logging", "DEBUG"), - ("run", {"transport": "stdio"}), + ("run", expected_kwargs), ]