j-hunt-mcp
# 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`](https://docs.astral.sh/uv/).
```bash
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
```bash
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
```bash
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.
TDQS
Scored across 12 tools
Each tool has a clearly distinct purpose: geocoding, profile management, job searching with two variants, saving jobs, logging applications, tracking status, setting follow-up actions, and updating resume. No overlap or ambiguity.
All tool names follow a consistent verb_noun pattern in snake_case (e.g., list_applications, save_job, update_application_status). The only minor variant, search_jobs_in_region, still adheres to the pattern with a qualifier.
12 tools cover the core workflows of a job hunt assistant: searching, saving, applying, tracking, profile/resume management, and geolocation. The count is well-scoped without redundancy.
The tool surface covers search, save, apply, status updates, follow-up actions, and profile management. Minor gaps exist (no delete operations for saved jobs or applications), but these are non-critical for the domain.