@@ -216,30 +216,26 @@ async def test_stdio_server_takes_stdin_off_the_descriptor_table_while_serving(
216216async def test_stdio_server_reads_stdin_in_place_when_descriptor_isolation_fails (
217217 failing_call : str , monkeypatch : pytest .MonkeyPatch
218218) -> None :
219- """A descriptor-table failure while claiming stdin degrades to reading sys.stdin in place.
219+ """A descriptor failure while claiming stdin degrades to reading sys.stdin in place.
220220
221- SDK-defined behavior: isolation is best-effort; the dup2 variant fails after the private duplicate exists.
221+ SDK-defined behavior: isolation is best-effort; when duplicating fd 0 or diverting
222+ it fails, the transport serves over the original stdin exactly as v1 did.
222223 """
223224 request = JSONRPCRequest (jsonrpc = "2.0" , id = 1 , method = "ping" )
224225 with _pipe_planted_on_fd0 (monkeypatch ) as (in_r , in_w ):
225226 os .write (in_w , _frame (request ))
226227 os .close (in_w )
227228 monkeypatch .setattr (sys , "stdout" , TextIOWrapper (io .BytesIO (), encoding = "utf-8" ))
228229
229- # Injectors fire once, then pass through: pytest capture also calls os.dup/os.dup2,
230- # and a still-armed injector detonating there corrupts every later test's capture.
231230 if failing_call == "dup" :
232- real_dup = os .dup
233- armed = [True ]
234231
235- def failing_dup (fd : int ) -> int :
236- if fd == 0 and armed [0 ]:
237- armed [0 ] = False
238- raise OSError ("injected descriptor failure" )
239- return real_dup (fd )
232+ def failing_dup_above_std (fd : int ) -> int :
233+ raise OSError ("injected descriptor failure" )
240234
241- monkeypatch .setattr (os , "dup " , failing_dup )
235+ monkeypatch .setattr ("mcp.server.stdio._dup_above_std " , failing_dup_above_std )
242236 else :
237+ # Fires once at the divert, then passes through: pytest's capture
238+ # machinery also calls os.dup2 at phase transitions.
243239 real_dup2 = os .dup2
244240 armed = [True ]
245241
@@ -256,12 +252,6 @@ def failing_dup2(fd: int, fd2: int, inheritable: bool = True) -> int:
256252 async with read_stream : # pragma: no branch
257253 # Isolation was skipped: fd 0 is still the protocol pipe.
258254 assert os .path .sameopenfile (0 , in_r )
259- # In-place transports still own the stream: a second one is refused.
260- with pytest .raises (RuntimeError , match = "already claimed fd 0" ):
261- async with stdio_server ():
262- pytest .fail ("unreachable" ) # pragma: no cover
263- # The spent injector passes calls through untouched.
264- os .close (os .dup (0 ))
265255 received = await read_stream .receive ()
266256 assert isinstance (received , SessionMessage )
267257 assert received .message == request
@@ -278,8 +268,7 @@ async def test_stdio_server_exits_cleanly_when_the_stdin_restore_fails(
278268 still-diverted fd must refuse later transports rather than serve them the diversion.
279269 """
280270 request = JSONRPCRequest (jsonrpc = "2.0" , id = 1 , method = "ping" )
281- fresh_claims : set [int ] = set ()
282- monkeypatch .setattr ("mcp.server.stdio._claimed" , fresh_claims ) # this test leaves fd 0 claimed
271+ monkeypatch .setattr ("mcp.server.stdio._claims" , {}) # this test leaves fd 0 claimed
283272 with _pipe_planted_on_fd0 (monkeypatch ) as (_ , in_w ):
284273 os .write (in_w , _frame (request ))
285274 os .close (in_w )
@@ -374,6 +363,8 @@ def failing_dup(fd: int) -> int:
374363 with anyio .fail_after (5 ):
375364 async with stdio_server (stdin = anyio .AsyncFile (io .StringIO ())) as (read_stream , write_stream ):
376365 read_stream .close ()
366+ # The spent injector passes later duplications through untouched.
367+ os .close (os .dup (0 ))
377368 devnull_probe = os .open (os .devnull , os .O_WRONLY )
378369 try :
379370 assert os .path .sameopenfile (1 , devnull_probe )
@@ -443,12 +434,14 @@ async def test_a_refused_claim_releases_the_stream_it_already_took(
443434
444435
445436@pytest .mark .anyio
446- async def test_stdio_server_serves_in_place_when_the_standard_descriptors_are_incomplete (
437+ async def test_the_claim_engages_even_when_stderr_is_closed (
447438 monkeypatch : pytest .MonkeyPatch ,
448439) -> None :
449- """A process missing a standard descriptor is served in place, without surgery .
440+ """A process missing fd 2 still gets full isolation .
450441
451- With fd 2 closed, a duplicate could land in the standard range: the transport must not touch the table.
442+ SDK-defined behavior: the wire duplicate is allocated above the standard range
443+ atomically, so a hole in the descriptor table cannot capture it; the stdout
444+ diversion falls back to the null device.
452445 """
453446 request = JSONRPCRequest (jsonrpc = "2.0" , id = 1 , method = "ping" )
454447 response = JSONRPCResponse (jsonrpc = "2.0" , id = 1 , result = {})
@@ -459,8 +452,12 @@ async def test_stdio_server_serves_in_place_when_the_standard_descriptors_are_in
459452 with anyio .fail_after (5 ):
460453 async with stdio_server () as (read_stream , write_stream ):
461454 async with read_stream : # pragma: no branch
462- assert os .path .sameopenfile (0 , in_r )
463- assert os .path .sameopenfile (1 , out_w )
455+ # Claimed: fd 0 reads the null device, not the pipe.
456+ devnull_probe = os .open (os .devnull , os .O_RDONLY )
457+ try :
458+ assert os .path .sameopenfile (0 , devnull_probe )
459+ finally :
460+ os .close (devnull_probe )
464461
465462 os .write (in_w , _frame (request ))
466463 received = await read_stream .receive ()
@@ -471,69 +468,39 @@ async def test_stdio_server_serves_in_place_when_the_standard_descriptors_are_in
471468 assert jsonrpc_message_adapter .validate_json (line .decode ().strip ()) == response
472469 os .close (in_w )
473470 await write_stream .aclose ()
471+
472+ assert os .path .sameopenfile (0 , in_r )
473+ assert os .path .sameopenfile (1 , out_w )
474474 finally :
475475 os .dup2 (saved2 , 2 )
476476 os .close (saved2 )
477477
478478
479479@pytest .mark .anyio
480- async def test_a_claim_that_cannot_roll_back_serves_the_wire_from_the_private_duplicate (
480+ async def test_stdio_server_serves_in_place_when_the_diversion_cannot_be_opened (
481481 monkeypatch : pytest .MonkeyPatch ,
482482) -> None :
483- """A mid-claim failure whose rollback also fails rolls forward instead of unclaiming.
484-
485- SDK-defined behavior: with fd 0 stuck on the diversion, the transport keeps the
486- claim and serves the protocol from the private duplicate; the restore at exit
487- still runs and puts fd 0 back on the pipe.
488- """
483+ """A diversion that cannot be opened leaves fd 0 untouched and serves in place."""
489484 request = JSONRPCRequest (jsonrpc = "2.0" , id = 1 , method = "ping" )
490485 with _pipe_planted_on_fd0 (monkeypatch ) as (in_r , in_w ):
486+ os .write (in_w , _frame (request ))
487+ os .close (in_w )
491488 monkeypatch .setattr (sys , "stdout" , TextIOWrapper (io .BytesIO (), encoding = "utf-8" ))
492489
493- # The first close (the diversion fd) and the second dup2 (the rollback)
494- # fail: the claim can neither complete nor be undone. One-shot and
495- # call-counted injectors, as elsewhere in this file.
496- real_close = os .close
497- close_armed = [True ]
490+ def failing_diversion () -> int :
491+ raise OSError ("injected diversion failure" )
498492
499- def failing_first_close (fd : int ) -> None :
500- if close_armed [0 ]:
501- close_armed [0 ] = False
502- raise OSError ("injected close failure" )
503- real_close (fd )
504-
505- real_dup2 = os .dup2
506- dup2_calls : list [int ] = []
507-
508- def flaky_dup2 (fd : int , fd2 : int , inheritable : bool = True ) -> int :
509- dup2_calls .append (fd )
510- if len (dup2_calls ) == 2 :
511- raise OSError ("injected rollback failure" )
512- return real_dup2 (fd , fd2 , inheritable )
513-
514- monkeypatch .setattr (os , "close" , failing_first_close )
515- monkeypatch .setattr (os , "dup2" , flaky_dup2 )
493+ monkeypatch .setattr ("mcp.server.stdio._open_stdin_diversion" , failing_diversion )
516494
517495 with anyio .fail_after (5 ):
518- async with stdio_server () as (read_stream , write_stream ):
496+ async with stdio_server () as (read_stream , write_stream ): # pragma: no branch
519497 async with read_stream : # pragma: no branch
520- # fd 0 is stuck on the null device, yet the wire still flows.
521- devnull_probe = os .open (os .devnull , os .O_RDONLY )
522- try :
523- assert os .path .sameopenfile (0 , devnull_probe )
524- finally :
525- os .close (devnull_probe )
526-
527- os .write (in_w , _frame (request ))
498+ assert os .path .sameopenfile (0 , in_r )
528499 received = await read_stream .receive ()
529500 assert isinstance (received , SessionMessage )
530501 assert received .message == request
531- os .close (in_w )
532502 await write_stream .aclose ()
533503
534- # The exit restore (third dup2) succeeded: fd 0 is back on the pipe.
535- assert os .path .sameopenfile (0 , in_r )
536-
537504
538505class _GatedStdin (io .RawIOBase ):
539506 """Raw stdin double: serves its frames, then blocks until released before EOF.
0 commit comments