@@ -594,6 +594,33 @@ def test_stop_closes_event_loop():
594594 assert client ._background_thread_event_loop is None
595595
596596
597+ def test_stop_skips_cleanup_during_interpreter_finalization ():
598+ """Test that stop() is a no-op when the interpreter is finalizing.
599+
600+ On Python 3.14+, threading.Thread.join() raises PythonFinalizationError at
601+ shutdown. The background thread is a daemon and is reclaimed automatically,
602+ so stop() should skip join() and event loop cleanup to avoid noisy
603+ tracebacks surfaced via Agent.__del__ during GC. See issue #2143.
604+ """
605+ client = MCPClient (MagicMock ())
606+
607+ mock_thread = MagicMock ()
608+ mock_event_loop = MagicMock ()
609+ client ._background_thread = mock_thread
610+ client ._background_thread_event_loop = mock_event_loop
611+
612+ with patch ("strands.tools.mcp.mcp_client.sys.is_finalizing" , return_value = True ):
613+ # Must not raise, and must not touch the thread or event loop.
614+ client .stop (None , None , None )
615+
616+ mock_thread .join .assert_not_called ()
617+ mock_event_loop .close .assert_not_called ()
618+ # State is intentionally left alone during finalization — the interpreter
619+ # is going away and cleanup is unnecessary.
620+ assert client ._background_thread is mock_thread
621+ assert client ._background_thread_event_loop is mock_event_loop
622+
623+
597624def test_mcp_client_state_reset_after_timeout ():
598625 """Test that all client state is properly reset after timeout."""
599626
0 commit comments