Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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")
27 changes: 27 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,32 @@ 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
Loading