@@ -604,6 +604,105 @@ 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 () # pragma: no cover
642+
643+ with _mock_sse_connection (events ()), anyio .fail_after (5 ):
644+ with pytest .raises (BaseExceptionGroup ) as exc_info : # noqa: F821 # builtin via anyio on 3.10
645+ async with sse_client ("http://test/sse" ): # pragma: no branch
646+ pytest .fail ("sse_client should not yield on origin mismatch" ) # pragma: no cover
647+ assert exc_info .group_contains (ValueError , match = "Endpoint origin does not match" )
648+
649+
650+ @pytest .mark .anyio
651+ async def test_sse_client_raises_on_error_before_endpoint () -> None :
652+ """Regression test for https://github.com/modelcontextprotocol/python-sdk/issues/447
653+
654+ Any exception raised while waiting for the endpoint event must propagate
655+ instead of deadlocking on the zero-buffer read stream.
656+ """
657+
658+ async def events () -> AsyncGenerator [ServerSentEvent , None ]:
659+ raise ConnectionError ("connection reset by peer" )
660+ yield # pragma: no cover
661+
662+ with _mock_sse_connection (events ()), anyio .fail_after (5 ):
663+ with pytest .raises (BaseExceptionGroup ) as exc_info : # noqa: F821 # builtin via anyio on 3.10
664+ async with sse_client ("http://test/sse" ): # pragma: no branch
665+ pytest .fail ("sse_client should not yield on pre-endpoint error" ) # pragma: no cover
666+ assert exc_info .group_contains (ConnectionError , match = "connection reset" )
667+
668+
669+ @pytest .mark .anyio
670+ async def test_sse_client_raises_on_message_before_endpoint () -> None :
671+ """Regression test for https://github.com/modelcontextprotocol/python-sdk/issues/447
672+
673+ If the server sends a message event before the endpoint event (protocol
674+ violation), sse_client must raise rather than deadlock trying to send the
675+ message to a stream nobody is reading yet.
676+ """
677+
678+ async def events () -> AsyncGenerator [ServerSentEvent , None ]:
679+ yield ServerSentEvent (event = "message" , data = '{"jsonrpc":"2.0","id":1,"result":{}}' )
680+ await anyio .sleep_forever () # pragma: no cover
681+
682+ with _mock_sse_connection (events ()), anyio .fail_after (5 ):
683+ with pytest .raises (BaseExceptionGroup ) as exc_info : # noqa: F821 # builtin via anyio on 3.10
684+ async with sse_client ("http://test/sse" ): # pragma: no branch
685+ pytest .fail ("sse_client should not yield on protocol violation" ) # pragma: no cover
686+ assert exc_info .group_contains (RuntimeError , match = "before endpoint event" )
687+
688+
689+ @pytest .mark .anyio
690+ async def test_sse_client_delivers_post_endpoint_errors_via_stream () -> None :
691+ """After the endpoint is received, errors in sse_reader are delivered on the
692+ read stream so the session can handle them, rather than crashing the task group.
693+ """
694+
695+ async def events () -> AsyncGenerator [ServerSentEvent , None ]:
696+ yield ServerSentEvent (event = "endpoint" , data = "/messages/?session_id=abc" )
697+ raise ConnectionError ("mid-stream failure" )
698+
699+ with _mock_sse_connection (events ()), anyio .fail_after (5 ):
700+ async with sse_client ("http://test/sse" ) as (read_stream , _ ):
701+ received = await read_stream .receive ()
702+ assert isinstance (received , ConnectionError )
703+ assert "mid-stream failure" in str (received )
704+
705+
607706@pytest .mark .anyio
608707async def test_sse_session_cleanup_on_disconnect (server : None , server_url : str ) -> None :
609708 """Regression test for https://github.com/modelcontextprotocol/python-sdk/issues/1227
0 commit comments