feat: add thread_id support to send_message for replying to InMails#451
feat: add thread_id support to send_message for replying to InMails#451czerwiakowskim wants to merge 1 commit into
Conversation
The send_message tool previously failed for non-connected users because LinkedIn does not show a Message button on their profile pages. This commit adds a thread_id parameter to send_message that navigates directly to an existing conversation thread and sends the message there, bypassing the Message-button lookup entirely. Changes: - messaging.py: Added thread_id parameter and updated docstring - extractor.py: Added thread-based sending path that reuses existing compose box and send logic Use case: Reply to InMails from recruiters or anyone who messaged you first, where you may not be connected.
Greptile SummaryThis PR adds a direct thread reply path to the LinkedIn messaging send tool. The main changes are:
Confidence Score: 3/5These issues should be fixed before merging.
Important Files Changed
|
| if thread_id: | ||
| thread_url = f"https://www.linkedin.com/messaging/thread/{thread_id}/" | ||
| logger.info( | ||
| "send_message: thread_id=%s — navigating directly to thread", | ||
| thread_id, | ||
| ) | ||
| await self._navigate_to_page(thread_url) |
There was a problem hiding this comment.
Normalize thread references The tools that users are told to use for discovery expose conversation references as
/messaging/thread/{id}/, but this branch interpolates thread_id directly into another thread URL. Passing that returned reference produces a malformed URL like /messaging/thread//messaging/thread/...//, so normal tool chaining from inbox/search can fail before reaching the conversation.
Prompt To Fix With AI
This is a comment left during a code review.
Path: linkedin_mcp_server/scraping/extractor.py
Line: 3170-3176
Comment:
**Normalize thread references** The tools that users are told to use for discovery expose conversation references as `/messaging/thread/{id}/`, but this branch interpolates `thread_id` directly into another thread URL. Passing that returned reference produces a malformed URL like `/messaging/thread//messaging/thread/...//`, so normal tool chaining from inbox/search can fail before reaching the conversation.
How can I resolve this? If you propose a fix, please make it concise.| try: | ||
| await self._page.wait_for_selector("main") | ||
| except PlaywrightTimeoutError: | ||
| logger.debug("Thread page did not load for thread %s", thread_id) | ||
|
|
||
| await handle_modal_close(self._page) | ||
|
|
||
| # The compose box at the bottom of an open conversation is the same | ||
| # element as the standalone composer — reuse existing helpers. | ||
| message_surface = await self._wait_for_message_surface() | ||
| logger.debug( | ||
| "Message surface in thread %s: %s", | ||
| thread_id, | ||
| message_surface, | ||
| ) | ||
|
|
||
| compose_box = await self._resolve_message_compose_box() | ||
| if compose_box is None: | ||
| return self._message_action_result( | ||
| self._page.url, | ||
| "composer_unavailable", | ||
| "LinkedIn did not expose a usable message composer in this thread.", | ||
| ) | ||
|
|
||
| # Recipient is already set by the conversation — skip resolution. | ||
| recipient_selected = True |
There was a problem hiding this comment.
Verify opened thread This path treats any visible composer after navigation as the requested recipient and sets
recipient_selected = True without checking the final URL or extracted thread id. If LinkedIn redirects an invalid or inaccessible thread to the inbox, or the previous thread remains selected after a load problem, confirm_send=True can send the message to whatever conversation is currently open while reporting success for the requested thread.
Prompt To Fix With AI
This is a comment left during a code review.
Path: linkedin_mcp_server/scraping/extractor.py
Line: 3179-3204
Comment:
**Verify opened thread** This path treats any visible composer after navigation as the requested recipient and sets `recipient_selected = True` without checking the final URL or extracted thread id. If LinkedIn redirects an invalid or inaccessible thread to the inbox, or the previous thread remains selected after a load problem, `confirm_send=True` can send the message to whatever conversation is currently open while reporting success for the requested thread.
How can I resolve this? If you propose a fix, please make it concise.| focused = await self._page.evaluate( | ||
| """() => { | ||
| const el = document.querySelector( | ||
| 'div[role="textbox"][contenteditable="true"][aria-label*="Write a message"],' | ||
| + 'div[role="textbox"][contenteditable="true"]' | ||
| ); | ||
| if (!el) return false; | ||
| el.focus(); | ||
| return true; | ||
| }""" |
There was a problem hiding this comment.
Wrong textbox target The code resolves
compose_box, then ignores it and focuses the first matching contenteditable in the whole document. Messaging pages can contain more than one editable surface or draft, so this can type into a different box than the active thread composer and then click a send button elsewhere or fail to send the intended reply.
Prompt To Fix With AI
This is a comment left during a code review.
Path: linkedin_mcp_server/scraping/extractor.py
Line: 3216-3225
Comment:
**Wrong textbox target** The code resolves `compose_box`, then ignores it and focuses the first matching contenteditable in the whole document. Messaging pages can contain more than one editable surface or draft, so this can type into a different box than the active thread composer and then click a send button elsewhere or fail to send the intended reply.
How can I resolve this? If you propose a fix, please make it concise.| if not await self._message_text_visible(message): | ||
| return self._message_action_result( | ||
| self._page.url, | ||
| "send_unavailable", | ||
| "LinkedIn did not confirm that the message was sent.", | ||
| recipient_selected=recipient_selected, | ||
| ) |
There was a problem hiding this comment.
Confirmation can preexist
_message_text_visible checks whether the whole page body contains the message text. In an existing conversation, the same text may already be visible from a previous message or quoted content before this send attempt, so a disabled send button or failed submit can still return status: "sent" and sent: true.
Prompt To Fix With AI
This is a comment left during a code review.
Path: linkedin_mcp_server/scraping/extractor.py
Line: 3252-3258
Comment:
**Confirmation can preexist** `_message_text_visible` checks whether the whole page body contains the message text. In an existing conversation, the same text may already be visible from a previous message or quoted content before this send attempt, so a disabled send button or failed submit can still return `status: "sent"` and `sent: true`.
How can I resolve this? If you propose a fix, please make it concise.
Problem
The
send_messagetool fails for non-connected users because LinkedIn does not show a 'Message' button on their profile pages (see issues #433, #441). This makes it impossible to reply to InMails from recruiters, people you're not connected with, or any conversation where you only have a thread URL.Solution
Added a
thread_idparameter tosend_message. When provided, it navigates directly to the existing conversation thread and sends the message there, bypassing the Message-button lookup entirely.How it works:
thread_idviaget_inboxorsearch_conversationssend_messagewiththread_id— the tool navigates to the thread, finds the compose box, types and sends the messagePriority order:
thread_id— direct thread navigation (best for replies)profile_urn— direct compose URL from profile URNlinkedin_username— profile page + Message button lookup (existing behavior)Changes
messaging.py: Addedthread_idparameter to thesend_messagetool with updated docstringextractor.py: Added thread-based sending path (~105 lines) that reuses existing compose box resolution, keyboard typing, and send button logicUsage
Testing
Verified locally — successfully sent a reply to an InMail conversation from a non-connected recruiter.