Skip to content

Commit 7f94680

Browse files
kgriteshclaude
andcommitted
feat(conversations): add send_message support with dry-run mode
New MCP tool and scraper method to send messages in existing or new LinkedIn conversations. Includes dry-run mode for verifying UI elements without actually sending. Covers both participant-name and profile-url targeting with multi-strategy DOM element detection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent cd6508d commit 7f94680

File tree

7 files changed

+400
-6
lines changed

7 files changed

+400
-6
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Added
11+
12+
- `send_message` tool — send messages in existing or new LinkedIn conversations with dry-run support
13+
814
## [v0.2.5] - 2026-03-19
915

1016
- No changes

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ A modern LinkedIn scraping library with built-in anti-detection, available as a
1616
- **Profile scraping** with experience, education, skills, and contact details
1717
- **Company scraping** with industry, size, specialties, and more
1818
- **Connection management** — retrieve and send connection requests
19-
- **Conversations** — list threads and read message history
19+
- **Conversations** — list threads, read message history, and send messages
2020
- **Proxy support** — route traffic through HTTP or SOCKS5 proxies
2121
- **Anti-detection** — human-like behavior simulation, stealth mode, session persistence
2222

@@ -73,7 +73,11 @@ What has OpenAI been posting about recently? https://www.linkedin.com/company/op
7373
Show me my pending connection requests and summarize who they are
7474
```
7575

76-
It provides 11 tools:
76+
```
77+
Send a message to John Doe saying "Thanks for connecting!"
78+
```
79+
80+
It provides 12 tools:
7781

7882
| Tool | Description |
7983
| ----------------------------- | ----------------------------------------------------------------------- |
@@ -86,6 +90,7 @@ It provides 11 tools:
8690
| `send_connection_request` | Send a connection request with optional note |
8791
| `scrape_conversations_list` | List messaging conversations |
8892
| `scrape_conversation` | Read messages from a conversation |
93+
| `send_message` | Send a message in an existing or new conversation |
8994
| `get_session_status` | Check if the browser session is active |
9095
| `reset_session` | Close and reset the browser session |
9196

@@ -241,6 +246,7 @@ scraper.send_connection_request("https://linkedin.com/in/someone", note="Hi!")
241246
# Conversations
242247
threads = scraper.scrape_conversations_list(max_results=10)
243248
messages = scraper.scrape_conversation_messages("John Doe")
249+
scraper.send_message("Thanks for connecting!", participant_name="John Doe")
244250

245251
# Always clean up
246252
scraper.close()

src/linkedin_spider/core/scraper.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,18 @@ def scrape_conversation_messages(
207207
"""Scrape messages from a conversation."""
208208
return self.conversation_scraper.scrape_conversation_messages(participant_name)
209209

210+
def send_message(
211+
self,
212+
message: str,
213+
participant_name: str | None = None,
214+
profile_url: str | None = None,
215+
dry_run: bool = False,
216+
) -> bool:
217+
"""Send a message in an existing or new conversation."""
218+
return self.conversation_scraper.send_message(
219+
message, participant_name, profile_url, dry_run=dry_run
220+
)
221+
210222
def send_connection_request(self, profile_url: str, note: str | None = None) -> bool:
211223
"""Send a connection request to a profile."""
212224
return self.connection_scraper.send_connection_request(profile_url, note)

src/linkedin_spider/mcp/server.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,50 @@ async def send_connection_request(profile_url: str, note: str | None = None) ->
291291
return f"Error sending connection request to {profile_url}: {e!s}"
292292

293293

294+
@mcp_app.tool()
295+
async def send_message(
296+
message: str,
297+
participant_name: str | None = None,
298+
profile_url: str | None = None,
299+
dry_run: bool = False,
300+
) -> str:
301+
"""Send a message in a LinkedIn conversation.
302+
303+
Args:
304+
message: The message text to send
305+
participant_name: Name of existing conversation participant (for existing conversations)
306+
profile_url: LinkedIn profile URL (for starting new conversations)
307+
dry_run: If True, verify message input and send button exist but do not send
308+
309+
Provide either participant_name or profile_url, not both.
310+
"""
311+
if not message or not message.strip():
312+
raise ValueError("message is required")
313+
314+
try:
315+
scraper = get_scraper()
316+
success = scraper.send_message(message, participant_name, profile_url, dry_run=dry_run)
317+
318+
if dry_run:
319+
status = "Dry run verification passed" if success else "Dry run verification failed"
320+
else:
321+
status = "Message sent successfully" if success else "Failed to send message"
322+
323+
result = {
324+
"message": message[:100],
325+
"participant_name": participant_name,
326+
"profile_url": profile_url,
327+
"dry_run": dry_run,
328+
"success": success,
329+
"status": status,
330+
}
331+
332+
return f"send_message_result:\n{json.dumps(result, indent=2, ensure_ascii=False)}"
333+
334+
except Exception as e:
335+
return f"Error sending message: {e!s}"
336+
337+
294338
@cli_app.command
295339
def serve(
296340
transport: Annotated[
@@ -359,7 +403,7 @@ def serve(
359403
logger.info(
360404
f"FastMCP {transport.upper()} Server initialized with tools: scrape_profile, search_profiles, scrape_company, "
361405
"search_posts, scrape_incoming_connections, scrape_outgoing_connections, scrape_conversations_list, "
362-
"scrape_conversation, send_connection_request, get_session_status, reset_session"
406+
"scrape_conversation, send_connection_request, send_message, get_session_status, reset_session"
363407
)
364408

365409
if transport in ["sse", "http", "streamable-http"]:

0 commit comments

Comments
 (0)