Skip to content

Commit d01d0e9

Browse files
committed
Add configure_logging opt-out to MCPServer (#1656)
1 parent d287c98 commit d01d0e9

2 files changed

Lines changed: 69 additions & 3 deletions

File tree

src/mcp/server/mcpserver/server.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@
8282
)
8383
from mcp.server.mcpserver.tools import Tool, ToolManager
8484
from mcp.server.mcpserver.utilities.context_injection import find_context_parameter
85-
from mcp.server.mcpserver.utilities.logging import configure_logging, get_logger
85+
from mcp.server.mcpserver.utilities.logging import configure_logging as _configure_logging
86+
from mcp.server.mcpserver.utilities.logging import get_logger
8687
from mcp.server.request_state import RequestStateBoundary, RequestStateSecurity
8788
from mcp.server.sse import SseServerTransport
8889
from mcp.server.stdio import stdio_server
@@ -174,6 +175,7 @@ def __init__(
174175
extensions: Sequence[Extension] | None = None,
175176
debug: bool = False,
176177
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO",
178+
configure_logging: bool = True,
177179
warn_on_duplicate_resources: bool = True,
178180
warn_on_duplicate_tools: bool = True,
179181
warn_on_duplicate_prompts: bool = True,
@@ -256,8 +258,10 @@ def __init__(
256258
self._token_verifier = ProviderTokenVerifier(auth_server_provider)
257259
self._custom_starlette_routes: list[Route] = []
258260

259-
# Configure logging
260-
configure_logging(self.settings.log_level)
261+
# Configure logging (opt-out via configure_logging=False for applications
262+
# that embed MCPServer and manage their own logging setup; see #1656)
263+
if configure_logging:
264+
_configure_logging(self.settings.log_level)
261265

262266
self._extensions: list[Extension] = []
263267
for extension in extensions or ():
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import logging
2+
3+
import pytest
4+
5+
from mcp.server.mcpserver import MCPServer
6+
7+
8+
@pytest.fixture
9+
def clean_root_logger():
10+
"""Save and restore the root logger's handlers/level around each test.
11+
12+
`MCPServer` can mutate the root logger via `logging.basicConfig()`, so
13+
tests that exercise this behavior must not leak handlers into other
14+
tests (or into pytest's own log capturing, which itself attaches a
15+
handler to the root logger -- hence comparing against a baseline
16+
snapshot below, rather than assuming an empty handler list).
17+
"""
18+
root = logging.getLogger()
19+
original_handlers = list(root.handlers)
20+
original_level = root.level
21+
yield root
22+
root.handlers.clear()
23+
root.handlers.extend(original_handlers)
24+
root.setLevel(original_level)
25+
26+
27+
class TestConfigureLogging:
28+
def test_default_does_not_override_existing_handlers(self, clean_root_logger):
29+
"""If an application has already configured the root logger before
30+
creating an `MCPServer`, the default behavior must not replace it.
31+
32+
This matches the standard library's own `logging.basicConfig()`
33+
contract: it is a no-op if the root logger already has handlers.
34+
"""
35+
baseline = list(clean_root_logger.handlers)
36+
app_handler = logging.StreamHandler()
37+
clean_root_logger.addHandler(app_handler)
38+
39+
MCPServer("test-server")
40+
41+
assert clean_root_logger.handlers == [*baseline, app_handler]
42+
43+
def test_configure_logging_false_leaves_root_logger_untouched(self, clean_root_logger):
44+
"""`configure_logging=False` opts a server out of touching the root
45+
logger entirely, so an application can safely configure its own
46+
logging before or after constructing the server.
47+
48+
See: https://github.com/modelcontextprotocol/python-sdk/issues/1656
49+
"""
50+
baseline = list(clean_root_logger.handlers)
51+
52+
MCPServer("test-server", configure_logging=False)
53+
assert clean_root_logger.handlers == baseline
54+
55+
# The application's own logging setup, performed afterwards, must
56+
# take effect (i.e. must not have been pre-empted by the server).
57+
# Clear first so `basicConfig` (a no-op if handlers already exist)
58+
# is guaranteed to actually apply the new format.
59+
clean_root_logger.handlers.clear()
60+
logging.basicConfig(format="MYAPP: %(message)s")
61+
assert len(clean_root_logger.handlers) == 1
62+
assert clean_root_logger.handlers[0].formatter._fmt == "MYAPP: %(message)s"

0 commit comments

Comments
 (0)