job-search
job-search MCP server
A local MCP server that scrapes job boards (Indeed, LinkedIn, ZipRecruiter, Google Jobs — via python-jobspy), filters postings against your saved criteria, dedupes against what you've already been shown, and tracks application status. Runs on your machine, registered with Claude Desktop.
1. Install dependencies
This has to be done in a real Terminal on your Mac (Cowork's sandboxed shell can't reach PyPI, so I couldn't run this step for you). One time:
cd ~/Projects/job-search-mcp
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtThat installs mcp, python-jobspy, and pandas into a virtualenv scoped
to this folder.
2. Smoke-test it
cd ~/Projects/job-search-mcp
.venv/bin/python -c "import server; print('imports ok')"If that prints imports ok with no traceback, the code is wired up
correctly. (It won't try to scrape anything yet — that only happens when a
tool is actually called.)
Optional: try a real scrape from the command line before wiring it into Claude Desktop:
.venv/bin/python -c "
from scraper import run_search
jobs = run_search('machine learning engineer', 'Remote', ['indeed'], hours_old=72, results_wanted=5)
for j in jobs:
print(j.get('title'), '-', j.get('company'), '-', j.get('job_url'))
"3. Register with Claude Desktop
Edit ~/Library/Application Support/Claude/claude_desktop_config.json
(create it if it doesn't exist) and add an entry under mcpServers:
{
"mcpServers": {
"job-search": {
"command": "/Users/mrtgaliakberov/Projects/job-search-mcp/.venv/bin/python",
"args": ["/Users/mrtgaliakberov/Projects/job-search-mcp/server.py"]
}
}
}If you already have other mcpServers entries, just add "job-search" as
another key alongside them — don't replace the whole file.
Then fully quit and reopen Claude Desktop. The server starts automatically whenever Claude Desktop launches it (you don't need to run it manually).
4. Try it
In a Claude Desktop chat:
"What are my saved job search criteria?" → calls
list_criteria"Get today's job matches" → calls
get_daily_matches"Search for AI engineer jobs in Seattle" → calls
search_jobs"Mark as applied" → calls
mark_applied"What have I marked as applied so far?" → calls
list_tracked
5a. Extra sources: ZipRecruiter, Dice, Indeed (connectors)
Beyond Indeed/LinkedIn (JobSpy), you can pull in results from Claude's native ZipRecruiter, Dice, and Indeed connectors if you've connected them (claude.ai connector settings). They hit real provider APIs, not scraping -- no blocking, no ToS risk.
They're a different mechanism than the job-search MCP server: Claude
calls the connector's search_jobs tool itself, then passes the raw
response to one of this server's record_zip_recruiter_matches /
record_dice_matches / record_indeed_matches tools, which normalizes
it and runs it through the same filter/dedupe/store pipeline
get_daily_matches uses -- same seen_jobs table, same dashboard, same
application tracker. Just ask in chat, e.g.:
"Search ZipRecruiter for machine learning engineer jobs, remote, entry-level, and record the matches"
ZipRecruiter's connector has real seniority_classes
(NO_EXPERIENCE/JUNIOR/MID/SENIOR) and location_types (REMOTE/HYBRID)
filters -- worth leaning on those directly rather than relying on this
project's title-keyword filtering, since they're much more precise.
Dice and Indeed's connectors don't have that, so the usual
title_exclude_keywords filtering still applies to their results.
The daily scheduled task also tries all three connectors automatically
if they're available in that session (see CLAUDE.md for the current
status of that -- it's unverified as of when this was written).
5. Browse matches in a dashboard
Once you've run get_daily_matches at least once, browse everything it's
found so far in a local web page:
python3 dashboard.pyOpens http://127.0.0.1:8765 in your browser automatically. Search,
filter by status or remote-only, sort any column, and change a job's
status (interested / applied / interviewing / rejected / skipped) right
from the table -- it writes to the same state.db mark_applied uses,
so status stays in sync whichever way you set it.
No install needed for this part -- it's stdlib only, doesn't touch
.venv. --port <n> to use a different port, --no-browser to skip
auto-opening.
Tools
Tool | Purpose |
| Ad-hoc search, no dedupe/state side effects |
| Full saved-criteria search → filter → dedupe → mark seen. This is what the daily scheduled check calls. |
| View/edit what counts as a match |
| Lightweight application tracker |
| Counts: jobs seen, applications tracked, by status |
Default criteria
Titles matched: anything containing an AI/ML term (machine learning, AI engineer, applied AI/ML, research engineer, etc. — any seniority), OR a general SWE term (software engineer, developer) AND an entry-level signal (new grad, junior, entry-level, associate, early career).
Excluded: senior/staff/principal/lead/director/manager/architect titles, regardless of the above.
Search terms: "machine learning engineer", "AI engineer", "software engineer new grad"
Locations: "Remote", "San Jose, CA" — no hard location filter beyond that (I left this broad since you said Remote / Bay Area / anywhere in the US all worked for you); edit
locationsviaupdate_criteriato narrow it.Sites: Indeed, LinkedIn, ZipRecruiter, Google Jobs, Glassdoor. In practice, only Indeed and LinkedIn actually return results right now — ZipRecruiter, Google, and Glassdoor all come back empty (no error, just 0 results) due to upstream JobSpy scraper issues (see GitHub #302 for ZipRecruiter/Google; Glassdoor tested the same way with the same result). No free fix exists — the workaround JobSpy documents is a paid rotating-residential-proxy subscription. They're left in
sitesin case JobSpy fixes this upstream (nothing to configure if it does).Non-US "Remote" listings are filtered out. LinkedIn's "Remote" search isn't country-scoped and returns postings from anywhere in the world —
filters.py'sis_non_us_location()drops anything whose location names a non-US country.Window: last 48 hours of postings, dedup'd against everything previously shown (stored in
~/.job-search-mcp/state.db).
All of this is editable at runtime with update_criteria — no code
changes or restart needed.
A note on LinkedIn/Indeed scraping
JobSpy scrapes these sites directly (no official API), which is against
their terms of service. It mimics ordinary browser traffic rather than
doing anything like credential stuffing or session hijacking, and this
server is meant to run about once a day, not be polled — but there's
still some risk of the scraper breaking or getting rate-limited if these
sites change their defenses. If you'd rather drop LinkedIn/Indeed
entirely, remove them from sites via update_criteria and keep
ZipRecruiter/Google/Indeed... (well, keep whichever subset you're
comfortable with).
Data
Everything is stored locally in ~/.job-search-mcp/state.db (SQLite) —
seen-job history, your criteria, and your application tracker. Nothing
leaves your machine except the scrape requests themselves.