@@ -604,6 +604,102 @@ async def mock_aiter_sse() -> AsyncGenerator[ServerSentEvent, None]:
604604 assert msg .message .id == 1
605605
606606
607+ def _mock_sse_connection (aiter_sse : AsyncGenerator [ServerSentEvent , None ]) -> Any :
608+ """Patch sse_client's HTTP layer to yield the given SSE event stream."""
609+ mock_event_source = MagicMock ()
610+ mock_event_source .aiter_sse .return_value = aiter_sse
611+ mock_event_source .response .raise_for_status = MagicMock ()
612+
613+ mock_aconnect_sse = MagicMock ()
614+ mock_aconnect_sse .__aenter__ = AsyncMock (return_value = mock_event_source )
615+ mock_aconnect_sse .__aexit__ = AsyncMock (return_value = None )
616+
617+ mock_client = MagicMock ()
618+ mock_client .__aenter__ = AsyncMock (return_value = mock_client )
619+ mock_client .__aexit__ = AsyncMock (return_value = None )
620+ mock_client .post = AsyncMock (return_value = MagicMock (status_code = 200 , raise_for_status = MagicMock ()))
621+
622+ return patch .multiple (
623+ "mcp.client.sse" ,
624+ create_mcp_http_client = Mock (return_value = mock_client ),
625+ aconnect_sse = Mock (return_value = mock_aconnect_sse ),
626+ )
627+
628+
629+ @pytest .mark .anyio
630+ async def test_sse_client_raises_on_endpoint_origin_mismatch () -> None :
631+ """Regression test for https://github.com/modelcontextprotocol/python-sdk/issues/447
632+
633+ When the server sends an endpoint URL with a different origin than the
634+ connection URL, sse_client must raise promptly instead of deadlocking.
635+ Before the fix, the ValueError was caught and sent to a zero-buffer stream
636+ with no reader, hanging forever.
637+ """
638+
639+ async def events () -> AsyncGenerator [ServerSentEvent , None ]:
640+ yield ServerSentEvent (event = "endpoint" , data = "http://wrong-host:9999/messages?sessionId=abc" )
641+ await anyio .sleep_forever ()
642+
643+ with _mock_sse_connection (events ()), anyio .fail_after (5 ):
644+ with pytest .RaisesGroup (pytest .RaisesExc (ValueError , match = "Endpoint origin does not match" )):
645+ async with sse_client ("http://test/sse" ):
646+ pytest .fail ("sse_client should not yield on origin mismatch" )
647+
648+
649+ @pytest .mark .anyio
650+ async def test_sse_client_raises_on_error_before_endpoint () -> None :
651+ """Regression test for https://github.com/modelcontextprotocol/python-sdk/issues/447
652+
653+ Any exception raised while waiting for the endpoint event must propagate
654+ instead of deadlocking on the zero-buffer read stream.
655+ """
656+
657+ async def events () -> AsyncGenerator [ServerSentEvent , None ]:
658+ raise ConnectionError ("connection reset by peer" )
659+ yield # pragma: no cover
660+
661+ with _mock_sse_connection (events ()), anyio .fail_after (5 ):
662+ with pytest .RaisesGroup (pytest .RaisesExc (ConnectionError , match = "connection reset" )):
663+ async with sse_client ("http://test/sse" ):
664+ pytest .fail ("sse_client should not yield on pre-endpoint error" )
665+
666+
667+ @pytest .mark .anyio
668+ async def test_sse_client_raises_on_message_before_endpoint () -> None :
669+ """Regression test for https://github.com/modelcontextprotocol/python-sdk/issues/447
670+
671+ If the server sends a message event before the endpoint event (protocol
672+ violation), sse_client must raise rather than deadlock trying to send the
673+ message to a stream nobody is reading yet.
674+ """
675+
676+ async def events () -> AsyncGenerator [ServerSentEvent , None ]:
677+ yield ServerSentEvent (event = "message" , data = '{"jsonrpc":"2.0","id":1,"result":{}}' )
678+ await anyio .sleep_forever ()
679+
680+ with _mock_sse_connection (events ()), anyio .fail_after (5 ):
681+ with pytest .RaisesGroup (pytest .RaisesExc (RuntimeError , match = "before endpoint event" )):
682+ async with sse_client ("http://test/sse" ):
683+ pytest .fail ("sse_client should not yield on protocol violation" )
684+
685+
686+ @pytest .mark .anyio
687+ async def test_sse_client_delivers_post_endpoint_errors_via_stream () -> None :
688+ """After the endpoint is received, errors in sse_reader are delivered on the
689+ read stream so the session can handle them, rather than crashing the task group.
690+ """
691+
692+ async def events () -> AsyncGenerator [ServerSentEvent , None ]:
693+ yield ServerSentEvent (event = "endpoint" , data = "/messages/?session_id=abc" )
694+ raise ConnectionError ("mid-stream failure" )
695+
696+ with _mock_sse_connection (events ()), anyio .fail_after (5 ):
697+ async with sse_client ("http://test/sse" ) as (read_stream , _ ):
698+ received = await read_stream .receive ()
699+ assert isinstance (received , ConnectionError )
700+ assert "mid-stream failure" in str (received )
701+
702+
607703@pytest .mark .anyio
608704async def test_sse_session_cleanup_on_disconnect (server : None , server_url : str ) -> None :
609705 """Regression test for https://github.com/modelcontextprotocol/python-sdk/issues/1227
0 commit comments