@@ -88,3 +88,39 @@ def capturing_background_task(self):
8888 )
8989 # Verify it was indeed a different thread
9090 assert background_thread_value ["thread_id" ] != main_thread_id , "Background task should run in a different thread"
91+
92+
93+ def test_mcp_client_clears_running_loop_in_background_thread (mock_transport , mock_session ):
94+ """Test that _background_task clears any leaked running event loop state.
95+
96+ When OpenTelemetry's ThreadingInstrumentor is active, Thread.run() is wrapped to propagate
97+ trace context, which can leak the parent thread's running event loop reference into child
98+ threads. This causes "RuntimeError: Cannot run the event loop while another loop is running"
99+ when the background thread calls run_until_complete().
100+
101+ This test simulates that scenario by setting a running loop before _background_task runs
102+ and verifying it gets cleared.
103+ """
104+ import asyncio
105+
106+ cleared_running_loop = {}
107+
108+ original_background_task = MCPClient ._background_task
109+
110+ def simulating_otel_leak_background_task (self ):
111+ # Simulate OTEL ThreadingInstrumentor leaking the parent's running loop
112+ fake_loop = asyncio .new_event_loop ()
113+ asyncio ._set_running_loop (fake_loop ) # type: ignore[attr-defined]
114+
115+ # Call the real _background_task — it should clear the leaked loop and succeed
116+ try :
117+ return original_background_task (self )
118+ finally :
119+ cleared_running_loop ["success" ] = True
120+ fake_loop .close ()
121+
122+ with patch .object (MCPClient , "_background_task" , simulating_otel_leak_background_task ):
123+ with MCPClient (mock_transport ["transport_callable" ]) as client :
124+ assert client ._background_thread is not None
125+
126+ assert cleared_running_loop .get ("success" ), "_background_task should have run successfully despite leaked loop"
0 commit comments