Skip to content

Commit 84295f3

Browse files
committed
refactor(setup): streamline Chrome driver creation and options
1 parent f2f2e28 commit 84295f3

File tree

3 files changed

+119
-90
lines changed

3 files changed

+119
-90
lines changed

linkedin_mcp_server/drivers/chrome.py

Lines changed: 97 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@
88

99
import logging
1010
import os
11-
import shutil
12-
import tempfile
1311
from typing import Dict, Optional
1412

1513
from linkedin_scraper.exceptions import (
@@ -34,29 +32,22 @@
3432
# Global driver storage to reuse sessions
3533
active_drivers: Dict[str, webdriver.Chrome] = {}
3634

37-
# Store user data directories for cleanup
38-
user_data_dirs: Dict[str, str] = {}
3935

4036
logger = logging.getLogger(__name__)
4137

4238

43-
def create_chrome_driver(session_id: str = "default") -> webdriver.Chrome:
39+
def create_chrome_options(config) -> Options:
4440
"""
45-
Create a new Chrome WebDriver instance with proper configuration.
41+
Create Chrome options with all necessary configuration for LinkedIn scraping.
4642
4743
Args:
48-
session_id: Unique identifier for the session (used for cleanup)
44+
config: AppConfig instance with Chrome configuration
4945
5046
Returns:
51-
webdriver.Chrome: Configured Chrome WebDriver instance
52-
53-
Raises:
54-
WebDriverException: If driver creation fails
47+
Options: Configured Chrome options object
5548
"""
56-
config = get_config()
57-
58-
# Set up Chrome options
5949
chrome_options = Options()
50+
6051
logger.info(
6152
f"Running browser in {'headless' if config.chrome.headless else 'visible'} mode"
6253
)
@@ -70,14 +61,15 @@ def create_chrome_driver(session_id: str = "default") -> webdriver.Chrome:
7061
chrome_options.add_argument("--window-size=1920,1080")
7162
chrome_options.add_argument("--disable-extensions")
7263
chrome_options.add_argument("--disable-background-timer-throttling")
73-
74-
# Create a unique user data directory to avoid conflicts
75-
user_data_dir = tempfile.mkdtemp(prefix="linkedin_mcp_chrome_")
76-
chrome_options.add_argument(f"--user-data-dir={user_data_dir}")
77-
logger.debug(f"Using Chrome user data directory: {user_data_dir}")
78-
79-
# Store the user data directory for cleanup
80-
user_data_dirs[session_id] = user_data_dir
64+
chrome_options.add_argument("--disable-background-networking")
65+
chrome_options.add_argument("--disable-default-apps")
66+
chrome_options.add_argument("--disable-sync")
67+
chrome_options.add_argument("--metrics-recording-only")
68+
chrome_options.add_argument("--no-default-browser-check")
69+
chrome_options.add_argument("--no-first-run")
70+
chrome_options.add_argument("--disable-features=TranslateUI,BlinkGenPropertyTrees")
71+
chrome_options.add_argument("--aggressive-cache-discard")
72+
chrome_options.add_argument("--disable-ipc-flooding-protection")
8173

8274
# Set user agent (configurable with sensible default)
8375
user_agent = getattr(config.chrome, "user_agent", DEFAULT_USER_AGENT)
@@ -87,20 +79,96 @@ def create_chrome_driver(session_id: str = "default") -> webdriver.Chrome:
8779
for arg in config.chrome.browser_args:
8880
chrome_options.add_argument(arg)
8981

90-
# Initialize Chrome driver
91-
logger.info("Initializing Chrome WebDriver...")
82+
return chrome_options
9283

84+
85+
def create_chrome_service(config):
86+
"""
87+
Create Chrome service with ChromeDriver path resolution.
88+
89+
Args:
90+
config: AppConfig instance with Chrome configuration
91+
92+
Returns:
93+
Service or None: Chrome service if path is configured, None for auto-detection
94+
"""
9395
# Use ChromeDriver path from environment or config
9496
chromedriver_path = (
9597
os.environ.get("CHROMEDRIVER_PATH") or config.chrome.chromedriver_path
9698
)
9799

98100
if chromedriver_path:
99101
logger.info(f"Using ChromeDriver at path: {chromedriver_path}")
100-
service = Service(executable_path=chromedriver_path)
101-
driver = webdriver.Chrome(service=service, options=chrome_options)
102+
return Service(executable_path=chromedriver_path)
102103
else:
103104
logger.info("Using auto-detected ChromeDriver")
105+
return None
106+
107+
108+
def create_temporary_chrome_driver() -> webdriver.Chrome:
109+
"""
110+
Create a temporary Chrome WebDriver instance for one-off operations.
111+
112+
This driver is NOT stored in the global active_drivers dict and should be
113+
manually cleaned up by the caller.
114+
115+
Returns:
116+
webdriver.Chrome: Configured Chrome WebDriver instance
117+
118+
Raises:
119+
WebDriverException: If driver creation fails
120+
"""
121+
config = get_config()
122+
123+
logger.info("Creating temporary Chrome WebDriver...")
124+
125+
# Create Chrome options using shared function
126+
chrome_options = create_chrome_options(config)
127+
128+
# Create Chrome service using shared function
129+
service = create_chrome_service(config)
130+
131+
# Initialize Chrome driver
132+
if service:
133+
driver = webdriver.Chrome(service=service, options=chrome_options)
134+
else:
135+
driver = webdriver.Chrome(options=chrome_options)
136+
137+
logger.info("Temporary Chrome WebDriver created successfully")
138+
139+
# Add a page load timeout for safety
140+
driver.set_page_load_timeout(60)
141+
142+
# Set shorter implicit wait for faster operations
143+
driver.implicitly_wait(10)
144+
145+
return driver
146+
147+
148+
def create_chrome_driver() -> webdriver.Chrome:
149+
"""
150+
Create a new Chrome WebDriver instance with proper configuration.
151+
152+
Returns:
153+
webdriver.Chrome: Configured Chrome WebDriver instance
154+
155+
Raises:
156+
WebDriverException: If driver creation fails
157+
"""
158+
config = get_config()
159+
160+
logger.info("Initializing Chrome WebDriver...")
161+
162+
# Create Chrome options using shared function
163+
chrome_options = create_chrome_options(config)
164+
165+
# Create Chrome service using shared function
166+
service = create_chrome_service(config)
167+
168+
# Initialize Chrome driver
169+
if service:
170+
driver = webdriver.Chrome(service=service, options=chrome_options)
171+
else:
104172
driver = webdriver.Chrome(options=chrome_options)
105173

106174
logger.info("Chrome WebDriver initialized successfully")
@@ -229,7 +297,7 @@ def get_or_create_driver(authentication: str) -> webdriver.Chrome:
229297

230298
try:
231299
# Create new driver
232-
driver = create_chrome_driver(session_id)
300+
driver = create_chrome_driver()
233301

234302
# Login to LinkedIn
235303
login_to_linkedin(driver, authentication)
@@ -261,7 +329,7 @@ def get_or_create_driver(authentication: str) -> webdriver.Chrome:
261329

262330
def close_all_drivers() -> None:
263331
"""Close all active drivers and clean up resources."""
264-
global active_drivers, user_data_dirs
332+
global active_drivers
265333

266334
for session_id, driver in active_drivers.items():
267335
try:
@@ -270,21 +338,8 @@ def close_all_drivers() -> None:
270338
except Exception as e:
271339
logger.warning(f"Error closing driver {session_id}: {e}")
272340

273-
# Clean up user data directory
274-
if session_id in user_data_dirs:
275-
try:
276-
user_data_dir = user_data_dirs[session_id]
277-
if os.path.exists(user_data_dir):
278-
shutil.rmtree(user_data_dir)
279-
logger.debug(f"Cleaned up user data directory: {user_data_dir}")
280-
except Exception as e:
281-
logger.warning(
282-
f"Error cleaning up user data directory for session {session_id}: {e}"
283-
)
284-
285341
active_drivers.clear()
286-
user_data_dirs.clear()
287-
logger.info("All Chrome WebDriver sessions closed and cleaned up")
342+
logger.info("All Chrome WebDriver sessions closed")
288343

289344

290345
def get_active_driver() -> Optional[webdriver.Chrome]:

linkedin_mcp_server/setup.py

Lines changed: 4 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,11 @@
66
"""
77

88
import logging
9-
import os
10-
import tempfile
119
from contextlib import contextmanager
1210
from typing import Dict, Iterator
1311

1412
import inquirer
1513
from selenium import webdriver
16-
from selenium.webdriver.chrome.options import Options
1714

1815
from linkedin_mcp_server.authentication import store_authentication
1916
from linkedin_mcp_server.config import get_config
@@ -105,44 +102,13 @@ def temporary_chrome_driver() -> Iterator[webdriver.Chrome]:
105102
Raises:
106103
Exception: If driver creation fails
107104
"""
108-
config: AppConfig = get_config()
109-
110-
logger.info("Creating temporary browser for cookie capture...")
111-
112-
# Set up Chrome options for cookie capture
113-
chrome_options = Options()
114-
if config.chrome.headless:
115-
chrome_options.add_argument("--headless=new")
116-
117-
# Add essential options
118-
# chrome_options.add_argument("--no-sandbox")
119-
# chrome_options.add_argument("--disable-dev-shm-usage")
120-
# chrome_options.add_argument("--disable-gpu")
121-
# chrome_options.add_argument("--window-size=3456,2234")
122-
123-
# Create a unique user data directory to avoid conflicts
124-
user_data_dir = tempfile.mkdtemp(prefix="linkedin_mcp_setup_")
125-
chrome_options.add_argument(f"--user-data-dir={user_data_dir}")
126-
logger.debug(f"Using Chrome user data directory for setup: {user_data_dir}")
105+
from linkedin_mcp_server.drivers.chrome import create_temporary_chrome_driver
127106

128107
driver = None
129108
try:
130-
# Create temporary driver
131-
chromedriver_path = (
132-
os.environ.get("CHROMEDRIVER_PATH") or config.chrome.chromedriver_path
133-
)
134-
135-
if chromedriver_path:
136-
from selenium.webdriver.chrome.service import Service
137-
138-
service = Service(executable_path=chromedriver_path)
139-
driver = webdriver.Chrome(service=service, options=chrome_options)
140-
else:
141-
driver = webdriver.Chrome(options=chrome_options)
142-
143-
driver.set_page_load_timeout(60)
109+
# Create temporary driver using shared function
110+
driver = create_temporary_chrome_driver()
144111
yield driver
145-
146112
finally:
147113
if driver:
148114
driver.quit()
@@ -174,7 +140,7 @@ def capture_cookie_from_credentials(email: str, password: str) -> str:
174140
email,
175141
password,
176142
timeout=60, # longer timeout for login (captcha, mobile verification, etc.)
177-
interactive=interactive, # Respect configuration setting
143+
interactive=interactive, # type: ignore # Respect configuration setting
178144
)
179145

180146
# Capture cookie

main.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -80,16 +80,20 @@ def get_cookie_and_exit() -> None:
8080
print(cookie)
8181

8282
# Try to copy to clipboard
83+
clipboard_success = False
8384
try:
8485
import pyperclip
8586

8687
pyperclip.copy(cookie)
88+
clipboard_success = True
89+
print("📋 Cookie copied to clipboard!")
90+
except Exception as e:
91+
logger.debug(f"pyperclip clipboard failed: {e}")
92+
93+
if not clipboard_success:
8794
print(
88-
"📋 Cookie copied to clipboard! Now you can set the LINKEDIN_COOKIE environment variable in your configuration"
95+
"💡 Set this cookie as an environment variable in your config or pass it with --cookie flag"
8996
)
90-
except Exception as e:
91-
logger.warning(f"Could not copy to clipboard: {e}")
92-
print("⚠️ Copy the cookie above manually")
9397

9498
except Exception as e:
9599
logger.error(f"Error getting cookie: {e}")
@@ -108,7 +112,7 @@ def get_cookie_and_exit() -> None:
108112
print("\n🍪 To get your LinkedIn cookie manually:")
109113
print(" 1. Login to LinkedIn in your browser")
110114
print(" 2. Open Developer Tools (F12)")
111-
print(" 3. Go to Application/Storage > Cookies > linkedin.com")
115+
print(" 3. Go to Application/Storage > Cookies > www.linkedin.com")
112116
print(" 4. Copy the 'li_at' cookie value")
113117
print(" 5. Set LINKEDIN_COOKIE environment variable or use --cookie flag")
114118
elif "invalid credentials" in error_msg:
@@ -144,8 +148,9 @@ def ensure_authentication_ready() -> str:
144148
# If in non-interactive mode and no auth, fail immediately
145149
if config.chrome.non_interactive:
146150
raise CredentialsNotFoundError(
147-
"No LinkedIn authentication found. Please provide cookie via "
148-
"environment variable (LINKEDIN_COOKIE) or run with --get-cookie to obtain one."
151+
"No LinkedIn cookie found for non-interactive mode. You can:\n"
152+
" 1. Set LINKEDIN_COOKIE environment variable with a valid LinkedIn session cookie\n"
153+
" 2. Run with --get-cookie to extract a cookie using email/password"
149154
)
150155

151156
# Run interactive setup
@@ -211,9 +216,12 @@ def main() -> None:
211216
logger.info("Authentication ready")
212217
except CredentialsNotFoundError as e:
213218
logger.error(f"Authentication setup failed: {e}")
214-
print(
215-
"\n❌ Authentication required - please provide LinkedIn cookie or credentials"
216-
)
219+
if config.chrome.non_interactive:
220+
print("\n❌ LinkedIn cookie required for Docker/non-interactive mode")
221+
else:
222+
print(
223+
"\n❌ Authentication required - please provide LinkedIn authentication"
224+
)
217225
sys.exit(1)
218226
except KeyboardInterrupt:
219227
print("\n\n👋 Setup cancelled by user")

0 commit comments

Comments
 (0)