@@ -217,30 +217,26 @@ async def test_stdio_server_takes_stdin_off_the_descriptor_table_while_serving(
217217async def test_stdio_server_reads_stdin_in_place_when_descriptor_isolation_fails (
218218 failing_call : str , monkeypatch : pytest .MonkeyPatch
219219) -> None :
220- """A descriptor-table failure while claiming stdin degrades to reading sys.stdin in place.
220+ """A descriptor failure while claiming stdin degrades to reading sys.stdin in place.
221221
222- SDK-defined behavior: isolation is best-effort; the dup2 variant fails after the private duplicate exists.
222+ SDK-defined behavior: isolation is best-effort; when duplicating fd 0 or diverting
223+ it fails, the transport serves over the original stdin exactly as v1 did.
223224 """
224225 request = JSONRPCRequest (jsonrpc = "2.0" , id = 1 , method = "ping" )
225226 with _pipe_planted_on_fd0 (monkeypatch ) as (in_r , in_w ):
226227 os .write (in_w , _frame (request ))
227228 os .close (in_w )
228229 monkeypatch .setattr (sys , "stdout" , TextIOWrapper (io .BytesIO (), encoding = "utf-8" ))
229230
230- # Injectors fire once, then pass through: pytest capture also calls os.dup/os.dup2,
231- # and a still-armed injector detonating there corrupts every later test's capture.
232231 if failing_call == "dup" :
233- real_dup = os .dup
234- armed = [True ]
235232
236- def failing_dup (fd : int ) -> int :
237- if fd == 0 and armed [0 ]:
238- armed [0 ] = False
239- raise OSError ("injected descriptor failure" )
240- return real_dup (fd )
233+ def failing_dup_above_std (fd : int ) -> int :
234+ raise OSError ("injected descriptor failure" )
241235
242- monkeypatch .setattr (os , "dup " , failing_dup )
236+ monkeypatch .setattr ("mcp.server.stdio._dup_above_std " , failing_dup_above_std )
243237 else :
238+ # Fires once at the divert, then passes through: pytest's capture
239+ # machinery also calls os.dup2 at phase transitions.
244240 real_dup2 = os .dup2
245241 armed = [True ]
246242
@@ -257,12 +253,6 @@ def failing_dup2(fd: int, fd2: int, inheritable: bool = True) -> int:
257253 async with read_stream : # pragma: no branch
258254 # Isolation was skipped: fd 0 is still the protocol pipe.
259255 assert os .path .sameopenfile (0 , in_r )
260- # In-place transports still own the stream: a second one is refused.
261- with pytest .raises (RuntimeError , match = "already claimed fd 0" ):
262- async with stdio_server ():
263- pytest .fail ("unreachable" ) # pragma: no cover
264- # The spent injector passes calls through untouched.
265- os .close (os .dup (0 ))
266256 received = await read_stream .receive ()
267257 assert isinstance (received , SessionMessage )
268258 assert received .message == request
@@ -279,8 +269,7 @@ async def test_stdio_server_exits_cleanly_when_the_stdin_restore_fails(
279269 still-diverted fd must refuse later transports rather than serve them the diversion.
280270 """
281271 request = JSONRPCRequest (jsonrpc = "2.0" , id = 1 , method = "ping" )
282- fresh_claims : set [int ] = set ()
283- monkeypatch .setattr ("mcp.server.stdio._claimed" , fresh_claims ) # this test leaves fd 0 claimed
272+ monkeypatch .setattr ("mcp.server.stdio._claims" , {}) # this test leaves fd 0 claimed
284273 with _pipe_planted_on_fd0 (monkeypatch ) as (_ , in_w ):
285274 os .write (in_w , _frame (request ))
286275 os .close (in_w )
@@ -375,6 +364,8 @@ def failing_dup(fd: int) -> int:
375364 with anyio .fail_after (5 ):
376365 async with stdio_server (stdin = anyio .AsyncFile (io .StringIO ())) as (read_stream , write_stream ):
377366 read_stream .close ()
367+ # The spent injector passes later duplications through untouched.
368+ os .close (os .dup (0 ))
378369 devnull_probe = os .open (os .devnull , os .O_WRONLY )
379370 try :
380371 assert os .path .sameopenfile (1 , devnull_probe )
@@ -444,12 +435,14 @@ async def test_a_refused_claim_releases_the_stream_it_already_took(
444435
445436
446437@pytest .mark .anyio
447- async def test_stdio_server_serves_in_place_when_the_standard_descriptors_are_incomplete (
438+ async def test_the_claim_engages_even_when_stderr_is_closed (
448439 monkeypatch : pytest .MonkeyPatch ,
449440) -> None :
450- """A process missing a standard descriptor is served in place, without surgery .
441+ """A process missing fd 2 still gets full isolation .
451442
452- With fd 2 closed, a duplicate could land in the standard range: the transport must not touch the table.
443+ SDK-defined behavior: the wire duplicate is allocated above the standard range
444+ atomically, so a hole in the descriptor table cannot capture it; the stdout
445+ diversion falls back to the null device.
453446 """
454447 request = JSONRPCRequest (jsonrpc = "2.0" , id = 1 , method = "ping" )
455448 response = JSONRPCResponse (jsonrpc = "2.0" , id = 1 , result = {})
@@ -460,8 +453,12 @@ async def test_stdio_server_serves_in_place_when_the_standard_descriptors_are_in
460453 with anyio .fail_after (5 ):
461454 async with stdio_server () as (read_stream , write_stream ):
462455 async with read_stream : # pragma: no branch
463- assert os .path .sameopenfile (0 , in_r )
464- assert os .path .sameopenfile (1 , out_w )
456+ # Claimed: fd 0 reads the null device, not the pipe.
457+ devnull_probe = os .open (os .devnull , os .O_RDONLY )
458+ try :
459+ assert os .path .sameopenfile (0 , devnull_probe )
460+ finally :
461+ os .close (devnull_probe )
465462
466463 os .write (in_w , _frame (request ))
467464 received = await read_stream .receive ()
@@ -472,69 +469,39 @@ async def test_stdio_server_serves_in_place_when_the_standard_descriptors_are_in
472469 assert jsonrpc_message_adapter .validate_json (line .decode ().strip ()) == response
473470 os .close (in_w )
474471 await write_stream .aclose ()
472+
473+ assert os .path .sameopenfile (0 , in_r )
474+ assert os .path .sameopenfile (1 , out_w )
475475 finally :
476476 os .dup2 (saved2 , 2 )
477477 os .close (saved2 )
478478
479479
480480@pytest .mark .anyio
481- async def test_a_claim_that_cannot_roll_back_serves_the_wire_from_the_private_duplicate (
481+ async def test_stdio_server_serves_in_place_when_the_diversion_cannot_be_opened (
482482 monkeypatch : pytest .MonkeyPatch ,
483483) -> None :
484- """A mid-claim failure whose rollback also fails rolls forward instead of unclaiming.
485-
486- SDK-defined behavior: with fd 0 stuck on the diversion, the transport keeps the
487- claim and serves the protocol from the private duplicate; the restore at exit
488- still runs and puts fd 0 back on the pipe.
489- """
484+ """A diversion that cannot be opened leaves fd 0 untouched and serves in place."""
490485 request = JSONRPCRequest (jsonrpc = "2.0" , id = 1 , method = "ping" )
491486 with _pipe_planted_on_fd0 (monkeypatch ) as (in_r , in_w ):
487+ os .write (in_w , _frame (request ))
488+ os .close (in_w )
492489 monkeypatch .setattr (sys , "stdout" , TextIOWrapper (io .BytesIO (), encoding = "utf-8" ))
493490
494- # The first close (the diversion fd) and the second dup2 (the rollback)
495- # fail: the claim can neither complete nor be undone. One-shot and
496- # call-counted injectors, as elsewhere in this file.
497- real_close = os .close
498- close_armed = [True ]
491+ def failing_diversion () -> int :
492+ raise OSError ("injected diversion failure" )
499493
500- def failing_first_close (fd : int ) -> None :
501- if close_armed [0 ]:
502- close_armed [0 ] = False
503- raise OSError ("injected close failure" )
504- real_close (fd )
505-
506- real_dup2 = os .dup2
507- dup2_calls : list [int ] = []
508-
509- def flaky_dup2 (fd : int , fd2 : int , inheritable : bool = True ) -> int :
510- dup2_calls .append (fd )
511- if len (dup2_calls ) == 2 :
512- raise OSError ("injected rollback failure" )
513- return real_dup2 (fd , fd2 , inheritable )
514-
515- monkeypatch .setattr (os , "close" , failing_first_close )
516- monkeypatch .setattr (os , "dup2" , flaky_dup2 )
494+ monkeypatch .setattr ("mcp.server.stdio._open_stdin_diversion" , failing_diversion )
517495
518496 with anyio .fail_after (5 ):
519- async with stdio_server () as (read_stream , write_stream ):
497+ async with stdio_server () as (read_stream , write_stream ): # pragma: no branch
520498 async with read_stream : # pragma: no branch
521- # fd 0 is stuck on the null device, yet the wire still flows.
522- devnull_probe = os .open (os .devnull , os .O_RDONLY )
523- try :
524- assert os .path .sameopenfile (0 , devnull_probe )
525- finally :
526- os .close (devnull_probe )
527-
528- os .write (in_w , _frame (request ))
499+ assert os .path .sameopenfile (0 , in_r )
529500 received = await read_stream .receive ()
530501 assert isinstance (received , SessionMessage )
531502 assert received .message == request
532- os .close (in_w )
533503 await write_stream .aclose ()
534504
535- # The exit restore (third dup2) succeeded: fd 0 is back on the pipe.
536- assert os .path .sameopenfile (0 , in_r )
537-
538505
539506class _GatedStdin (io .RawIOBase ):
540507 """Raw stdin double: serves its frames, then blocks until released before EOF.
0 commit comments