From b46ebd611e3fa8824342d39029ebb88eb657604b Mon Sep 17 00:00:00 2001 From: g97iulio1609 Date: Sat, 28 Feb 2026 04:37:02 +0100 Subject: [PATCH 1/3] fix: prevent stdio transport from closing real stdin/stdout When using transport='stdio', the server wraps sys.stdin.buffer and sys.stdout.buffer in TextIOWrapper and anyio.AsyncFile. When these wrappers are garbage collected after the server exits, they close the underlying sys.stdin/sys.stdout, causing subsequent I/O operations to fail with ValueError. Use os.dup() to duplicate the file descriptors before wrapping them, so closing the wrapped streams only closes the duplicates, leaving the original stdin/stdout intact. Fixes #1933 --- src/mcp/server/stdio.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/mcp/server/stdio.py b/src/mcp/server/stdio.py index e526bab56..77c6fb067 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,16 @@ 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] From 76362c335bea1f82c5c09273ef6f1b606ad692c8 Mon Sep 17 00:00:00 2001 From: g97iulio1609 Date: Sat, 28 Feb 2026 04:38:07 +0100 Subject: [PATCH 2/3] fix: configure only mcp namespace logger instead of root logger FastMCP.__init__() calls configure_logging() which previously called logging.basicConfig(), configuring the root logger with handlers and level. This violates Python logging best practices: library code should never configure the root logger, as that is the prerogative of the application developer. Replace logging.basicConfig() with targeted configuration of the 'mcp' namespace logger only. This ensures: - MCP SDK logs still work out of the box for quickstart scripts - Application-level logging configuration is not overridden - No duplicate handlers on repeated FastMCP instantiations References: - https://docs.python.org/3/howto/logging.html#configuring-logging-for-a-library Fixes #1656 --- src/mcp/server/mcpserver/utilities/logging.py | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/mcp/server/mcpserver/utilities/logging.py b/src/mcp/server/mcpserver/utilities/logging.py index 04ca38853..6ac459da9 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,30 @@ 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) From a223218316a748d7a74ed197f5d4331afcb7f5d4 Mon Sep 17 00:00:00 2001 From: g97iulio1609 Date: Sat, 28 Feb 2026 14:05:56 +0100 Subject: [PATCH 3/3] style: apply ruff format --- src/mcp/server/mcpserver/utilities/logging.py | 4 +--- src/mcp/server/stdio.py | 8 ++------ 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/mcp/server/mcpserver/utilities/logging.py b/src/mcp/server/mcpserver/utilities/logging.py index 6ac459da9..b1233161d 100644 --- a/src/mcp/server/mcpserver/utilities/logging.py +++ b/src/mcp/server/mcpserver/utilities/logging.py @@ -45,9 +45,7 @@ def configure_logging( from rich.console import Console from rich.logging import RichHandler - handler: logging.Handler = RichHandler( - console=Console(stderr=True), rich_tracebacks=True - ) + handler: logging.Handler = RichHandler(console=Console(stderr=True), rich_tracebacks=True) except ImportError: # pragma: no cover handler = logging.StreamHandler() handler.setFormatter(logging.Formatter("%(message)s")) diff --git a/src/mcp/server/stdio.py b/src/mcp/server/stdio.py index 77c6fb067..508de3438 100644 --- a/src/mcp/server/stdio.py +++ b/src/mcp/server/stdio.py @@ -42,13 +42,9 @@ async def stdio_server(stdin: anyio.AsyncFile[str] | None = None, stdout: anyio. # 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(os.fdopen(os.dup(sys.stdin.fileno()), "rb"), 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(os.fdopen(os.dup(sys.stdout.fileno()), "wb"), 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]