feat: linkedin messaging, get sidebar profiles#291
feat: linkedin messaging, get sidebar profiles#291stickerdaniel merged 7 commits intostickerdaniel:mainfrom
Conversation
…file - Add get_sidebar_profiles() extractor method that scrapes sidebar sections (More profiles for you, Explore premium profiles, People you may know), follows Show all links, and skips any /premium redirects - Add _extract_profile_urn() helper that reads the recipient URN from the Message button compose href on the current profile page - Expose profile_urn in scrape_person results when available - Register get_sidebar_profiles MCP tool in person.py - Add to manifest.json and README tool table - Tests: TestGetSidebarProfiles, TestExtractProfileUrn, TestScrapePersonProfileUrn, TestGetSidebarProfilesTool
Adds four messaging tools: get_inbox, get_conversation, search_conversations, and send_message (with profile_urn bypass for reliable compose URL routing). Includes all browser helper methods and full test coverage. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
feat: add get_sidebar_profiles tool and profile_urn in get_person_profile
Greptile SummaryThis PR adds five new tools to the LinkedIn MCP server: Key observations:
Confidence Score: 4/5Mostly safe to merge; one P1 issue means send_message can return sent: True even on a failed send, which should be fixed before production use. The P1 issue in _message_text_visible means the sent field in the send_message response is not a reliable confirmation — callers cannot trust sent: True as proof of delivery. All other changes are well-implemented and tested. linkedin_mcp_server/scraping/extractor.py — specifically the _message_text_visible method and its caller in send_message. Important Files Changed
Sequence DiagramsequenceDiagram
participant C as MCP Client
participant T as send_message tool
participant E as LinkedInExtractor
participant LI as LinkedIn (browser)
C->>T: send_message(username, message, confirm_send, profile_urn?)
T->>E: send_message(username, message, confirm_send, profile_urn)
E->>LI: navigate /in/{username}/
E->>LI: read profile display name
alt profile_urn provided
E-->>E: compose_url = /messaging/compose/?recipient={urn}
else
E->>LI: find messaging compose link href
end
E->>LI: navigate compose_url
alt recipient_picker surface
E->>LI: select recipient by display name
alt selection fails
E-->>T: recipient_resolution_failed
end
end
E->>LI: verify compose page matches recipient
alt confirm_send == False
E-->>T: confirmation_required, sent: false
end
E->>LI: type message in compose box
E->>LI: wait for send button enabled
E->>LI: click send button
E->>LI: _message_text_visible checks body.innerText
Note over E,LI: Text already in compose box after typing - check may pass prematurely
alt text found in body
E-->>T: sent: true
else
E-->>T: send_unavailable, sent: false
end
T-->>C: result dict
|
There was a problem hiding this comment.
Pull request overview
Adds new LinkedIn capabilities to the MCP server: scraping sidebar “recommended profiles” from a person page, and a new messaging tool suite (inbox/conversations/search/send), backed by new extractor functionality and tests.
Changes:
- Add
get_sidebar_profilestool and extractor support for sidebar recommendation sections. - Introduce messaging tools (
get_inbox,get_conversation,search_conversations,send_message) and register them on the server. - Extend scraping/extractor with profile URN extraction and messaging page automation; update manifest/tests/docs accordingly.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_tools.py | Adds tool-level tests for sidebar + messaging tools and updates timeout coverage list. |
| tests/test_scraping.py | Adds extractor-level tests for sidebar profiles, profile URN extraction, and messaging flows. |
| manifest.json | Publishes new tool names/descriptions in the MCP bundle manifest. |
| linkedin_mcp_server/tools/person.py | Registers the new get_sidebar_profiles MCP tool. |
| linkedin_mcp_server/tools/messaging.py | New module registering inbox/conversation/search/send messaging tools. |
| linkedin_mcp_server/tools/init.py | Updates tool module documentation to mention messaging tools. |
| linkedin_mcp_server/server.py | Registers messaging tools on server startup. |
| linkedin_mcp_server/scraping/extractor.py | Implements sidebar scraping, profile URN extraction, and messaging automation helpers. |
| README.md | Adds get_sidebar_profiles to the tool status table. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| result: dict[str, Any] = { | ||
| "url": f"{base_url}/", | ||
| "sections": sections, | ||
| } | ||
| if profile_urn: | ||
| result["profile_urn"] = profile_urn | ||
| if references: |
There was a problem hiding this comment.
scrape_person now conditionally adds a profile_urn field to the returned dict (see the new result["profile_urn"] assignment). The method’s docstring still documents only {url, sections} as the return shape; please update it so callers/tests/documentation reflect the optional profile_urn key.
|
|
||
| def __init__(self, page: Page): | ||
| self._page = page | ||
| self._conversation_thread_cache: dict[str, str] = {} |
There was a problem hiding this comment.
LinkedInExtractor now has _conversation_thread_cache, but get_ready_extractor() constructs a new LinkedInExtractor(browser.page) for each tool call, so this cache will be empty on every call in normal usage. Consider removing it or moving the cache to a longer-lived place (e.g., session/browser state) if cross-call caching is intended.
| self._conversation_thread_cache: dict[str, str] = {} | |
| # Share the conversation thread cache across extractor instances that | |
| # operate on the same Page, so cached data persists between tool calls. | |
| existing_cache = getattr(page, "_conversation_thread_cache", None) | |
| if existing_cache is None: | |
| existing_cache = {} | |
| setattr(page, "_conversation_thread_cache", existing_cache) | |
| self._conversation_thread_cache = existing_cache |
| await ctx.report_progress(progress=100, total=100, message="Complete") | ||
|
|
||
| return result | ||
|
|
There was a problem hiding this comment.
get_sidebar_profiles does not handle AuthenticationError the same way the other person tools do. Since LinkedInExtractor navigation can raise AuthenticationError, this tool should catch it and delegate to handle_auth_error(e, ctx) (mirroring get_person_profile/search_people/connect_with_person) so callers get the standard re-login flow instead of a generic ToolError.
| except AuthenticationError as e: | |
| try: | |
| await handle_auth_error(e, ctx) | |
| except Exception as relogin_exc: | |
| raise_tool_error(relogin_exc, "get_sidebar_profiles") |
| try: | ||
| extractor = extractor or await get_ready_extractor( | ||
| ctx, tool_name="get_inbox" | ||
| ) | ||
| logger.info("Fetching inbox (limit=%d)", limit) | ||
|
|
||
| await ctx.report_progress( | ||
| progress=0, total=100, message="Loading messaging inbox" | ||
| ) | ||
|
|
||
| result = await extractor.get_inbox(limit=limit) | ||
|
|
||
| await ctx.report_progress(progress=100, total=100, message="Complete") | ||
|
|
||
| return result | ||
|
|
||
| except Exception as e: | ||
| raise_tool_error(e, "get_inbox") # NoReturn |
There was a problem hiding this comment.
Messaging tools currently don’t handle AuthenticationError/re-login the way the existing person/company/job tools do. Because extractor navigation can raise AuthenticationError after get_ready_extractor returns, these tools should add an except AuthenticationError as e: await handle_auth_error(e, ctx) branch (and import AuthenticationError + handle_auth_error) so stale sessions trigger the standard re-auth flow.
| _MESSAGING_THREAD_LINK_SELECTOR = 'main a[href*="/messaging/thread/"]' | ||
| _MESSAGING_RESULT_ITEM_SELECTOR = "main [role='listitem'], main li" | ||
| _MESSAGING_COMPOSE_LINK_SELECTOR = 'main a[href*="/messaging/compose/"]' | ||
| _MESSAGING_COMPOSE_SELECTOR = ( | ||
| 'div[role="textbox"][contenteditable="true"][aria-label*="Write a message"]' | ||
| ) | ||
| _MESSAGING_COMPOSE_FALLBACK_SELECTORS = ( | ||
| _MESSAGING_COMPOSE_SELECTOR, | ||
| 'main div[role="textbox"][contenteditable="true"]', | ||
| 'main [contenteditable="true"][aria-label*="message"]', | ||
| ) | ||
| _MESSAGING_SEND_SELECTOR = ( | ||
| 'button[type="submit"], button[aria-label*="Send"], button[aria-label*="send"]' | ||
| ) |
There was a problem hiding this comment.
New messaging selector constants _MESSAGING_THREAD_LINK_SELECTOR, _MESSAGING_RESULT_ITEM_SELECTOR, and _MESSAGING_SEND_SELECTOR are currently unused in this module. Either remove them to avoid dead code, or switch the relevant querySelector/querySelectorAll calls to use them so there’s a single source of truth for these selectors.
| async def _click_first(self, selector: str, *, timeout: int = 5000) -> None: | ||
| """Click the first visible locator that matches a selector.""" | ||
| target = self._page.locator(selector).first | ||
| try: | ||
| await target.scroll_into_view_if_needed(timeout=timeout) | ||
| except Exception: | ||
| logger.debug("Could not scroll %s into view", selector, exc_info=True) | ||
| await target.click(timeout=timeout) | ||
|
|
There was a problem hiding this comment.
_click_first’s docstring says it clicks the first visible locator, but the implementation always targets .first without ensuring visibility. Either adjust the implementation to find a visible element (or wait for visible before clicking), or update the docstring to reflect the actual behavior to avoid misleading future changes.
| | `get_sidebar_profiles` | Extract profile URLs from sidebar recommendation sections ("More profiles for you", "Explore premium profiles", "People you may know") on a profile page | Working | | ||
| | `get_company_profile` | Extract company information with explicit section selection (posts, jobs) | Working | |
There was a problem hiding this comment.
The README tool-status table is meant to enumerate the available MCP tools, but it now omits the newly added messaging tools (get_inbox, get_conversation, search_conversations, send_message). Please add rows for these tools (and brief descriptions) so documentation matches the server/manifest capabilities.
- Remove unused selector constants (_MESSAGING_THREAD_LINK_SELECTOR, _MESSAGING_RESULT_ITEM_SELECTOR, _MESSAGING_SEND_SELECTOR) - Remove dead _conversation_thread_cache (new extractor per tool call) - Add AuthenticationError handling to get_sidebar_profiles and all messaging tools - Pass CSS selector as evaluate() arg instead of f-string interpolation - Replace deprecated execCommand with press_sequentially - Guard sidebar container walk against depth-limit exhaustion - Update scrape_person docstring to document profile_urn return key - Add messaging tools to README tool-status table
LinkedIn redirects /messaging/ to the most recent thread; capture baseline_thread_id after the SPA settles so search-selected threads can be distinguished from the auto-opened one.
feat: linkedin messaging, get sidebar profiles
feat: linkedin messaging, get sidebar profiles
feat: linkedin messaging, get sidebar profiles
…connect feat: linkedin messaging, get sidebar profiles
No description provided.