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..e31bcb930d --- /dev/null +++ b/tests/server/mcpserver/test_configure_logging.py @@ -0,0 +1,35 @@ +from unittest.mock import patch + +from mcp.server.mcpserver import MCPServer + + +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") + + 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. + + See: https://github.com/modelcontextprotocol/python-sdk/issues/1656 + """ + with patch("mcp.server.mcpserver.server._configure_logging") as spy: + MCPServer("test-server", configure_logging=False) + + spy.assert_not_called() + + 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") + + spy.assert_called_once_with("DEBUG") \ No newline at end of file