j-hunt-mcp
A local job-hunting MCP server built on the official Python SDK (mcp / FastMCP). It runs over stdio for Claude Desktop / Claude Code and helps with the full loop:
- Discover jobs across pluggable web sources — including geo / map-region search.
- Track applications through a status lifecycle.
- Store a profile & resume the assistant can reference.
- Tailor resumes / cover letters via reusable prompts (the client LLM does the writing).
Architecture
Strict, one-directional layering keeps business logic out of the MCP glue:
tools/ • resources.py • prompts.py (thin MCP adapters — no logic)
│
▼
services/ (all business logic; constructor-injected deps)
│
┌────────┼─────────────┐
▼ ▼ ▼
repositories/ scraping/ geo.py (SQLite • job sources • Google Maps)
│
▼
models/ (pure Pydantic domain types, reused by every layer)
| Layer | Location | Responsibility | |-------|----------|----------------| | Models | src/jhunt_mcp/models/ | Pure Pydantic domain types (one file per domain) | | Repositories | src/jhunt_mcp/repositories/ | SQLite persistence; map rows ↔ models (SQLAlchemy tables kept separate) | | Services | src/jhunt_mcp/services/ | Business logic (search, tracking, profile, geo) | | Scraping | src/jhunt_mcp/scraping/ | Pluggable JobSource registry + sources | | Tools/Resources/Prompts | src/jhunt_mcp/tools/, resources.py, prompts.py | Thin MCP adapters over services |
Setup
Requires Python ≥ 3.10 and uv.
uv sync # install deps
cp .env.example .env # optional: add GOOGLE_MAPS_API_KEY for geo search
uv run pytest # run the test suite (offline)
Run / develop
uv run j-hunt-mcp # run the stdio server directly
uv run mcp dev src/jhunt_mcp/server.py # open the MCP Inspector (needs Node/npx)
Register with Claude Desktop
uv run mcp install src/jhunt_mcp/server.py --name "Job Hunt" \
-v GOOGLE_MAPS_API_KEY=your_key_here
Restart Claude Desktop; the Job Hunt server's tools then appear.
Capabilities
Tools
| Tool | Purpose | |------|---------| | search_jobs | Keyword search; optional location + radius_km for nearby jobs | | search_jobs_in_region | Search inside a map-selected bounding box (NE/SW corners) | | geocode_location | Resolve a place name → coordinates + formatted address | | save_job / list_saved_jobs | Persist and list jobs | | log_application | Record an application (by saved job_id or ad-hoc url) | | update_application_status | Move an application through its lifecycle (validated) | | set_next_action | Set a follow-up reminder on an application | | list_applications | List applications, optionally filtered by status | | get_profile / update_profile / set_resume | Manage the stored profile & resume |
Resources: profile://me, resume://current, jobs://saved, applications://{status} (use all).
Prompts: tailor_resume, draft_cover_letter, application_followup_email.
Geo / map-region search
There is no map UI in the server itself — a client passes the result of a map selection as parameters. Three shapes are supported, all requiring GOOGLE_MAPS_API_KEY:
- a place string +
radius_km(search_jobs), - a bounding box (
search_jobs_in_region) — what a map rectangle yields, - (internally) a center + radius.
Geocoding results are cached in SQLite to conserve API quota. Without a key, plain keyword search still works; geo paths return a clear error.
Job sources & scraping note
Major boards (LinkedIn, Indeed) actively block scraping and forbid it in their ToS. v1 therefore ships sources that expose public JSON/RSS and are scraping-tolerant: RemoteOK, WeWorkRemotely, Hacker News "Who is hiring?". Requests are rate-limited per host. Add a board by implementing
scraping/base.py:JobSource.
Secrets
Never commit credentials. The only secret today is GOOGLE_MAPS_API_KEY, read from a gitignored .env via pydantic-settings. When authenticated boards are added later, use the OS keyring for passwords and the SQLite DB for session cookies — never JSON in the repo. mcp install ... -v KEY=value injects env vars without writing them to source.
Data
The SQLite database lives at data/jhunt.db by default (override with JHUNT_DB_PATH). The data/ directory is gitignored.











