diff --git a/src/mcp/server/mcpserver/utilities/logging.py b/src/mcp/server/mcpserver/utilities/logging.py index 04ca38853..b1233161d 100644 --- a/src/mcp/server/mcpserver/utilities/logging.py +++ b/src/mcp/server/mcpserver/utilities/logging.py @@ -3,6 +3,11 @@ import logging from typing import Literal +# Namespace logger for all MCP SDK logging. +# Per Python logging best practices, library code should only configure +# its own namespace logger, never the root logger. +_MCP_LOGGER_NAME = "mcp" + def get_logger(name: str) -> logging.Logger: """Get a logger nested under MCP namespace. @@ -21,19 +26,28 @@ def configure_logging( ) -> None: """Configure logging for MCP. + Configures only the ``mcp`` namespace logger so that application-level + logging configuration is not overridden. Per the Python logging docs, + library code should never call ``logging.basicConfig()`` or add handlers + to the root logger. + Args: level: The log level to use. """ - handlers: list[logging.Handler] = [] + mcp_logger = logging.getLogger(_MCP_LOGGER_NAME) + mcp_logger.setLevel(level) + + # Avoid adding duplicate handlers on repeated calls. + if mcp_logger.handlers: + return + try: from rich.console import Console from rich.logging import RichHandler - handlers.append(RichHandler(console=Console(stderr=True), rich_tracebacks=True)) + handler: logging.Handler = RichHandler(console=Console(stderr=True), rich_tracebacks=True) except ImportError: # pragma: no cover - pass - - if not handlers: # pragma: no cover - handlers.append(logging.StreamHandler()) + handler = logging.StreamHandler() + handler.setFormatter(logging.Formatter("%(message)s")) - logging.basicConfig(level=level, format="%(message)s", handlers=handlers) + mcp_logger.addHandler(handler) diff --git a/src/mcp/server/stdio.py b/src/mcp/server/stdio.py index e526bab56..508de3438 100644 --- a/src/mcp/server/stdio.py +++ b/src/mcp/server/stdio.py @@ -17,6 +17,7 @@ async def run_server(): ``` """ +import os import sys from contextlib import asynccontextmanager from io import TextIOWrapper @@ -38,10 +39,12 @@ async def stdio_server(stdin: anyio.AsyncFile[str] | None = None, stdout: anyio. # standard process handles. Encoding of stdin/stdout as text streams on # python is platform-dependent (Windows is particularly problematic), so we # re-wrap the underlying binary stream to ensure UTF-8. + # We duplicate the file descriptors via os.dup() to avoid closing the + # real sys.stdin/sys.stdout when the wrapped streams are closed. if not stdin: - stdin = anyio.wrap_file(TextIOWrapper(sys.stdin.buffer, encoding="utf-8")) + stdin = anyio.wrap_file(TextIOWrapper(os.fdopen(os.dup(sys.stdin.fileno()), "rb"), encoding="utf-8")) if not stdout: - stdout = anyio.wrap_file(TextIOWrapper(sys.stdout.buffer, encoding="utf-8")) + stdout = anyio.wrap_file(TextIOWrapper(os.fdopen(os.dup(sys.stdout.fileno()), "wb"), encoding="utf-8")) read_stream: MemoryObjectReceiveStream[SessionMessage | Exception] read_stream_writer: MemoryObjectSendStream[SessionMessage | Exception]