linkedin-mcp
# LinkedIn MCP
An MCP server that lets an AI assistant search LinkedIn jobs and read LinkedIn
profiles, by driving a real Chrome window over the DevTools protocol.
It is **read-only**. There is no tool here that connects, messages, endorses,
posts or applies — not as a setting, but because the code to do it does not
exist. That is a deliberate line: LinkedIn treats automated *writing* far more
harshly than automated reading, and mixing the two is what gets accounts
restricted.
## Tools
| Tool | What it returns |
| --- | --- |
| `linkedin_search_jobs` | Listings with title, company, location, workplace type, salary when posted, posted age, and `job_id`. Filters: location, posted-within-days, experience level, job type, workplace type, Easy Apply, sort order. |
| `linkedin_get_job` | One posting in full — company, location, LinkedIn's job insights, and the whole description with the "See more" clamp expanded. |
| `linkedin_get_profile` | Name, headline, location, about text, experience history, education and skills. |
| `linkedin_check_session` | Whether the saved browser session is still signed in. |
| `linkedin_close_session` | Detaches from Chrome, leaving the window and session alone. |
## Install
Requires Python 3.12+, [uv](https://docs.astral.sh/uv/), and Google Chrome.
```bash
git clone https://github.com/TSS99/linkedin-mcp.git
cd linkedin-mcp
uv sync
```
Sign in once. This opens a Chrome window and waits for you to log in by hand —
credentials are never typed by the script, because LinkedIn challenges scripted
credential entry:
```bash
uv run linkedin-mcp --login
```
The session persists in `~/.linkedin-mcp/chrome-profile`. Check it any time with
`uv run linkedin-mcp --check`, clear it with `uv run linkedin-mcp --logout`.
## Which Chrome profile it drives
By default the server opens a **throwaway profile of its own**. That window will
look brand new — no bookmarks, no extensions, signed into nothing but LinkedIn.
That is the point: the server can only ever see LinkedIn.
If you would rather it drove **your everyday Chrome**, so it reuses the LinkedIn
session you already have and does not open a stranger window, set:
```bash
LINKEDIN_MCP_PROFILE_DIR=system
```
or point it at any profile directory explicitly. In an MCP client config that
goes in the `env` block:
```json
{
"mcpServers": {
"linkedin": {
"command": "uv",
"args": ["--directory", "/path/to/linkedin-mcp", "run", "linkedin-mcp"],
"env": { "LINKEDIN_MCP_PROFILE_DIR": "system" }
}
}
}
```
Two things to understand before you do:
**You still have to restart Chrome once.** `--remote-debugging-port` only takes
effect at launch; it cannot be switched on for a process that is already
running. So a Chrome that is open right now can never be attached to, whatever
the profile setting says. Quit it fully (Cmd+Q, not just closing the window)
and let the server start it, or start it yourself with the command
`--login` prints.
**The debug port is not LinkedIn-scoped.** Anything that can reach
`127.0.0.1:9224` can drive *every* tab in that Chrome and read *every* session
in it — your mail, your bank, all of it — not just LinkedIn. On the isolated
profile there is nothing else to reach. On your everyday profile there is
everything. Run it that way only on a machine you trust, and close the debug
Chrome when you are done.
`--logout` refuses to delete a profile it did not create, so pointing this at
your real Chrome cannot wipe your browser state.
## Configure
Add to your MCP client config (see `mcp-config.example.json`):
```json
{
"mcpServers": {
"linkedin": {
"command": "uv",
"args": ["--directory", "/path/to/linkedin-mcp", "run", "linkedin-mcp"]
}
}
}
```
For Claude Code: `claude mcp add linkedin -- uv --directory /path/to/linkedin-mcp run linkedin-mcp`
## How it works, and why
**CDP against a real Chrome profile, not a launched browser.** Playwright's own
Chromium advertises itself in a dozen ways LinkedIn checks. Attaching to a real
Chrome you logged into yourself keeps the fingerprint and the cookie honest.
The server runs on debug port `9224`, so it coexists with other browser-driven
MCP servers on 9222/9223.
**Navigations are paced.** LinkedIn rate-limits on cadence, not just volume, so
`goto` enforces a jittered minimum gap. A long research run is slower than it
could be, on purpose.
**Every string in LinkedIn's DOM appears twice** — once visible, once in a
`visually-hidden` span for screen readers — so raw `innerText` reads as
"Acme Corp\nAcme Corp". `dedupe_lines` collapses consecutive repeats; without
it every parsed field comes out doubled.
**Selectors are ordered fallback lists.** LinkedIn's class names are obfuscated
and renamed often. Where possible the parsers anchor on things that have
outlived the class churn — the `data-occludable-job-id` attribute, the stable
`div#experience` anchor ids — and fall back to parsing line order out of card
text rather than trusting a per-field hook.
**A broken scrape never returns an empty result.** Every tool is wrapped so a
missing anchor comes back as `{"status": "error", "error_type":
"stale_selector", ...}` naming the selector and URL. An LLM handed `[]` will
report "no jobs found" with total confidence; this makes tool breakage and
genuine emptiness distinguishable.
## Tests
```bash
uv run pytest
```
The tests cover the pure parsers — URL/filter construction, card parsing,
profile entry parsing, line deduplication — against fixtures in the shape
LinkedIn's DOM actually produces. They need no browser and no network.
## When it breaks
It will. LinkedIn ships markup changes constantly, and a scraper is a guess
about someone else's HTML. Expect `stale_selector` errors eventually; the fix
is usually one more entry in a selector list in `tools/jobs.py` or
`tools/profiles.py`, plus a fixture in `tests/test_parsers.py`.
If you get `auth_required` with a `checkpoint` URL, LinkedIn wants a human:
open the Chrome window, clear the challenge, and retry.
## A word on terms of use
Automated access is against LinkedIn's User Agreement regardless of how careful
the implementation is, and LinkedIn does restrict accounts for it. This is a
personal research tool; read-only design and paced requests reduce the risk but
do not eliminate it, and the account you point it at is the one that carries
that risk. Use your judgement.
## Licence
Apache-2.0
TDQS
Scored across 5 tools
Each tool has a clearly distinct purpose: searching jobs, fetching job details, reading profiles, and managing session state. No overlap between the data-access tools and the session-management tools.
All five tools follow a consistent 'linkedin_<verb>_<noun>' pattern (search_jobs, get_job, get_profile, check_session, close_session). The naming is uniform and predictable.
Five tools is well within the ideal range for a focused server. Every tool contributes to a clear workflow: session management, job search, job details, and profile lookup.
The server covers the core read workflows for LinkedIn jobs and profiles, but lacks profile search or listing capabilities. However, for its stated purpose (searching jobs and reading profiles), the surface is largely complete.