Skip to content

Commit 1df66bf

Browse files
committed
Allow subprocess constants (e.g. DEVNULL) for stdio errlog parameter
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
1 parent f27d2aa commit 1df66bf

File tree

3 files changed

+45
-5
lines changed

3 files changed

+45
-5
lines changed

src/mcp/client/stdio.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ class StdioServerParameters(BaseModel):
102102

103103

104104
@asynccontextmanager
105-
async def stdio_client(server: StdioServerParameters, errlog: TextIO = sys.stderr):
105+
async def stdio_client(server: StdioServerParameters, errlog: TextIO | int = sys.stderr):
106106
"""Client transport for stdio: this will connect to a server by spawning a
107107
process and communicating with it over stdin/stdout.
108108
"""
@@ -230,7 +230,7 @@ async def _create_platform_compatible_process(
230230
command: str,
231231
args: list[str],
232232
env: dict[str, str] | None = None,
233-
errlog: TextIO = sys.stderr,
233+
errlog: TextIO | int = sys.stderr,
234234
cwd: Path | str | None = None,
235235
):
236236
"""Creates a subprocess in a platform-compatible way.

src/mcp/os/win32/utilities.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ async def create_windows_process(
138138
command: str,
139139
args: list[str],
140140
env: dict[str, str] | None = None,
141-
errlog: TextIO | None = sys.stderr,
141+
errlog: TextIO | int | None = sys.stderr,
142142
cwd: Path | str | None = None,
143143
) -> Process | FallbackProcess:
144144
"""Creates a subprocess in a Windows-compatible way with Job Object support.
@@ -155,7 +155,7 @@ async def create_windows_process(
155155
command (str): The executable to run
156156
args (list[str]): List of command line arguments
157157
env (dict[str, str] | None): Environment variables
158-
errlog (TextIO | None): Where to send stderr output (defaults to sys.stderr)
158+
errlog (TextIO | int | None): Where to send stderr output (defaults to sys.stderr)
159159
cwd (Path | str | None): Working directory for the subprocess
160160
161161
Returns:
@@ -196,7 +196,7 @@ async def _create_windows_fallback_process(
196196
command: str,
197197
args: list[str],
198198
env: dict[str, str] | None = None,
199-
errlog: TextIO | None = sys.stderr,
199+
errlog: TextIO | int | None = sys.stderr,
200200
cwd: Path | str | None = None,
201201
) -> FallbackProcess:
202202
"""Create a subprocess using subprocess.Popen as a fallback when anyio fails.

tests/client/test_stdio.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import errno
22
import shutil
3+
import subprocess
34
import sys
45
import textwrap
56
import time
@@ -70,6 +71,45 @@ async def test_stdio_client():
7071
assert read_messages[1] == JSONRPCResponse(jsonrpc="2.0", id=2, result={})
7172

7273

74+
@pytest.mark.anyio
75+
async def test_stdio_client_devnull_errlog():
76+
"""Test that stdio_client accepts subprocess.DEVNULL for errlog,
77+
allowing callers to suppress stderr output from the child process.
78+
79+
Regression test for https://github.com/modelcontextprotocol/python-sdk/issues/1806
80+
"""
81+
# A script that writes to stderr then echoes stdin to stdout
82+
script_content = textwrap.dedent(
83+
"""
84+
import sys
85+
sys.stderr.write("this goes to devnull\\n")
86+
sys.stderr.flush()
87+
for line in sys.stdin:
88+
sys.stdout.write(line)
89+
sys.stdout.flush()
90+
"""
91+
)
92+
93+
server_params = StdioServerParameters(
94+
command=sys.executable,
95+
args=["-c", script_content],
96+
)
97+
98+
async with stdio_client(server_params, errlog=subprocess.DEVNULL) as (read_stream, write_stream):
99+
message = JSONRPCRequest(jsonrpc="2.0", id=1, method="ping")
100+
session_message = SessionMessage(message)
101+
102+
async with write_stream:
103+
await write_stream.send(session_message)
104+
105+
async with read_stream:
106+
async for received in read_stream:
107+
if isinstance(received, Exception): # pragma: no cover
108+
raise received
109+
assert received.message == message
110+
break
111+
112+
73113
@pytest.mark.anyio
74114
async def test_stdio_client_bad_path():
75115
"""Check that the connection doesn't hang if process errors."""

0 commit comments

Comments
 (0)