Skip to content

Commit 9d0d913

Browse files
committed
refactor(error_handler): streamline error handling for tools, fix multiple small isssues, verify all tools are working as expected
1 parent 66ec545 commit 9d0d913

File tree

8 files changed

+305
-279
lines changed

8 files changed

+305
-279
lines changed

linkedin_mcp_server/cli.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,7 @@ def print_claude_config() -> None:
2626
and copies it to the clipboard for easy pasting.
2727
"""
2828
config = get_config()
29-
current_dir = os.path.abspath(
30-
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
31-
)
29+
current_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
3230

3331
# Find the full path to uv executable
3432
try:

linkedin_mcp_server/drivers/chrome.py

Lines changed: 99 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -98,31 +98,37 @@ def get_or_create_driver() -> Optional[webdriver.Chrome]:
9898
# Add a page load timeout for safety
9999
driver.set_page_load_timeout(60)
100100

101-
# Try to log in
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
117-
driver.quit()
118-
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
101+
# Try to log in with retry loop
102+
max_retries = 3
103+
for attempt in range(max_retries):
104+
try:
105+
if login_to_linkedin(driver):
106+
print("Successfully logged in to LinkedIn")
107+
active_drivers[session_id] = driver
108+
return driver
109+
except (
110+
CaptchaRequiredError,
111+
InvalidCredentialsError,
112+
SecurityChallengeError,
113+
TwoFactorAuthError,
114+
RateLimitError,
115+
LoginTimeoutError,
116+
CredentialsNotFoundError,
117+
) as e:
118+
if config.chrome.non_interactive:
119+
# In non-interactive mode, propagate the error
120+
driver.quit()
121+
raise e
122+
else:
123+
# In interactive mode, handle the error and potentially retry
124+
should_retry = handle_login_error(e)
125+
if should_retry and attempt < max_retries - 1:
126+
print(f"🔄 Retry attempt {attempt + 2}/{max_retries}")
127+
continue
128+
else:
129+
# Clean up driver on final failure
130+
driver.quit()
131+
return None
126132
except Exception as e:
127133
error_msg = f"🛑 Error creating web driver: {e}"
128134
print(error_msg)
@@ -167,26 +173,74 @@ def login_to_linkedin(driver: webdriver.Chrome) -> bool:
167173

168174
from linkedin_scraper import actions # type: ignore
169175

170-
# linkedin-scraper now handles all error detection and raises appropriate exceptions
171-
actions.login(
172-
driver,
173-
credentials["email"],
174-
credentials["password"],
175-
interactive=not config.chrome.non_interactive,
176-
)
176+
# Use linkedin-scraper login but with simplified error handling
177+
try:
178+
actions.login(
179+
driver,
180+
credentials["email"],
181+
credentials["password"],
182+
interactive=not config.chrome.non_interactive,
183+
)
184+
185+
print("✅ Successfully logged in to LinkedIn")
186+
return True
177187

178-
print("✅ Successfully logged in to LinkedIn")
179-
return True
188+
except Exception:
189+
# Check current page to determine the real issue
190+
current_url = driver.current_url
180191

192+
if "checkpoint/challenge" in current_url:
193+
# We're on a challenge page - this is the real issue, not credentials
194+
if "security check" in driver.page_source.lower():
195+
raise SecurityChallengeError(
196+
challenge_url=current_url,
197+
message="LinkedIn requires a security challenge. Please complete it manually and restart the application.",
198+
)
199+
else:
200+
raise CaptchaRequiredError(
201+
captcha_url=current_url,
202+
)
181203

182-
def handle_login_error(error: Exception) -> None:
183-
"""Handle login errors in interactive mode."""
204+
elif "feed" in current_url or "mynetwork" in current_url:
205+
# Actually logged in successfully despite the exception
206+
print("✅ Successfully logged in to LinkedIn")
207+
return True
208+
209+
else:
210+
# Check for actual credential issues
211+
page_source = driver.page_source.lower()
212+
if any(
213+
pattern in page_source
214+
for pattern in ["wrong email", "wrong password", "incorrect", "invalid"]
215+
):
216+
raise InvalidCredentialsError("Invalid LinkedIn email or password.")
217+
elif "too many" in page_source:
218+
raise RateLimitError(
219+
"Too many login attempts. Please wait and try again later."
220+
)
221+
else:
222+
raise LoginTimeoutError(
223+
"Login failed. Please check your credentials and network connection."
224+
)
225+
226+
227+
def handle_login_error(error: Exception) -> bool:
228+
"""Handle login errors in interactive mode.
229+
230+
Returns:
231+
bool: True if user wants to retry, False if they want to exit
232+
"""
184233
config = get_config()
185234

186-
print(f"\n❌ Login failed: {str(error)}")
235+
print(f"\n{str(error)}")
236+
237+
if config.chrome.headless:
238+
print(
239+
"🔍 Try running with visible browser window: uv run main.py --no-headless"
240+
)
187241

242+
# Only allow retry for credential errors
188243
if isinstance(error, InvalidCredentialsError):
189-
print("⚠️ Please check your email and password.")
190244
retry = inquirer.prompt(
191245
[
192246
inquirer.Confirm(
@@ -197,51 +251,12 @@ def handle_login_error(error: Exception) -> None:
197251
]
198252
)
199253
if retry and retry.get("retry", False):
200-
# Clear credentials from keyring
201254
clear_credentials_from_keyring()
202255
print("✅ Credentials cleared from keyring.")
203-
print("💡 Please restart the application to try with new credentials.")
204-
print(" Example: uv run main.py --no-headless")
205-
206-
elif isinstance(error, CaptchaRequiredError):
207-
print("⚠️ LinkedIn requires captcha verification.")
208-
captcha_url = getattr(error, "captcha_url", str(error))
209-
print(f"🔗 Please complete the captcha at: {captcha_url}")
210-
if config.chrome.headless:
211-
print(
212-
"🔍 Try running with visible browser window to complete captcha: "
213-
"uv run main.py --no-headless"
214-
)
256+
print("🔄 Retrying with new credentials...")
257+
return True
215258

216-
elif isinstance(error, SecurityChallengeError):
217-
print("⚠️ LinkedIn requires a security challenge.")
218-
challenge_url = getattr(error, "challenge_url", str(error))
219-
print(f"🔗 Please complete the security challenge at: {challenge_url}")
220-
if config.chrome.headless:
221-
print(
222-
"🔍 Try running with visible browser window to complete challenge: "
223-
"uv run main.py --no-headless"
224-
)
225-
226-
elif isinstance(error, TwoFactorAuthError):
227-
print("⚠️ Two-factor authentication is required.")
228-
print(
229-
"📱 Please confirm the login in your LinkedIn mobile app or enter the 2FA code."
230-
)
231-
if config.chrome.headless:
232-
print(
233-
"🔍 Try running with visible browser window to complete 2FA: "
234-
"uv run main.py --no-headless"
235-
)
236-
237-
elif isinstance(error, RateLimitError):
238-
print("⚠️ Too many login attempts. Please wait before trying again.")
239-
240-
elif isinstance(error, LoginTimeoutError):
241-
print("⚠️ Login timed out. Please check your network connection.")
242-
243-
else:
244-
print("⚠️ An unexpected error occurred during login.")
259+
return False
245260

246261

247262
def initialize_driver() -> None:
@@ -277,11 +292,8 @@ def initialize_driver() -> None:
277292
if driver:
278293
print("✅ Web driver initialized successfully")
279294
else:
280-
if config.chrome.non_interactive:
281-
raise DriverInitializationError(
282-
"Failed to initialize web driver in non-interactive mode"
283-
)
284-
print("❌ Failed to initialize web driver.")
295+
# Driver creation failed - always raise an error
296+
raise DriverInitializationError("Failed to initialize web driver")
285297
except (
286298
CaptchaRequiredError,
287299
InvalidCredentialsError,
@@ -291,11 +303,8 @@ def initialize_driver() -> None:
291303
LoginTimeoutError,
292304
CredentialsNotFoundError,
293305
) as e:
294-
# In non-interactive mode, let the error propagate
295-
if config.chrome.non_interactive:
296-
raise e
297-
# In interactive mode, handle gracefully
298-
print(f"❌ Error: {str(e)}")
306+
# Always re-raise login-related errors so main.py can handle them
307+
raise e
299308
except WebDriverException as e:
300309
if config.chrome.non_interactive:
301310
raise DriverInitializationError(

linkedin_mcp_server/error_handler.py

Lines changed: 14 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -23,49 +23,34 @@
2323
)
2424

2525

26-
def handle_linkedin_errors(func):
26+
def handle_tool_error(exception: Exception, context: str = "") -> Dict[str, Any]:
2727
"""
28-
Decorator to handle LinkedIn MCP errors consistently across all tools.
29-
30-
This decorator wraps tool functions and converts exceptions into
31-
structured error responses that MCP clients can understand.
28+
Handle errors from tool functions and return structured responses.
3229
3330
Args:
34-
func: The tool function to wrap
31+
exception: The exception that occurred
32+
context: Context about which tool failed
3533
3634
Returns:
37-
The decorated function that returns structured error responses
35+
Structured error response dictionary
3836
"""
37+
return convert_exception_to_response(exception, context)
3938

40-
async def wrapper(*args, **kwargs):
41-
try:
42-
return await func(*args, **kwargs)
43-
except Exception as e:
44-
return convert_exception_to_response(e, func.__name__)
45-
46-
return wrapper
4739

48-
49-
def handle_linkedin_errors_list(func):
40+
def handle_tool_error_list(
41+
exception: Exception, context: str = ""
42+
) -> List[Dict[str, Any]]:
5043
"""
51-
Decorator to handle LinkedIn MCP errors for functions that return lists.
52-
53-
Similar to handle_linkedin_errors but returns errors in list format.
44+
Handle errors from tool functions that return lists.
5445
5546
Args:
56-
func: The tool function to wrap
47+
exception: The exception that occurred
48+
context: Context about which tool failed
5749
5850
Returns:
59-
The decorated function that returns structured error responses in list format
51+
List containing structured error response dictionary
6052
"""
61-
62-
async def wrapper(*args, **kwargs):
63-
try:
64-
return await func(*args, **kwargs)
65-
except Exception as e:
66-
return convert_exception_to_list_response(e, func.__name__)
67-
68-
return wrapper
53+
return convert_exception_to_list_response(exception, context)
6954

7055

7156
def convert_exception_to_response(

0 commit comments

Comments
 (0)