From d01d0e9039791da8ba899305a0b883c3d57133a7 Mon Sep 17 00:00:00 2001 From: Gautam N Chipkar Date: Tue, 7 Jul 2026 13:07:29 +0530 Subject: [PATCH 1/2] Add configure_logging opt-out to MCPServer (#1656) --- src/mcp/server/mcpserver/server.py | 10 ++- .../mcpserver/test_configure_logging.py | 62 +++++++++++++++++++ 2 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 tests/server/mcpserver/test_configure_logging.py diff --git a/src/mcp/server/mcpserver/server.py b/src/mcp/server/mcpserver/server.py index d933e82d55..c1eb016e0c 100644 --- a/src/mcp/server/mcpserver/server.py +++ b/src/mcp/server/mcpserver/server.py @@ -82,7 +82,8 @@ ) 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 configure_logging as _configure_logging +from mcp.server.mcpserver.utilities.logging import get_logger from mcp.server.request_state import RequestStateBoundary, RequestStateSecurity from mcp.server.sse import SseServerTransport from mcp.server.stdio import stdio_server @@ -174,6 +175,7 @@ def __init__( extensions: Sequence[Extension] | None = None, debug: bool = False, log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO", + configure_logging: bool = True, warn_on_duplicate_resources: bool = True, warn_on_duplicate_tools: bool = True, warn_on_duplicate_prompts: bool = True, @@ -256,8 +258,10 @@ def __init__( self._token_verifier = ProviderTokenVerifier(auth_server_provider) self._custom_starlette_routes: list[Route] = [] - # Configure logging - configure_logging(self.settings.log_level) + # Configure logging (opt-out via configure_logging=False for applications + # that embed MCPServer and manage their own logging setup; see #1656) + if configure_logging: + _configure_logging(self.settings.log_level) self._extensions: list[Extension] = [] for extension in extensions or (): diff --git a/tests/server/mcpserver/test_configure_logging.py b/tests/server/mcpserver/test_configure_logging.py new file mode 100644 index 0000000000..42800c1b9c --- /dev/null +++ b/tests/server/mcpserver/test_configure_logging.py @@ -0,0 +1,62 @@ +import logging + +import pytest + +from mcp.server.mcpserver import MCPServer + + +@pytest.fixture +def clean_root_logger(): + """Save and restore the root logger's handlers/level around each test. + + `MCPServer` can mutate the root logger via `logging.basicConfig()`, so + tests that exercise this behavior must not leak handlers into other + tests (or into pytest's own log capturing, which itself attaches a + handler to the root logger -- hence comparing against a baseline + snapshot below, rather than assuming an empty handler list). + """ + root = logging.getLogger() + original_handlers = list(root.handlers) + original_level = root.level + yield root + root.handlers.clear() + root.handlers.extend(original_handlers) + root.setLevel(original_level) + + +class TestConfigureLogging: + def test_default_does_not_override_existing_handlers(self, clean_root_logger): + """If an application has already configured the root logger before + creating an `MCPServer`, the default behavior must not replace it. + + This matches the standard library's own `logging.basicConfig()` + contract: it is a no-op if the root logger already has handlers. + """ + baseline = list(clean_root_logger.handlers) + app_handler = logging.StreamHandler() + clean_root_logger.addHandler(app_handler) + + MCPServer("test-server") + + assert clean_root_logger.handlers == [*baseline, app_handler] + + def test_configure_logging_false_leaves_root_logger_untouched(self, clean_root_logger): + """`configure_logging=False` opts a server out of touching the root + logger entirely, so an application can safely configure its own + logging before or after constructing the server. + + See: https://github.com/modelcontextprotocol/python-sdk/issues/1656 + """ + baseline = list(clean_root_logger.handlers) + + MCPServer("test-server", configure_logging=False) + assert clean_root_logger.handlers == baseline + + # The application's own logging setup, performed afterwards, must + # take effect (i.e. must not have been pre-empted by the server). + # Clear first so `basicConfig` (a no-op if handlers already exist) + # is guaranteed to actually apply the new format. + clean_root_logger.handlers.clear() + logging.basicConfig(format="MYAPP: %(message)s") + assert len(clean_root_logger.handlers) == 1 + assert clean_root_logger.handlers[0].formatter._fmt == "MYAPP: %(message)s" From 7bd4c4433efd4d070bb4d2e042541b5af52fa416 Mon Sep 17 00:00:00 2001 From: Gautam N Chipkar Date: Tue, 7 Jul 2026 13:20:28 +0530 Subject: [PATCH 2/2] Address review: assert on _configure_logging call directly, not root logger state --- .../mcpserver/test_configure_logging.py | 75 ++++++------------- 1 file changed, 24 insertions(+), 51 deletions(-) diff --git a/tests/server/mcpserver/test_configure_logging.py b/tests/server/mcpserver/test_configure_logging.py index 42800c1b9c..e31bcb930d 100644 --- a/tests/server/mcpserver/test_configure_logging.py +++ b/tests/server/mcpserver/test_configure_logging.py @@ -1,62 +1,35 @@ -import logging - -import pytest +from unittest.mock import patch from mcp.server.mcpserver import MCPServer -@pytest.fixture -def clean_root_logger(): - """Save and restore the root logger's handlers/level around each test. - - `MCPServer` can mutate the root logger via `logging.basicConfig()`, so - tests that exercise this behavior must not leak handlers into other - tests (or into pytest's own log capturing, which itself attaches a - handler to the root logger -- hence comparing against a baseline - snapshot below, rather than assuming an empty handler list). - """ - root = logging.getLogger() - original_handlers = list(root.handlers) - original_level = root.level - yield root - root.handlers.clear() - root.handlers.extend(original_handlers) - root.setLevel(original_level) +class TestConfigureLogging: + def test_default_calls_configure_logging(self): + """By default, `MCPServer` configures logging on construction (the + pre-existing behavior, preserved for backwards compatibility).""" + with patch("mcp.server.mcpserver.server._configure_logging") as spy: + MCPServer("test-server") + spy.assert_called_once_with("INFO") -class TestConfigureLogging: - def test_default_does_not_override_existing_handlers(self, clean_root_logger): - """If an application has already configured the root logger before - creating an `MCPServer`, the default behavior must not replace it. + def test_configure_logging_false_skips_configuration(self): + """`configure_logging=False` opts a server out of calling + `_configure_logging` (and therefore `logging.basicConfig()`) + entirely, so an application can safely manage its own logging + setup without the server racing to configure the root logger + first. - This matches the standard library's own `logging.basicConfig()` - contract: it is a no-op if the root logger already has handlers. + See: https://github.com/modelcontextprotocol/python-sdk/issues/1656 """ - baseline = list(clean_root_logger.handlers) - app_handler = logging.StreamHandler() - clean_root_logger.addHandler(app_handler) - - MCPServer("test-server") + with patch("mcp.server.mcpserver.server._configure_logging") as spy: + MCPServer("test-server", configure_logging=False) - assert clean_root_logger.handlers == [*baseline, app_handler] + spy.assert_not_called() - def test_configure_logging_false_leaves_root_logger_untouched(self, clean_root_logger): - """`configure_logging=False` opts a server out of touching the root - logger entirely, so an application can safely configure its own - logging before or after constructing the server. + def test_configure_logging_true_still_calls_configure_logging(self): + """Explicitly passing `configure_logging=True` behaves the same as + the default (sanity check for the new parameter's positive case).""" + with patch("mcp.server.mcpserver.server._configure_logging") as spy: + MCPServer("test-server", configure_logging=True, log_level="DEBUG") - See: https://github.com/modelcontextprotocol/python-sdk/issues/1656 - """ - baseline = list(clean_root_logger.handlers) - - MCPServer("test-server", configure_logging=False) - assert clean_root_logger.handlers == baseline - - # The application's own logging setup, performed afterwards, must - # take effect (i.e. must not have been pre-empted by the server). - # Clear first so `basicConfig` (a no-op if handlers already exist) - # is guaranteed to actually apply the new format. - clean_root_logger.handlers.clear() - logging.basicConfig(format="MYAPP: %(message)s") - assert len(clean_root_logger.handlers) == 1 - assert clean_root_logger.handlers[0].formatter._fmt == "MYAPP: %(message)s" + spy.assert_called_once_with("DEBUG") \ No newline at end of file