@@ -126,9 +126,8 @@ async def stdio_client(
126126 """
127127 command = _get_executable_command (server .command )
128128
129- # A child process inherits stderr as an OS file descriptor. Writer objects that
130- # have none (Jupyter's ipykernel stream, io.StringIO) cannot be inherited, so the
131- # server's stderr is piped back and forwarded into errlog by a reader task instead.
129+ # A child inherits stderr as a file descriptor; writers without one (ipykernel,
130+ # StringIO) need the server's stderr piped back and forwarded by a reader task.
132131 forward_stderr = _lacks_file_descriptor (errlog )
133132
134133 process = await _create_platform_compatible_process (
@@ -176,11 +175,7 @@ async def stdout_reader() -> None:
176175 logger .exception ("Reading from the MCP server's stdout failed mid-session" )
177176
178177 async def stderr_reader () -> None :
179- """Forwards the server's piped stderr into errlog.
180-
181- Only runs when errlog could not be inherited by the child; keeps the
182- server's diagnostics visible where a bare fd hand-off would have lost them.
183- """
178+ """Forwards the server's piped stderr into errlog when the child could not inherit it."""
184179 assert process .stderr , "Piped stderr is missing from the opened process"
185180
186181 stderr = TextReceiveStream (process .stderr , encoding = server .encoding , errors = "replace" )
@@ -191,12 +186,10 @@ async def stderr_reader() -> None:
191186 except (anyio .ClosedResourceError , anyio .BrokenResourceError , ConnectionError ):
192187 pass # the pipe went away with the process; nothing left to forward
193188 except ValueError :
194- # errlog was closed under us (a notebook cell finishing, a StringIO
195- # released). The server's own traffic must not fail over a lost log sink.
189+ # errlog closed under us; the session must not fail over a lost log sink.
196190 logger .debug ("Stopped forwarding the MCP server's stderr: the log stream was closed" )
197191 finally :
198- # Reaching EOF means the server's last diagnostics are in errlog, which
199- # is what shutdown waits on before cancelling this task.
192+ # EOF means the last diagnostics have landed; shutdown waits on this.
200193 stderr_done .set ()
201194
202195 async def stdin_writer () -> None :
@@ -227,8 +220,8 @@ async def shutdown() -> None:
227220 if flush_scope .cancelled_caught :
228221 await anyio .lowlevel .cancel_shielded_checkpoint () # resync coverage on 3.11 (gh-106749)
229222 await _stop_server_process (process )
230- # The server is dead, so its stderr pipe is at EOF with at most a buffer left;
231- # let the forwarder finish it before the task group's cancel takes the task out.
223+ # The dead server's stderr is at EOF with at most a buffer left; let the
224+ # forwarder drain it before the task group's cancel takes the task out.
232225 if forward_stderr :
233226 with anyio .move_on_after (_STDERR_DRAIN_TIMEOUT ):
234227 await stderr_done .wait ()
@@ -361,10 +354,8 @@ def _close_subprocess_transport(process: ServerProcess) -> None:
361354def _lacks_file_descriptor (errlog : TextIO ) -> bool :
362355 """Reports whether errlog has no OS file descriptor for a child to inherit.
363356
364- Jupyter's ipykernel stream and io.StringIO answer fileno() with an error;
365- real files, pipes and terminals return one. ipykernel does expose a
366- descriptor once fd capture is on, and that path already reaches the
367- notebook, so inheriting it stays correct.
357+ ipykernel with fd capture on does expose one, and that path already reaches
358+ the notebook, so inheriting it stays correct.
368359 """
369360 try :
370361 errlog .fileno ()
0 commit comments