diff --git a/litellm/proxy/_experimental/mcp_server/server.py b/litellm/proxy/_experimental/mcp_server/server.py index 5da1ce50dbc..49e88334df3 100644 --- a/litellm/proxy/_experimental/mcp_server/server.py +++ b/litellm/proxy/_experimental/mcp_server/server.py @@ -322,7 +322,12 @@ if MCP_AVAILABLE: async def _cleanup_expired_stateful_session_auth_contexts() -> None: while True: await asyncio.sleep(_STATEFUL_SESSION_IDLE_TIMEOUT_SECONDS) - await _purge_expired_stateful_session_auth_contexts() + try: + await _purge_expired_stateful_session_auth_contexts() + except Exception as e: + verbose_logger.exception( + f"Error cleaning up expired MCP stateful sessions: {e}" + ) async def initialize_session_managers(): """Initialize the session managers. Can be called from main app lifespan.""" diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py index be7b52898bd..821c13e6592 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py @@ -1515,6 +1515,30 @@ async def test_stateful_mcp_auth_context_cleanup_respects_zero_now(): mcp_server._stateful_session_auth_context_last_seen.pop(session_id, None) +@pytest.mark.asyncio +async def test_stateful_mcp_cleanup_loop_survives_purge_errors(): + """Cleanup loop should keep running after one purge attempt fails.""" + try: + from litellm.proxy._experimental.mcp_server import server as mcp_server + except ImportError: + pytest.skip("MCP server not available") + + purge = AsyncMock( + side_effect=[RuntimeError("terminate failed"), asyncio.CancelledError()] + ) + + with ( + patch.object(mcp_server.asyncio, "sleep", AsyncMock(return_value=None)), + patch.object( + mcp_server, "_purge_expired_stateful_session_auth_contexts", purge + ), + ): + with pytest.raises(asyncio.CancelledError): + await mcp_server._cleanup_expired_stateful_session_auth_contexts() + + assert purge.await_count == 2 + + @pytest.mark.asyncio async def test_owner_fingerprint_distinguishes_oauth_callers(): """