@@ -70,7 +70,7 @@ class FallbackProcess:
7070 A fallback process wrapper for Windows to handle async I/O
7171 when using subprocess.Popen, which provides sync-only FileIO objects.
7272
73- This wraps stdin and stdout into async-compatible
73+ This wraps stdin, stdout, and stderr into async-compatible
7474 streams (FileReadStream, FileWriteStream),
7575 so that MCP clients expecting async streams can work properly.
7676 """
@@ -79,10 +79,12 @@ def __init__(self, popen_obj: subprocess.Popen[bytes]):
7979 self .popen : subprocess .Popen [bytes ] = popen_obj
8080 self .stdin_raw = popen_obj .stdin # type: ignore[assignment]
8181 self .stdout_raw = popen_obj .stdout # type: ignore[assignment]
82- self .stderr = popen_obj .stderr # type: ignore[assignment]
82+ self .stderr_raw = popen_obj .stderr # type: ignore[assignment]
8383
8484 self .stdin = FileWriteStream (cast (BinaryIO , self .stdin_raw )) if self .stdin_raw else None
8585 self .stdout = FileReadStream (cast (BinaryIO , self .stdout_raw )) if self .stdout_raw else None
86+ # Wrap stderr in async stream if it's piped (for Jupyter compatibility)
87+ self .stderr = FileReadStream (cast (BinaryIO , self .stderr_raw )) if self .stderr_raw else None
8688
8789 async def __aenter__ (self ):
8890 """Support async context manager entry."""
@@ -103,12 +105,14 @@ async def __aexit__(
103105 await self .stdin .aclose ()
104106 if self .stdout :
105107 await self .stdout .aclose ()
108+ if self .stderr :
109+ await self .stderr .aclose ()
106110 if self .stdin_raw :
107111 self .stdin_raw .close ()
108112 if self .stdout_raw :
109113 self .stdout_raw .close ()
110- if self .stderr :
111- self .stderr .close ()
114+ if self .stderr_raw :
115+ self .stderr_raw .close ()
112116
113117 async def wait (self ):
114118 """Async wait for process completion."""
@@ -139,6 +143,7 @@ async def create_windows_process(
139143 env : dict [str , str ] | None = None ,
140144 errlog : TextIO | None = sys .stderr ,
141145 cwd : Path | str | None = None ,
146+ pipe_stderr : bool = False ,
142147) -> Process | FallbackProcess :
143148 """
144149 Creates a subprocess in a Windows-compatible way with Job Object support.
@@ -155,15 +160,20 @@ async def create_windows_process(
155160 command (str): The executable to run
156161 args (list[str]): List of command line arguments
157162 env (dict[str, str] | None): Environment variables
158- errlog (TextIO | None): Where to send stderr output (defaults to sys.stderr)
163+ errlog (TextIO | None): Where to send stderr output (defaults to sys.stderr).
164+ Only used when pipe_stderr is False.
159165 cwd (Path | str | None): Working directory for the subprocess
166+ pipe_stderr (bool): If True, pipe stderr instead of redirecting to errlog.
167+ This allows async reading of stderr for Jupyter compatibility.
160168
161169 Returns:
162170 Process | FallbackProcess: Async-compatible subprocess with stdin and stdout streams
163171 """
164172 job = _create_job_object ()
165173 process = None
166174
175+ stderr_target = subprocess .PIPE if pipe_stderr else errlog
176+
167177 try :
168178 # First try using anyio with Windows-specific flags to hide console window
169179 process = await anyio .open_process (
@@ -173,18 +183,18 @@ async def create_windows_process(
173183 creationflags = subprocess .CREATE_NO_WINDOW # type: ignore
174184 if hasattr (subprocess , "CREATE_NO_WINDOW" )
175185 else 0 ,
176- stderr = errlog ,
186+ stderr = stderr_target ,
177187 cwd = cwd ,
178188 )
179189 except NotImplementedError :
180190 # If Windows doesn't support async subprocess creation, use fallback
181- process = await _create_windows_fallback_process (command , args , env , errlog , cwd )
191+ process = await _create_windows_fallback_process (command , args , env , errlog , cwd , pipe_stderr = pipe_stderr )
182192 except Exception :
183193 # Try again without creation flags
184194 process = await anyio .open_process (
185195 [command , * args ],
186196 env = env ,
187- stderr = errlog ,
197+ stderr = stderr_target ,
188198 cwd = cwd ,
189199 )
190200
@@ -198,19 +208,30 @@ async def _create_windows_fallback_process(
198208 env : dict [str , str ] | None = None ,
199209 errlog : TextIO | None = sys .stderr ,
200210 cwd : Path | str | None = None ,
211+ pipe_stderr : bool = False ,
201212) -> FallbackProcess :
202213 """
203214 Create a subprocess using subprocess.Popen as a fallback when anyio fails.
204215
205216 This function wraps the sync subprocess.Popen in an async-compatible interface.
217+
218+ Args:
219+ command: The executable to run
220+ args: List of command line arguments
221+ env: Environment variables
222+ errlog: Where to send stderr output (only used when pipe_stderr is False)
223+ cwd: Working directory for the subprocess
224+ pipe_stderr: If True, pipe stderr instead of redirecting to errlog
206225 """
226+ stderr_target = subprocess .PIPE if pipe_stderr else errlog
227+
207228 try :
208229 # Try launching with creationflags to avoid opening a new console window
209230 popen_obj = subprocess .Popen (
210231 [command , * args ],
211232 stdin = subprocess .PIPE ,
212233 stdout = subprocess .PIPE ,
213- stderr = errlog ,
234+ stderr = stderr_target ,
214235 env = env ,
215236 cwd = cwd ,
216237 bufsize = 0 , # Unbuffered output
@@ -222,7 +243,7 @@ async def _create_windows_fallback_process(
222243 [command , * args ],
223244 stdin = subprocess .PIPE ,
224245 stdout = subprocess .PIPE ,
225- stderr = errlog ,
246+ stderr = stderr_target ,
226247 env = env ,
227248 cwd = cwd ,
228249 bufsize = 0 ,
0 commit comments