MCP Servers

模型上下文协议服务器、框架、SDK 和模板的综合目录。

Free MIT-licensed MCP server from HiredSignal — resolve real ATS apply URLs, search LinkedIn jobs with full filter coverage + pagination, drive Easy Apply, via your real Brave session over CDP.

创建于 5/21/2026
更新于 about 18 hours ago
Repository documentation and setup instructions

hs-linkedin-mcp

A full LinkedIn agent kit, free and open-source

License: MIT Python 3.12+ Sponsored by HiredSignal

Sponsored by HiredSignal — built to find you a job, not to find candidates for a position. A job-search platform for job seekers, with an MCP for AI agents. Every role. Timestamped. Verified.


What this is

An MCP server that drives your real, signed-in Brave browser over the Chrome DevTools Protocol to do the things LinkedIn doesn't let regular scrapers do cleanly:

  • Resolve a LinkedIn job posting to its real ATS apply URL (Greenhouse, Ashby, Workday, Lever, Avature, company careers, etc).
  • Search jobs with full filter coverage across LinkedIn's 19 filter dimensions, with real pagination, plus client-side filters LinkedIn doesn't even expose.
  • Search the LinkedIn feed for hiring posts — find recruiters / hiring managers who just posted about an opening, with their profile URL ready for outreach.
  • Walk an Easy Apply modal and dump every field, then optionally drive it to completion with your answers.
  • Send a connection request or message to anyone you find — with dry_run=True by default so you preview every action before it commits.

No headless tricks. No stealth library. No cookie theft. LinkedIn sees the same fingerprint, the same session, and the same human it always has — because that's literally what's there.


Why HiredSignal built this

HiredSignal exists to help you find a job. Not to help a recruiter find you.

We're a job-search platform built for candidates. We aggregate openings from across the internet, verify and timestamp them, and expose them through a public job board and an MCP for AI agents. Our incentives are aligned with yours — we win when you land a role, not when we sell your profile.

To do this well we needed a clean way to operate against LinkedIn data without fighting a scraper-vs-bot-detection arms race. The cleanest answer turned out to be: don't be a bot. Use your own logged-in browser, just automate the clicking. Everything in this repo is the result of that approach packaged into an MCP, polished, documented, and given away.

If this is useful to you, HiredSignal is the same philosophy at the job-board scale — search across hundreds of sources with intent-driven ranking, all from the job seeker's side of the table.


The tool surface (10 tools)

| Tool | What it does | |---|---| | resolve_apply_url(job_id) | Click the LinkedIn Apply button, follow LinkedIn's /safety/go redirector, and return the real external ATS URL (Greenhouse, Ashby, Workday, etc). Distinguishes external / easy_apply / none (closed) and surfaces a closure reason when present. | | search_jobs(...) | LinkedIn job search with all 19 filter dimensions (date, work-type, job-type, experience, easy-apply, has-verifications, under-10-applicants, in-your-network, fair-chance-employer, company, industry, function, title, location, salary bucket, benefits, commitments, distance, sort) plus 4 client-side filters (exclude/require keywords, exclude/include companies, title regex, posted-within-hours, salary parsing from body, strict work-type) and real pagination via &start=N. | | search_hiring_posts(...) | LinkedIn content search filtered to "Job posts" (contentType=["jobs"]). Returns recruiters/managers posting about hiring, with author name, title, profile URL, post snippet, embedded job card, and posted-ago label. Sort by latest or relevance. Dedupe across reposts. | | discover_search_filters(keywords, location) | Live-introspect LinkedIn's "All filters" panel and return every option's label + numeric ID + URL param. Use this to get the IDs to plug into search_jobs. | | easy_apply_inspect(job_id) | Walk the Easy Apply multi-step modal/page and return the full form schema per step — every field, type, required flag, options, current value. Does NOT submit. Discards on exit. | | easy_apply_submit(job_id, answers, dry_run=True) | Drive the Easy Apply flow with your answers. Defaults to dry_run=True (discards at the submit step). Set dry_run=False to actually apply. | | send_message(profile_url, message, dry_run=True) | Open the LinkedIn message composer on a profile, type your message. Defaults to dry_run=True (doesn't click Send). | | connect_with(profile_url, note=None, dry_run=True) | Send a connection request, optionally with a 300-char personal note. Defaults to dry_run=True. | | get_saved_jobs() | Return your LinkedIn "My items → Saved jobs" list. | | status() | Report whether the MCP can attach to your Brave CDP endpoint right now. |


How it works

                 ┌─────────────────────────┐
  Your AI agent  │                         │
       │        │   hs-linkedin-mcp        │
       │  MCP   │   (Python, ~700 LOC)    │
       ▼  stdio │                         │
  ┌─────────┐   │  Playwright via CDP     │
  │  Claude │ ──▶                         │
  │  / etc  │   │       attach            │
  └─────────┘   └────────────┬────────────┘
                             │ http://127.0.0.1:9222
                             ▼
                ┌──────────────────────────┐
                │   Brave Browser          │
                │   (your real session,    │
                │    signed in to          │
                │    linkedin.com)         │
                └──────────────────────────┘

You launch Brave once with --remote-debugging-port=9222, sign into LinkedIn normally, and leave it open. The MCP attaches to that browser via CDP whenever called and reuses its real cookies, fingerprint, and network identity.


Install

git clone https://github.com/michaeltabet/hs-linkedin-mcp.git
cd hs-linkedin-mcp

# with uv (recommended)
uv sync
uv run playwright install chromium

# or with pip
pip install -e .
playwright install chromium

Launch Brave with CDP enabled

macOS

open -na "Brave Browser" --args --remote-debugging-port=9222

Linux

brave-browser --remote-debugging-port=9222

Windows

"C:\Program Files\BraveSoftware\Brave-Browser\Application\brave.exe" --remote-debugging-port=9222

Sign in to linkedin.com in that window and leave it open.

Chrome / Chromium / Edge work too. Brave is what we test against.

Register with your MCP client

Claude Code

claude mcp add hs-linkedin --command "uv --directory /path/to/hs-linkedin-mcp run hs-linkedin-mcp"

Claude Desktop / Cursor / any MCP host

{
  "mcpServers": {
    "hs-linkedin": {
      "command": "uv",
      "args": ["--directory", "/path/to/hs-linkedin-mcp", "run", "hs-linkedin-mcp"]
    }
  }
}

Examples

1. Resolve a LinkedIn job to the real ATS URL

resolve_apply_url(job_id="4330754680")
# →
{
  "job_id": "4330754680",
  "linkedin_url": "https://www.linkedin.com/jobs/view/4330754680/",
  "kind": "external",
  "apply_url": "https://jobs.ashbyhq.com/deliveroo/41a644ee-…?utm_source=LinkedInPaid"
}

2. Search jobs with rich filters + pagination

search_jobs(
    keywords="platform engineer",
    location="London",
    date_posted="past_week",
    work_type="hybrid,remote",
    experience_level="mid_senior,director",
    salary_min_bucket=44,                           # £50k+
    industry_ids=[4, 96],                            # Software Dev + IT Services
    title_ids=[6483],                                # Platform Engineer
    in_your_network=True,
    exclude_keywords=["staff", "VP", "Head of"],
    exclude_companies=["Tata", "Infosys"],
    title_regex_include=r"\b(Platform|SRE|DevOps)\b",
    posted_within_hours=72,
    fetch_details=True,
    max_pages=5,
)

Returns { search_url, pages_walked, total_seen, returned, jobs: [...] }.

3. Find recruiters posting about hiring

search_hiring_posts(
    keywords="hiring senior software engineer London",
    date_posted="past_week",
    sort_by="latest",
    exclude_recruiters=False,        # set True to filter out agency recruiters
    only_with_embedded_job=True,     # only posts with an attached job card
    posted_within_hours=48,
)
# → list of authors with profile URLs, titles, post bodies, embedded job info

4. Discover the numeric IDs you need

discover_search_filters(keywords="data engineer", location="Berlin")
# →
{
  "filters": [
    {"legend": "Industry filter",
     "options": [
       {"label": "Software Development", "value": "4",  "param": "f_I"},
       {"label": "IT Services and IT Consulting", "value": "96", "param": "f_I"},
       ...
     ]},
    ...
  ]
}

5. Inspect an Easy Apply form before answering

easy_apply_inspect(job_id="4415910815")
# → multi-step schema with every field, type, label, required flag, options

6. Drive Easy Apply (dry-run first)

easy_apply_submit(
    job_id="4415910815",
    answers={"qx-...": "8", "qy-...": "Yes", ...},
    dry_run=True,    # discards at final step. dry_run=False to actually apply.
)

7. Outreach — message + connect

send_message(
    profile_url="https://www.linkedin.com/in/some-recruiter/",
    message="Hi! Saw your post about the Platform Engineer role at Acme — I'd love to chat.",
    dry_run=True,
)

connect_with(
    profile_url="https://www.linkedin.com/in/some-recruiter/",
    note="Hi! Saw your post about Platform Engineer at Acme — would love to connect.",
    dry_run=True,
)

dry_run=True (default) opens the composer/dialog and types everything, but does not click Send. Flip to False only when you're sure.


LinkedIn filter reference

Stable URL-param filters (pass by argument):

| Param | Filter | Sample values | |---|---|---| | keywords | search query | free text | | location | location | free text | | f_TPR | date posted | r3600 past hour, r86400 past day, r604800 past week, r2592000 past month | | f_WT | work type | 1 on-site, 2 remote, 3 hybrid | | f_JT | job type | F full-time, P part-time, C contract, T temp, V volunteer, I internship, O other | | f_E | experience | 1 internship → 6 executive | | f_F | function | eng, it, fin, prdm, dsgn, mgmt, sale, cnsl, mnfc, othr | | f_SB2 | salary bucket | 41 £20k+ → 49 £100k+ | | f_AL | easy apply / actively hiring | true | | f_VJ | has verifications | true | | f_EA | under 10 applicants | true | | f_JIYN | in your network | true | | f_FCE | fair chance employer | true | | sortBy | sort order | DD date, R relevance | | distance | radius | int (mi US / km elsewhere) | | start | pagination offset | 0, 25, 50, … |

Dynamic-ID filters (use discover_search_filters to find the IDs):

| Param | Filter | |---|---| | f_C | company (numeric LinkedIn ID) | | f_I | industry | | f_T | job title | | f_PP | populated place / location | | f_BE | benefits | | f_CM | commitments |

All 21 of these are verified live against the current LinkedIn UI (every URL param was confirmed by toggling the corresponding filter and reading the resulting URL).


Privacy and safety

  • No credentials are read by this MCP. Auth lives entirely in your Brave profile. The MCP uses CDP to drive an already-signed-in window. It never reads your storage, never logs you in, never stores anything.
  • No bot-detection evasion. The browser is real Brave with your real fingerprint. LinkedIn sees a real human session because there is one.
  • Every state-changing tool defaults to dry_run=True. easy_apply_submit, send_message, connect_with all open their respective composers and type your input but do not click Send unless you opt in explicitly.
  • You own ToS compliance. Use this responsibly. LinkedIn's Terms of Service apply to whatever you do with the browser session. This MCP is a thin automation layer, not permission to abuse the platform.

Why pair it with HiredSignal

Three problems job seekers hit on LinkedIn:

  1. Apply URLs are obscured. LinkedIn rewrites every external apply to /safety/go which strips its destination on direct visits.
  2. Filters are leaky. "Remote" returns hybrid. "Past 24 hours" returns older. No way to exclude keywords. No salary-in-body match.
  3. You're stuck inside one platform. Even with all of the above solved, you're only seeing what LinkedIn has — not what every other job board has.

hs-linkedin-mcp solves (1) and (2) for the LinkedIn side. HiredSignal solves (3) — a job-search platform built for job seekers, indexing roles from hundreds of sources into a single vector-searchable, timestamped, verified store with an MCP of its own.

Use this MCP alone if all you care about is LinkedIn. Pair it with HiredSignal if you want one search across the entire job market — built for the candidate, not the recruiter.


Contributing

Issues and PRs welcome at github.com/michaeltabet/hs-linkedin-mcp.

If LinkedIn changes a selector or URL param, the most useful PR is one that ships a fresh discover_search_filters output plus the updated mapping — see the verification scripts in the commit history for the pattern.


License

MIT — see LICENSE. Use it however you like.


Built and sponsored by HiredSignal — built to find you a job.

快速设置
此服务器的安装指南

安装包 (如果需要)

uvx hs-linkedin-mcp

Cursor 配置 (mcp.json)

{ "mcpServers": { "michaeltabet-hs-linkedin-mcp": { "command": "uvx", "args": [ "hs-linkedin-mcp" ] } } }