search_people
Find LinkedIn profiles by name, job title, or keywords, with optional location filtering to identify professionals matching specific criteria.
Instructions
Search for people on LinkedIn.
Args: keywords: Search keywords (e.g., 'product manager', 'ML engineer at Meta') location: Optional location filter (e.g., 'London', 'Berlin')
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| keywords | Yes | ||
| location | No |
Implementation Reference
- MCP tool registration for 'search_people' - defines the tool interface with name, description, parameters (keywords, location), and error handling. Calls SearchPeopleUseCase.execute().
@mcp.tool( name="search_people", description=( "Search for people on LinkedIn.\n\n" "Args:\n" " keywords: Search keywords (e.g., 'product manager', 'ML engineer at Meta')\n" " location: Optional location filter (e.g., 'London', 'Berlin')" ), ) async def search_people( keywords: str, ctx: Context, location: str | None = None, ) -> dict[str, Any]: try: result = await search_people_uc.execute(keywords, location) return { "url": result.url, "sections": serialize_sections(result.sections), } except Exception as e: map_domain_error(e, "search_people") - Main use case implementation - SearchPeopleUseCase class with execute() method that builds LinkedIn search URL, fetches HTML via browser, parses search results, and returns ScrapeResponse.
class SearchPeopleUseCase: """Search for people on LinkedIn.""" def __init__(self, browser: BrowserPort, auth: AuthPort, *, debug: bool = False): self._browser = browser self._auth = auth self._debug = debug async def execute( self, keywords: str, location: str | None = None, ) -> ScrapeResponse: await self._auth.ensure_authenticated() params = f"keywords={quote_plus(keywords)}" if location: params += f"&location={quote_plus(location)}" url = f"https://www.linkedin.com/search/results/people/?{params}" content = await self._browser.extract_page_html(url) sections: dict[str, Any] = {} if content.html: sections["search_results"] = parse_section( "search_results", content.html, entity_type="search_people", include_raw=self._debug, ) return ScrapeResponse(url=url, sections=sections) - Domain models for search results - PersonSearchResult and PeopleSearchResults dataclasses defining the structure of parsed people search data including name, connection_degree, headline, location, profile_url, etc.
@dataclass class PersonSearchResult: """A single person result from people search.""" name: str connection_degree: str headline: str | None = None location: str | None = None current: str | None = None mutual_connections: str | None = None followers: str | None = None profile_url: str | None = None linkedin_username: str | None = None profile_image_url: str | None = None @dataclass class PeopleSearchResults: """People search results page.""" people: list[PersonSearchResult] = field(default_factory=list) raw: str | None = None - HTML parser implementation - parse_search_results_people() function that extracts person search results from LinkedIn's SDUI layout, parsing name, profile URL, connection degree, headline, location, mutual connections, and profile image.
def parse_search_results_people( html: str, *, include_raw: bool = False ) -> PeopleSearchResults: """Parse people search results page HTML. Extracts list of PersonSearchResult from SDUI search result cards. Each card is identified by data-view-name="people-search-result". """ soup = _soup(html) results: list[PersonSearchResult] = [] cards = soup.find_all( attrs={"data-view-name": "people-search-result"} ) for card in cards: # Profile URL and username from the main <a> link profile_link = card.find( "a", attrs={"data-view-name": "search-result-lockup-title"}, ) profile_url: str | None = None linkedin_username: str | None = None name: str = "" if profile_link: name = _text(profile_link) or "" href = profile_link.get("href", "") if href: profile_url = href m = _PROFILE_URL_RE.search(href) if m: linkedin_username = m.group(1) if not name: continue # Connection degree from <span class="_45102191"> connection_degree = "" degree_container = card.find( "span", class_=lambda c: c and "_45102191" in c ) if degree_container: degree_text = _text(degree_container) if degree_text: # Extract "1st", "2nd", "3rd" etc. m = re.search(r"(\d+(?:st|nd|rd|th))", degree_text) if m: connection_degree = m.group(1) # Profile image from <figure> with aria-label profile_image_url: str | None = None figure = card.find("figure", attrs={"data-view-name": "image"}) if figure: img = figure.find("img") if img: src = img.get("src", "") if src and "profile-displayphoto" in src: profile_image_url = src # Headline — first <p> with _37677861 class in name's parent headline: str | None = None location: str | None = None # The listitem div contains the name + headline + location in order listitem = card.find("div", attrs={"role": "listitem"}) if listitem: # Find all <p> with _37677861 class that are direct text content info_divs = listitem.find_all( "div", class_=lambda c: c and "_04bda81b" in c and "_9dfef8a0" in c and "_837488b5" in c, ) for i, div in enumerate(info_divs): p = div.find("p", class_=lambda c: c and "_37677861" in c) if p: text = _text(p) if text: if i == 0: headline = text elif i == 1: location = text # Mutual connections from social proof insight mutual_connections: str | None = None social_proof_links = card.find_all( "a", attrs={"data-view-name": "search-result-social-proof-insight"}, ) for sp_link in social_proof_links: sp_text = _text(sp_link) if sp_text and "mutual connection" in sp_text.lower(): mutual_connections = sp_text # Followers from social proof followers: str | None = None for sp_link in social_proof_links: sp_text = _text(sp_link) if sp_text and "follower" in sp_text.lower(): followers = sp_text results.append( PersonSearchResult( name=name, connection_degree=connection_degree, headline=headline, location=location, mutual_connections=mutual_connections, followers=followers, profile_url=profile_url, linkedin_username=linkedin_username, profile_image_url=profile_image_url, ) ) return PeopleSearchResults( people=results, raw=html if include_raw else None, ) - src/linkedin_mcp_server/adapters/driving/mcp_server.py:34-34 (registration)Server registration call - register_person_tools() invoked with container.search_people dependency, wiring the use case into the MCP server.
register_person_tools(mcp, container.scrape_person, container.search_people)