linkedin-job-mcp
# LinkedIn Jobs MCP
LinkedIn job search and full job details for your AI apps, plus **Ghost Job Radar**, a dashboard that shows which
job postings are likely not really open. A Windows app connects itself to Claude Code, Codex, VS Code and other MCP
clients; the same tools also come as an npm package, a REST API and a command line.
[](https://www.npmjs.com/package/linkedin-jobs-mcp)
[](https://www.npmjs.com/package/linkedin-jobs-mcp)
[](https://github.com/GhoshSrinjoy/linkedin-job-mcp/actions/workflows/node.js.yml)
[](https://github.com/GhoshSrinjoy/linkedin-job-mcp/blob/main/LICENSE)

## Tools
| Tool | What your AI app gets |
|------|-----------------------|
| `search_linkedin_jobs` | Jobs by keyword and location, filtered by date, job type, remote work or fewer than 10 applicants |
| `get_linkedin_job_details` | One job's full description, seniority, employment type, industries, posting age and applicant count |
| `check_ghost_job` | Ghost Job Radar's grade for one posting: how likely it is a ghost job, your chance against the competition, and the facts behind it. Needs the radar running |
It reads LinkedIn's public job pages, so it needs no account and no browser. Requests are spaced out and cached.
## Ghost Job Radar
A dashboard on your computer that grades every job it sees for ghost-job risk: postings that stay up far longer than
usual for their occupation, reposts with unchanged text, jobs gone from the employer's own site, start dates long
past, talent-pool wording, layoff news while still hiring, and what you know about the employer. Every job gets three
separate verdicts: ghost-risk, your chance, and whether the employer answers.

```bash
linkedin-jobs-mcp radar
```
With the Windows app, choose **5** in its menu or run `.\linkedin-jobs-mcp.exe radar`. It needs
[Python 3.11 or newer](https://www.python.org/downloads/). The first start sets up the radar's Python environment;
every start also runs the LinkedIn source, so LinkedIn search and job pages work right away. Your data stays on your
computer, in `%LOCALAPPDATA%\linkedin-jobs-mcp\radar` (macOS: `~/Library/Application Support/linkedin-jobs-mcp/radar`,
Linux: `~/.local/share/linkedin-jobs-mcp/radar`). A `.env` file there switches on more sources, such as the
Bundesagentur job board or Google for Jobs; [radar/README.md](radar/README.md) explains every view, signal and source.

## Install
### Windows app
1. Download `linkedin-jobs-mcp.exe` from the [latest release](https://github.com/GhoshSrinjoy/linkedin-job-mcp/releases/latest).
It is one file with Node.js inside.
2. Double-click it. The file is not code-signed, so Windows may say "Windows protected your PC": choose
**More info → Run anyway**.
3. Choose **1 Install and connect the apps found**. The app copies itself to
`%LOCALAPPDATA%\Programs\linkedin-jobs-mcp\` and adds itself to every supported app it finds.
4. Restart those apps. The app has no window of its own: they start it in the background when they need the tools.

### npm (Windows, macOS, Linux)
Needs Node.js 18 or newer.
```bash
npm install -g linkedin-jobs-mcp
linkedin-jobs-mcp connect
```
Or add it to an app by hand, through npx:
```json
{
"mcpServers": {
"linkedin-jobs": { "command": "npx", "args": ["-y", "linkedin-jobs-mcp"] }
}
}
```
On Windows, some apps need `"command": "cmd", "args": ["/c", "npx", "-y", "linkedin-jobs-mcp"]`.
### From source
```bash
git clone https://github.com/GhoshSrinjoy/linkedin-job-mcp.git
cd linkedin-job-mcp
npm install
npm run app -- connect
```
## Connect your apps
| App | Name for `connect` | Where the setting goes |
|-----|--------------------|------------------------|
| Claude Code | `claude-code` | `claude mcp add --scope user` |
| Codex | `codex` | `~/.codex/config.toml` |
| VS Code | `vscode` | `%APPDATA%\Code\User\mcp.json` |
| VS Code Insiders | `vscode-insiders` | `%APPDATA%\Code - Insiders\User\mcp.json` |
| Claude Desktop | `claude-desktop` | `%APPDATA%\Claude\claude_desktop_config.json` |
| Cursor | `cursor` | `~/.cursor/mcp.json` |
| Windsurf | `windsurf` | `~/.codeium/windsurf/mcp_config.json` |
| Gemini CLI | `gemini-cli` | `~/.gemini/settings.json` |
| LM Studio | `lm-studio` | `~/.lmstudio/mcp.json` |
| Cline | `cline` | `%APPDATA%\Code\User\globalStorage\saoudrizwan.claude-dev\settings\cline_mcp_settings.json` |
`connect` only adds or replaces the `linkedin-jobs` entry: other servers and settings stay, and the first change keeps a
copy of the file next to it (`<file>.linkedin-jobs.bak`). A settings file with comments is left unchanged, and the
entry to add is printed instead.

### Commands
| Command | What it does |
|---------|--------------|
| `linkedin-jobs-mcp` | The setup menu in a console window; the MCP server when an app starts it |
| `linkedin-jobs-mcp install` | Copy the Windows app to `%LOCALAPPDATA%\Programs\linkedin-jobs-mcp\` and connect the apps found |
| `linkedin-jobs-mcp connect [app ...]` | Connect the apps found, or only the named ones (also when they are not found) |
| `linkedin-jobs-mcp disconnect [app ...]` | Remove the connection from every app, or only from the named ones |
| `linkedin-jobs-mcp status` | Which apps are found and connected |
| `linkedin-jobs-mcp config [app ...]` | Print the settings to add by hand |
| `linkedin-jobs-mcp serve [--port 3000] [--host 127.0.0.1]` | REST API plus MCP over HTTP |
| `linkedin-jobs-mcp mcp` | MCP over stdio (what the connected apps run) |
| `linkedin-jobs-mcp job <id or URL>` | One job's details as JSON |
| `linkedin-jobs-mcp search --keyword "data engineer" --location "Berlin, Germany"` | Search from the command line |
| `linkedin-jobs-mcp radar [--port 8000] [--no-browser]` | Open Ghost Job Radar |
| `linkedin-jobs-mcp uninstall` | Disconnect every app and remove the installed Windows app |
With the Windows app, run them from its folder, for example `.\linkedin-jobs-mcp.exe status`.
### Add it by hand
Every app needs the same thing: the program, started with the `mcp` argument. VS Code (`mcp.json`):
```json
{
"servers": {
"linkedin-jobs": {
"type": "stdio",
"command": "C:\\Users\\you\\AppData\\Local\\Programs\\linkedin-jobs-mcp\\linkedin-jobs-mcp.exe",
"args": ["mcp"]
}
}
}
```
Claude Desktop, Cursor, Windsurf, Gemini CLI, LM Studio and Cline take the same entry under `"mcpServers"`, without
`"type"`. Codex (`config.toml`):
```toml
[mcp_servers.linkedin-jobs]
command = "C:\\Users\\you\\AppData\\Local\\Programs\\linkedin-jobs-mcp\\linkedin-jobs-mcp.exe"
args = ["mcp"]
```
Claude Code:
```bash
claude mcp add --scope user linkedin-jobs -- "C:\Users\you\AppData\Local\Programs\linkedin-jobs-mcp\linkedin-jobs-mcp.exe" mcp
```
Ollama, through [mcp-client-for-ollama](https://github.com/jonigl/mcp-client-for-ollama): save the `mcpServers`
entry in a file and start `ollmcp -j servers.json`.
`linkedin-jobs-mcp config` prints all of these with the real path.
### MCP over HTTP
Apps that connect by URL can use the HTTP server instead. Start `linkedin-jobs-mcp serve` (or `npm run api`) and add
`http://127.0.0.1:3000/mcp`:
```bash
claude mcp add --transport http linkedin-jobs http://127.0.0.1:3000/mcp
```
```json
{ "servers": { "linkedin-jobs": { "type": "http", "url": "http://127.0.0.1:3000/mcp" } } }
```
The server listens on this computer only and refuses requests that name another host, so web pages cannot reach it
through DNS rebinding. `HOST=0.0.0.0` makes it reachable from other machines, for example from a container; protect
it yourself then.
## Search options
| Option | Values |
|--------|--------|
| `keyword` | Job title or keywords, e.g. `data engineer` |
| `location` | City, region or country, e.g. `Berlin, Germany` |
| `city`, `country` | Narrow the location |
| `geoId` | LinkedIn location id, skips the location lookup |
| `dateSincePosted` | `past 24 hours`, `past week`, `past month` |
| `jobType` | `full time`, `part time`, `contract`, `temporary`, `volunteer`, `internship` |
| `remoteFilter` | `on site`, `remote`, `hybrid` |
| `under_10_applicants` | Jobs with fewer than 10 applicants |
| `limit` | Number of jobs (default 10; 25 in the REST API) |
| `sortBy` | `recent` or `relevant` |
| `page` | Page of results, from 0; a page is `limit` jobs long |
The REST API accepts the same options. LinkedIn's public search ignores salary, experience level and verified-job
filters, so there are none.
## Responses
A search returns:
```json
{
"success": true,
"total_jobs": 1,
"jobs": [
{
"position": "Data Engineer (m/w/d)",
"company": "Example GmbH",
"location": "Berlin, Berlin, Germany",
"date": "2026-10-20",
"agoTime": "4 days ago",
"salary": "Not specified",
"jobUrl": "https://www.linkedin.com/jobs/view/1234567890/",
"companyLogo": "https://media.licdn.com/…"
}
],
"search_params": { "keyword": "data engineer", "location": "Berlin, Germany", "limit": "10" }
}
```
Job details return:
```json
{
"id": "1234567890",
"position": "Data Engineer (m/w/d)",
"company": "Example GmbH",
"location": "Berlin, Berlin, Germany",
"postedAgo": "3 weeks ago",
"applicants": "Over 200 applicants",
"salary": null,
"seniority": "Mid-Senior level",
"employmentType": "Full-time",
"jobFunction": null,
"industries": "Software Development",
"criteria": { "Seniority level": "Mid-Senior level", "Employment type": "Full-time", "Industries": "Software Development" },
"acceptingApplications": true,
"description": "We are looking for a data engineer to join our platform team. …",
"jobUrl": "https://www.linkedin.com/jobs/view/1234567890/"
}
```
A job LinkedIn no longer has returns `{ "id": "…", "removed": true }`.
## REST API
`npm run api` or `linkedin-jobs-mcp serve` starts it on `http://127.0.0.1:3000`.
| Endpoint | What it does |
|----------|--------------|
| `POST /search` | Search, with the options above as a JSON body; returns `{ "cached", "total", "jobs" }` |
| `GET /job/:id`, `GET /job?url=<link>` | One job's details; 404 when LinkedIn no longer has it, 400 for an invalid id or link |
| `POST /mcp` | MCP over Streamable HTTP |
| `GET /health` | `{ "ok": true }` |
## Library
```js
const { query, getJobDetails } = require("linkedin-jobs-mcp");
const jobs = await query({ keyword: "data engineer", location: "Berlin, Germany", limit: "10" });
const job = await getJobDetails("https://www.linkedin.com/jobs/view/1234567890/");
```
## Configuration
Set these in the environment or in a `.env` file in the folder you start it from.
| Variable | Default | What it sets |
|----------|---------|--------------|
| `PORT` | `3000` | Port of the REST API and MCP over HTTP |
| `HOST` | `127.0.0.1` | Address the server listens on |
| `LINKEDIN_REQUEST_TIMEOUT_MS` | `15000` | Timeout of one request to LinkedIn |
| `LINKEDIN_CACHE_TTL_MS` | `3600000` | How long search results stay cached |
| `LINKEDIN_DETAIL_MIN_INTERVAL_MS` | `1500` | Minimum gap between two job-page requests |
| `LINKEDIN_DETAIL_CACHE_TTL_MS` | `21600000` | How long job details stay cached |
| `LINKEDIN_USER_AGENT` | | User agent for job-page requests |
| `API_RATE_LIMIT_WINDOW_MS` | `60000` | Rate-limit window of the REST API |
| `API_RATE_LIMIT_MAX` | `60` | Requests per window and address |
## Build the Windows app
On Windows with Node.js 20 or newer:
```bash
npm install
npm run build:win # dist/linkedin-jobs-mcp.exe and its SHA-256
```
The build bundles the code with esbuild and injects it into a copy of `node.exe` (a Node.js single executable
application). Pushing a tag such as `v1.2.0` builds the app on GitHub Actions and attaches it to a release.
## Tests
```bash
npm test # job pages, settings files, the MCP server and the HTTP API
cd radar && python -m pytest # Ghost Job Radar
```
Both run offline.
## Repository layout
| Path | What it is |
|------|------------|
| `index.js` | LinkedIn search and job details |
| `server.js`, `api.js` | MCP server (stdio and HTTP) and REST API |
| `app.js`, `clients.js`, `radar.js` | The app: commands, client setup, starting the radar |
| `radar/` | Ghost Job Radar (Python): dashboard, scoring, research report and notes, browser extension |
| `scripts/build-win.js` | Builds the Windows app |
| `test/` | Tests of the Node part |
## Responsible use
The tools read public job pages slowly and cache what they read, for personal job searching and research. Automated
access can conflict with LinkedIn's User Agreement, so keep the volume low and use it at your own risk.
## License
[PolyForm Noncommercial 1.0.0](https://github.com/GhoshSrinjoy/linkedin-job-mcp/blob/main/LICENSE): free for personal, research and other non-commercial use. For commercial use, ask for permission.
Srinjoy Ghosh ([@GhoshSrinjoy](https://github.com/GhoshSrinjoy), ghoshsrinjoy97@gmail.com)
TDQS
Scored across 3 tools
Each tool maps to a distinct step in a job-hunting workflow: search a list, fetch one posting's full details, and grade a posting for ghost-job likelihood. The descriptions explicitly cross-reference each other (use get_linkedin_job_details on a jobUrl, check_ghost_job for openness), leaving no realistic path for misselection.
All three names use the same snake_case verb_noun pattern: search_linkedin_jobs, get_linkedin_job_details, check_ghost_job. Object naming is consistent (linkedin_job / job) and the verbs accurately signal the action.
Three tools cover the entire stated scope with no redundancy, which is defensible for a narrow, single-domain server. It sits at the lean edge of the scale, so users needing richer filtering must rely on the search tool's parameters alone.
The search → inspect → validate loop is fully closed, with salary, applicant count, and ghost-job grading all reachable. Minor gaps remain: no company-based lookup, no way to enumerate more than what a keyword search returns, and check_ghost_job hard-depends on a separately running local radar process.