From 1df66bfea1880ab8cabd0a2570304da48dfccf23 Mon Sep 17 00:00:00 2001 From: SamSi0322 Date: Fri, 10 Apr 2026 10:17:47 -0400 Subject: [PATCH] Allow subprocess constants (e.g. DEVNULL) for stdio errlog parameter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Widen the `errlog` type on `stdio_client()` and the platform process helpers from `TextIO` to `TextIO | int` so callers can pass `subprocess.DEVNULL`, `subprocess.PIPE`, or other integer constants accepted by `subprocess.Popen` and `anyio.open_process`. Previously, passing `subprocess.DEVNULL` required a type: ignore or a custom wrapper. The underlying process APIs already support int values for stderr — this change surfaces that capability in the public signature. Github-Issue: #1806 Reported-by: Seyed Sajad Kahani --- src/mcp/client/stdio.py | 4 ++-- src/mcp/os/win32/utilities.py | 6 +++--- tests/client/test_stdio.py | 40 +++++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/mcp/client/stdio.py b/src/mcp/client/stdio.py index 902dc8576..531ab480b 100644 --- a/src/mcp/client/stdio.py +++ b/src/mcp/client/stdio.py @@ -102,7 +102,7 @@ class StdioServerParameters(BaseModel): @asynccontextmanager -async def stdio_client(server: StdioServerParameters, errlog: TextIO = sys.stderr): +async def stdio_client(server: StdioServerParameters, errlog: TextIO | int = sys.stderr): """Client transport for stdio: this will connect to a server by spawning a process and communicating with it over stdin/stdout. """ @@ -230,7 +230,7 @@ async def _create_platform_compatible_process( command: str, args: list[str], env: dict[str, str] | None = None, - errlog: TextIO = sys.stderr, + errlog: TextIO | int = sys.stderr, cwd: Path | str | None = None, ): """Creates a subprocess in a platform-compatible way. diff --git a/src/mcp/os/win32/utilities.py b/src/mcp/os/win32/utilities.py index 6f68405f7..aeb5e8e50 100644 --- a/src/mcp/os/win32/utilities.py +++ b/src/mcp/os/win32/utilities.py @@ -138,7 +138,7 @@ async def create_windows_process( command: str, args: list[str], env: dict[str, str] | None = None, - errlog: TextIO | None = sys.stderr, + errlog: TextIO | int | None = sys.stderr, cwd: Path | str | None = None, ) -> Process | FallbackProcess: """Creates a subprocess in a Windows-compatible way with Job Object support. @@ -155,7 +155,7 @@ async def create_windows_process( command (str): The executable to run args (list[str]): List of command line arguments env (dict[str, str] | None): Environment variables - errlog (TextIO | None): Where to send stderr output (defaults to sys.stderr) + errlog (TextIO | int | None): Where to send stderr output (defaults to sys.stderr) cwd (Path | str | None): Working directory for the subprocess Returns: @@ -196,7 +196,7 @@ async def _create_windows_fallback_process( command: str, args: list[str], env: dict[str, str] | None = None, - errlog: TextIO | None = sys.stderr, + errlog: TextIO | int | None = sys.stderr, cwd: Path | str | None = None, ) -> FallbackProcess: """Create a subprocess using subprocess.Popen as a fallback when anyio fails. diff --git a/tests/client/test_stdio.py b/tests/client/test_stdio.py index 06e2cba4b..ee5e9e50a 100644 --- a/tests/client/test_stdio.py +++ b/tests/client/test_stdio.py @@ -1,5 +1,6 @@ import errno import shutil +import subprocess import sys import textwrap import time @@ -70,6 +71,45 @@ async def test_stdio_client(): assert read_messages[1] == JSONRPCResponse(jsonrpc="2.0", id=2, result={}) +@pytest.mark.anyio +async def test_stdio_client_devnull_errlog(): + """Test that stdio_client accepts subprocess.DEVNULL for errlog, + allowing callers to suppress stderr output from the child process. + + Regression test for https://github.com/modelcontextprotocol/python-sdk/issues/1806 + """ + # A script that writes to stderr then echoes stdin to stdout + script_content = textwrap.dedent( + """ + import sys + sys.stderr.write("this goes to devnull\\n") + sys.stderr.flush() + for line in sys.stdin: + sys.stdout.write(line) + sys.stdout.flush() + """ + ) + + server_params = StdioServerParameters( + command=sys.executable, + args=["-c", script_content], + ) + + async with stdio_client(server_params, errlog=subprocess.DEVNULL) as (read_stream, write_stream): + message = JSONRPCRequest(jsonrpc="2.0", id=1, method="ping") + session_message = SessionMessage(message) + + async with write_stream: + await write_stream.send(session_message) + + async with read_stream: + async for received in read_stream: + if isinstance(received, Exception): # pragma: no cover + raise received + assert received.message == message + break + + @pytest.mark.anyio async def test_stdio_client_bad_path(): """Check that the connection doesn't hang if process errors."""