Skip to content

Commit 0f57b49

Browse files
fix(connect): Use exact text matching and scroll to top before clicking (#287)
2 parents c000ff2 + fd064f1 commit 0f57b49

1 file changed

Lines changed: 30 additions & 13 deletions

File tree

linkedin_mcp_server/scraping/extractor.py

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -97,14 +97,18 @@ def _connection_result(
9797
message: str,
9898
*,
9999
note_sent: bool = False,
100+
profile: str = "",
100101
) -> dict[str, Any]:
101102
"""Build a structured response for a profile connection attempt."""
102-
return {
103+
result: dict[str, Any] = {
103104
"url": url,
104105
"status": status,
105106
"message": message,
106107
"note_sent": note_sent,
107108
}
109+
if profile:
110+
result["profile"] = profile
111+
return result
108112

109113

110114
def _normalize_csv(value: str, mapping: dict[str, str]) -> str:
@@ -393,25 +397,28 @@ async def get_page_text(self) -> str:
393397
async def click_button_by_text(
394398
self, text: str, *, scope: str = "main", timeout: int = 5000
395399
) -> bool:
396-
"""Click the first button/link matching *text* within *scope*.
400+
"""Click the first button/link whose visible text is exactly *text*.
397401
398-
The text comes from LLM analysis at runtime — not hardcoded.
402+
Uses a regex filter for exact matching to avoid substring false
403+
positives (e.g. "Connect" matching "connections").
399404
Returns True if clicked, False if no match found.
400405
"""
401-
selector = (
402-
f'{scope} button:has-text("{text}"), '
403-
f'{scope} a:has-text("{text}"), '
404-
f'{scope} [role="button"]:has-text("{text}")'
406+
matches = (
407+
self._page.locator(scope)
408+
.locator("button, a, [role='button']")
409+
.filter(has_text=re.compile(rf"^{re.escape(text)}$"))
405410
)
406-
locator = self._page.locator(selector).first
411+
count = await matches.count()
412+
logger.debug("click_button_by_text(%r): %d matches in %s", text, count, scope)
413+
if count == 0:
414+
return False
415+
target = matches.first
407416
try:
408-
if await self._page.locator(selector).count() == 0:
409-
return False
410-
await locator.scroll_into_view_if_needed(timeout=timeout)
417+
await target.scroll_into_view_if_needed(timeout=timeout)
411418
except Exception:
412419
logger.debug("Scroll failed for button '%s'", text, exc_info=True)
413420
try:
414-
await locator.click(timeout=timeout)
421+
await target.click(timeout=timeout)
415422
return True
416423
except Exception:
417424
logger.debug("Click failed for button '%s'", text, exc_info=True)
@@ -777,25 +784,31 @@ async def connect_with_person(
777784

778785
if state == "already_connected":
779786
return _connection_result(
780-
url, "already_connected", "You are already connected with this profile."
787+
url,
788+
"already_connected",
789+
"You are already connected with this profile.",
790+
profile=page_text,
781791
)
782792
if state == "pending":
783793
return _connection_result(
784794
url,
785795
"pending",
786796
"A connection request is already pending for this profile.",
797+
profile=page_text,
787798
)
788799
if state == "follow_only":
789800
return _connection_result(
790801
url,
791802
"follow_only",
792803
"This profile currently exposes Follow but not Connect.",
804+
profile=page_text,
793805
)
794806
if state == "unavailable":
795807
return _connection_result(
796808
url,
797809
"connect_unavailable",
798810
"LinkedIn did not expose a usable Connect action for this profile.",
811+
profile=page_text,
799812
)
800813

801814
# state is "connectable" or "incoming_request"
@@ -864,6 +877,9 @@ async def connect_with_person(
864877
except PlaywrightTimeoutError:
865878
logger.debug("Dialog did not close after clicking send")
866879

880+
# Read the current page text (already on the profile after the action)
881+
updated_text = await self.get_page_text()
882+
867883
status = "accepted" if state == "incoming_request" else "connected"
868884
return _connection_result(
869885
url,
@@ -872,6 +888,7 @@ async def connect_with_person(
872888
if status == "connected"
873889
else "Connection request accepted.",
874890
note_sent=note_sent,
891+
profile=updated_text,
875892
)
876893

877894
async def scrape_company(

0 commit comments

Comments
 (0)