Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ This is a **LinkedIn MCP (Model Context Protocol) Server** that enables AI assis
| `get_job_details` | Get job posting details |
| `search_jobs` | Search jobs by keywords and location |
| `close_session` | Close browser session and clean up resources |
| `search_people` | Search for people by keywords and location |

**Tool Return Format:**

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ What has Anthropic been posting about recently? https://www.linkedin.com/company
| `get_company_profile` | Extract company information with explicit section selection (posts, jobs) | Working |
| `get_company_posts` | Get recent posts from a company's LinkedIn feed | Working |
| `search_jobs` | Search for jobs with keywords and location filters | Working |
| `search_people` | Search for people by keywords and location | Working |
| `get_job_details` | Get detailed information about a specific job posting | Working |
| `close_session` | Close browser session and clean up resources | Working |

Expand Down
1 change: 1 addition & 0 deletions docs/docker-hub.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ A Model Context Protocol (MCP) server that connects AI assistants to LinkedIn. A
- **Company Profiles**: Extract comprehensive company data
- **Job Details**: Retrieve job posting information
- **Job Search**: Search for jobs with keywords and location filters
- **People Search**: Search for people by keywords and location
- **Company Posts**: Get recent posts from a company's LinkedIn feed

## Quick Start
Expand Down
28 changes: 28 additions & 0 deletions linkedin_mcp_server/scraping/extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,3 +383,31 @@ async def search_jobs(
"pages_visited": [url],
"sections_requested": ["search_results"],
}

async def search_people(
self,
keywords: str,
location: str | None = None,
) -> dict[str, Any]:
"""Search for people and extract the results page.

Returns:
{url, sections: {name: text}, pages_visited, sections_requested}
"""
params = f"keywords={quote_plus(keywords)}"
if location:
params += f"&location={quote_plus(location)}"

url = f"https://www.linkedin.com/search/results/people/?{params}"
text = await self.extract_page(url)

sections: dict[str, str] = {}
if text:
sections["search_results"] = text

return {
"url": url,
"sections": sections,
"pages_visited": [url],
"sections_requested": ["search_results"],
}
50 changes: 50 additions & 0 deletions linkedin_mcp_server/tools/person.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,53 @@ async def get_person_profile(

except Exception as e:
return handle_tool_error(e, "get_person_profile")

@mcp.tool(
annotations=ToolAnnotations(
title="Search People",
readOnlyHint=True,
destructiveHint=False,
openWorldHint=True,
)
)
async def search_people(
keywords: str,
ctx: Context,
location: str | None = None,
) -> dict[str, Any]:
"""
Search for people on LinkedIn.

Args:
keywords: Search keywords (e.g., "software engineer", "recruiter at Google")
ctx: FastMCP context for progress reporting
location: Optional location filter (e.g., "New York", "Remote")

Returns:
Dict with url, sections (name -> raw text), pages_visited, and sections_requested.
The LLM should parse the raw text to extract individual people and their profiles.
"""
try:
await ensure_authenticated()

logger.info(
"Searching people: keywords='%s', location='%s'",
keywords,
location,
)

browser = await get_or_create_browser()
extractor = LinkedInExtractor(browser.page)

await ctx.report_progress(
progress=0, total=100, message="Starting people search"
)

result = await extractor.search_people(keywords, location)

await ctx.report_progress(progress=100, total=100, message="Complete")

return result

except Exception as e:
return handle_tool_error(e, "search_people")
26 changes: 26 additions & 0 deletions tests/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def _make_mock_extractor(scrape_result: dict) -> MagicMock:
mock.scrape_company = AsyncMock(return_value=scrape_result)
mock.scrape_job = AsyncMock(return_value=scrape_result)
mock.search_jobs = AsyncMock(return_value=scrape_result)
mock.search_people = AsyncMock(return_value=scrape_result)
mock.extract_page = AsyncMock(return_value="some text")
return mock

Expand Down Expand Up @@ -128,6 +129,31 @@ async def test_get_person_profile_error(self, mock_context, monkeypatch):
result = await tool_fn("test-user", mock_context)
assert result["error"] == "session_expired"

async def test_search_people(self, mock_context, patch_tool_deps, monkeypatch):
expected = {
"url": "https://www.linkedin.com/search/results/people/?keywords=AI+engineer&location=New+York",
"sections": {"search_results": "Jane Doe\nAI Engineer at Acme\nNew York"},
"pages_visited": [
"https://www.linkedin.com/search/results/people/?keywords=AI+engineer&location=New+York"
],
"sections_requested": ["search_results"],
}
mock_extractor = _make_mock_extractor(expected)
monkeypatch.setattr(
"linkedin_mcp_server.tools.person.LinkedInExtractor",
lambda *a, **kw: mock_extractor,
)

from linkedin_mcp_server.tools.person import register_person_tools

mcp = FastMCP("test")
register_person_tools(mcp)

tool_fn = await get_tool_fn(mcp, "search_people")
result = await tool_fn("AI engineer", mock_context, location="New York")
assert "search_results" in result["sections"]
mock_extractor.search_people.assert_awaited_once_with("AI engineer", "New York")


class TestCompanyTools:
async def test_get_company_profile(
Expand Down