Skip to content

Commit 70fe29f

Browse files
committed
feat(exceptions): enhance error handling and logging to give accurate login action feedback
1 parent 1d75bb7 commit 70fe29f

File tree

13 files changed

+680
-281
lines changed

13 files changed

+680
-281
lines changed

README.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,17 @@ Suggest improvements for my CV to target this job posting https://www.linkedin.c
3737
> - **Recommended Jobs** (`get_recommended_jobs`): Selenium method compatibility issues
3838
> - **Company Profiles** (`get_company_profile`): Some companies can't be accessed / may return empty results (need further investigation)
3939
40+
## 🛡️ Error Handling & Non-Interactive Mode
41+
42+
**NEW**: Enhanced error handling for Docker and CI/CD environments!
43+
44+
The server now provides detailed error information when login fails:
45+
- **Specific error types**: `credentials_not_found`, `invalid_credentials`, `captcha_required`, `two_factor_auth_required`, `rate_limit`
46+
- **Non-interactive mode**: Use `--no-setup` to skip all prompts (perfect for Docker)
47+
- **Structured responses**: Each error includes type, message, and resolution steps
48+
49+
For detailed error handling documentation, see [ERROR_HANDLING.md](ERROR_HANDLING.md)
50+
4051
---
4152

4253
## 🐳 Docker Setup (Recommended - Universal)
@@ -57,7 +68,8 @@ Suggest improvements for my CV to target this job posting https://www.linkedin.c
5768
"run", "-i", "--rm",
5869
"-e", "LINKEDIN_EMAIL",
5970
"-e", "LINKEDIN_PASSWORD",
60-
"stickerdaniel/linkedin-mcp-server"
71+
"stickerdaniel/linkedin-mcp-server",
72+
"--no-setup"
6173
],
6274
"env": {
6375
"LINKEDIN_EMAIL": "your.email@example.com",
@@ -76,6 +88,7 @@ Suggest improvements for my CV to target this job posting https://www.linkedin.c
7688
- **Streamable HTTP**: For a web-based MCP server
7789

7890
**CLI Options:**
91+
- `--no-setup` - Skip interactive prompts (required for Docker/non-interactive environments)
7992
- `--debug` - Enable detailed logging
8093
- `--no-lazy-init` - Login to LinkedIn immediately instead of waiting for the first tool call
8194
- `--transport {stdio,streamable-http}` - Set transport mode
@@ -90,7 +103,7 @@ docker run -i --rm \
90103
-e LINKEDIN_PASSWORD="your_password" \
91104
-p 8080:8080 \
92105
stickerdaniel/linkedin-mcp-server \
93-
--transport streamable-http --host 0.0.0.0 --port 8080 --path /mcp
106+
--no-setup --transport streamable-http --host 0.0.0.0 --port 8080 --path /mcp
94107
```
95108
**Test with mcp inspector:**
96109
1. Install and run mcp inspector ```bunx @modelcontextprotocol/inspector```

linkedin_mcp_server/config/loaders.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,9 @@ def load_from_args(config: AppConfig) -> AppConfig:
135135

136136
if args.no_setup:
137137
config.server.setup = False
138+
config.chrome.non_interactive = (
139+
True # Automatically set when --no-setup is used
140+
)
138141

139142
if args.no_lazy_init:
140143
config.server.lazy_init = False

linkedin_mcp_server/config/secrets.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
# src/linkedin_mcp_server/config/secrets.py
22
import logging
3-
from typing import Dict, Optional
3+
from typing import Dict
44

55
import inquirer # type: ignore
66

77
from linkedin_mcp_server.config import get_config
8+
from linkedin_mcp_server.exceptions import CredentialsNotFoundError
89

910
from .providers import (
1011
get_credentials_from_keyring,
@@ -15,7 +16,7 @@
1516
logger = logging.getLogger(__name__)
1617

1718

18-
def get_credentials() -> Optional[Dict[str, str]]:
19+
def get_credentials() -> Dict[str, str]:
1920
"""Get LinkedIn credentials from config, keyring, or prompt."""
2021
config = get_config()
2122

@@ -31,10 +32,12 @@ def get_credentials() -> Optional[Dict[str, str]]:
3132
print(f"Using LinkedIn credentials from {get_keyring_name()}")
3233
return {"email": credentials["email"], "password": credentials["password"]}
3334

34-
# If in non-interactive mode and no credentials found, return None
35+
# If in non-interactive mode and no credentials found, raise error
3536
if config.chrome.non_interactive:
36-
print("No credentials found in non-interactive mode")
37-
return None
37+
raise CredentialsNotFoundError(
38+
"No LinkedIn credentials found. Please provide credentials via "
39+
"environment variables (LINKEDIN_EMAIL, LINKEDIN_PASSWORD) or keyring."
40+
)
3841

3942
# Otherwise, prompt for credentials
4043
return prompt_for_credentials()

linkedin_mcp_server/drivers/chrome.py

Lines changed: 147 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@
1818
from linkedin_mcp_server.config import get_config
1919
from linkedin_mcp_server.config.providers import clear_credentials_from_keyring
2020
from linkedin_mcp_server.config.secrets import get_credentials
21+
from linkedin_scraper.exceptions import (
22+
CaptchaRequiredError,
23+
InvalidCredentialsError,
24+
LoginTimeoutError,
25+
RateLimitError,
26+
SecurityChallengeError,
27+
TwoFactorAuthError,
28+
)
29+
from linkedin_mcp_server.exceptions import (
30+
CredentialsNotFoundError,
31+
DriverInitializationError,
32+
)
2133

2234
# Global driver storage to reuse sessions
2335
active_drivers: Dict[str, webdriver.Chrome] = {}
@@ -87,24 +99,41 @@ def get_or_create_driver() -> Optional[webdriver.Chrome]:
8799
driver.set_page_load_timeout(60)
88100

89101
# Try to log in
90-
if login_to_linkedin(driver):
91-
print("Successfully logged in to LinkedIn")
92-
elif config.chrome.non_interactive:
93-
# In non-interactive mode, if login fails, return None
102+
try:
103+
if login_to_linkedin(driver):
104+
print("Successfully logged in to LinkedIn")
105+
active_drivers[session_id] = driver
106+
return driver
107+
except (
108+
CaptchaRequiredError,
109+
InvalidCredentialsError,
110+
SecurityChallengeError,
111+
TwoFactorAuthError,
112+
RateLimitError,
113+
LoginTimeoutError,
114+
CredentialsNotFoundError,
115+
) as e:
116+
# Clean up driver on login failure
94117
driver.quit()
95-
return None
96118

97-
active_drivers[session_id] = driver
98-
return driver
119+
if config.chrome.non_interactive:
120+
# In non-interactive mode, propagate the error
121+
raise e
122+
else:
123+
# In interactive mode, handle the error
124+
handle_login_error(e)
125+
return None
99126
except Exception as e:
100127
error_msg = f"🛑 Error creating web driver: {e}"
101128
print(error_msg)
102129

103130
if config.chrome.non_interactive:
104-
print("🛑 Failed to initialize driver in non-interactive mode")
105-
return None
131+
raise DriverInitializationError(error_msg)
132+
else:
133+
raise WebDriverException(error_msg)
134+
106135

107-
raise WebDriverException(error_msg)
136+
# Remove this function - linkedin-scraper now handles all error detection
108137

109138

110139
def login_to_linkedin(driver: webdriver.Chrome) -> bool:
@@ -116,59 +145,105 @@ def login_to_linkedin(driver: webdriver.Chrome) -> bool:
116145
117146
Returns:
118147
bool: True if login was successful, False otherwise
148+
149+
Raises:
150+
Various login-related errors from linkedin-scraper
119151
"""
120152
config = get_config()
121153

122154
# Get LinkedIn credentials from config
123-
credentials = get_credentials()
155+
try:
156+
credentials = get_credentials()
157+
except CredentialsNotFoundError as e:
158+
if config.chrome.non_interactive:
159+
raise e
160+
# Only prompt if not in non-interactive mode
161+
from linkedin_mcp_server.config.secrets import prompt_for_credentials
162+
163+
credentials = prompt_for_credentials()
124164

125165
if not credentials:
126-
print("❌ No credentials available")
127-
return False
166+
raise CredentialsNotFoundError("No credentials available")
128167

129-
try:
130-
# Login to LinkedIn
131-
print("🔑 Logging in to LinkedIn...")
168+
# Login to LinkedIn using enhanced linkedin-scraper
169+
print("🔑 Logging in to LinkedIn...")
132170

133-
from linkedin_scraper import actions # type: ignore
171+
from linkedin_scraper import actions # type: ignore
134172

135-
actions.login(driver, credentials["email"], credentials["password"])
173+
# linkedin-scraper now handles all error detection and raises appropriate exceptions
174+
actions.login(
175+
driver,
176+
credentials["email"],
177+
credentials["password"],
178+
interactive=not config.chrome.non_interactive,
179+
)
136180

137-
print("✅ Successfully logged in to LinkedIn")
138-
return True
139-
except Exception as e:
140-
error_msg = f"Failed to login: {str(e)}"
141-
print(f"❌ {error_msg}")
181+
print("✅ Successfully logged in to LinkedIn")
182+
return True
142183

143-
if not config.chrome.non_interactive:
184+
185+
def handle_login_error(error: Exception) -> None:
186+
"""Handle login errors in interactive mode."""
187+
config = get_config()
188+
189+
print(f"\n❌ Login failed: {str(error)}")
190+
191+
if isinstance(error, InvalidCredentialsError):
192+
print("⚠️ Please check your email and password.")
193+
retry = inquirer.prompt(
194+
[
195+
inquirer.Confirm(
196+
"retry",
197+
message="Would you like to try with different credentials?",
198+
default=True,
199+
),
200+
]
201+
)
202+
if retry and retry.get("retry", False):
203+
# Clear credentials from keyring and try again
204+
clear_credentials_from_keyring()
205+
# Try again
206+
initialize_driver()
207+
208+
elif isinstance(error, CaptchaRequiredError):
209+
print("⚠️ LinkedIn requires captcha verification.")
210+
captcha_url = getattr(error, "captcha_url", str(error))
211+
print(f"🔗 Please complete the captcha at: {captcha_url}")
212+
if config.chrome.headless:
144213
print(
145-
"⚠️ You might need to confirm the login in your LinkedIn mobile app. "
146-
"Please try again and confirm the login."
214+
"🔍 Try running with visible browser window to complete captcha: "
215+
"uv run main.py --no-headless"
147216
)
148217

149-
if config.chrome.headless:
150-
print(
151-
"🔍 Try running with visible browser window to see what's happening: "
152-
"uv run main.py --no-headless"
153-
)
218+
elif isinstance(error, SecurityChallengeError):
219+
print("⚠️ LinkedIn requires a security challenge.")
220+
challenge_url = getattr(error, "challenge_url", str(error))
221+
print(f"🔗 Please complete the security challenge at: {challenge_url}")
222+
if config.chrome.headless:
223+
print(
224+
"🔍 Try running with visible browser window to complete challenge: "
225+
"uv run main.py --no-headless"
226+
)
154227

155-
retry = inquirer.prompt(
156-
[
157-
inquirer.Confirm(
158-
"retry",
159-
message="Would you like to try with different credentials?",
160-
default=True,
161-
),
162-
]
228+
elif isinstance(error, TwoFactorAuthError):
229+
print("⚠️ Two-factor authentication is required.")
230+
print(
231+
"📱 Please confirm the login in your LinkedIn mobile app or enter the 2FA code."
232+
)
233+
if config.chrome.headless:
234+
print(
235+
"🔍 Try running with visible browser window to complete 2FA: "
236+
"uv run main.py --no-headless"
163237
)
164238

165-
if retry and retry.get("retry", False):
166-
# Clear credentials from keyring and try again
167-
clear_credentials_from_keyring()
168-
# Try again with new credentials
169-
return login_to_linkedin(driver)
239+
elif isinstance(error, RateLimitError):
240+
print("⚠️ Too many login attempts. Please wait before trying again.")
170241

171-
return False
242+
elif isinstance(error, LoginTimeoutError):
243+
print("⚠️ Login timed out. Please check your network connection.")
244+
245+
else:
246+
print("⚠️ An unexpected error occurred during login.")
172247

173248

174249
def initialize_driver() -> None:
@@ -204,8 +279,30 @@ def initialize_driver() -> None:
204279
if driver:
205280
print("✅ Web driver initialized successfully")
206281
else:
282+
if config.chrome.non_interactive:
283+
raise DriverInitializationError(
284+
"Failed to initialize web driver in non-interactive mode"
285+
)
207286
print("❌ Failed to initialize web driver.")
287+
except (
288+
CaptchaRequiredError,
289+
InvalidCredentialsError,
290+
SecurityChallengeError,
291+
TwoFactorAuthError,
292+
RateLimitError,
293+
LoginTimeoutError,
294+
CredentialsNotFoundError,
295+
) as e:
296+
# In non-interactive mode, let the error propagate
297+
if config.chrome.non_interactive:
298+
raise e
299+
# In interactive mode, handle gracefully
300+
print(f"❌ Error: {str(e)}")
208301
except WebDriverException as e:
302+
if config.chrome.non_interactive:
303+
raise DriverInitializationError(
304+
f"Failed to initialize web driver: {str(e)}"
305+
)
209306
print(f"❌ Failed to initialize web driver: {str(e)}")
210307
handle_driver_error()
211308

@@ -216,6 +313,11 @@ def handle_driver_error() -> None:
216313
"""
217314
config = get_config()
218315

316+
# Skip interactive handling in non-interactive mode
317+
if config.chrome.non_interactive:
318+
print("❌ ChromeDriver is required for this application to work properly.")
319+
sys.exit(1)
320+
219321
questions = [
220322
inquirer.List(
221323
"chromedriver_action",

0 commit comments

Comments
 (0)