Skip to main content
Glama
juanzjck

jobfinder-mcp

by juanzjck
README.md
# jobfinder-mcp

A local [MCP](https://modelcontextprotocol.io) server for searching job listings
and tracking applications. Runs entirely on your machine over stdio; saved jobs
live in a local SQLite file.

## Install

```bash
python3 -m venv .venv
.venv/bin/pip install -e .
```

Requires Python 3.10+. The SDK is pinned to `mcp>=1.2,<2`: mcp 2.x removed
`mcp.server.fastmcp`, which this server is built on.

## Run

```bash
.venv/bin/jobfinder-mcp          # or: .venv/bin/python -m jobfinder_mcp
```

The server speaks MCP over stdio, so running it by hand just blocks waiting for
a client. To inspect it interactively:

```bash
.venv/bin/mcp dev src/jobfinder_mcp/server.py
```

## Web UI

```bash
.venv/bin/jobfinder-web            # http://127.0.0.1:8765, opens a browser
.venv/bin/jobfinder-web --port 0 --no-browser
```

Two panes: tracked jobs on the left (search box, status filters, a badge showing
the tailored resume's file type), the selected job on the right with tabs for the
full **Description** and its **Tailored resume** — copy it, download the file, or
take it as a self-contained `data:application/x-tex;base64,…` link (shown under
the buttons, with *copy data URI* next to it). The whole document lives inside
that link, so it can be pasted anywhere and downloaded with this server stopped.

For `.tex` output there is also **Open in Overleaf**, which POSTs that data URI to
`overleaf.com/docs` as `snip_uri` and opens the compiled CV in a new tab — the way
to get a PDF without a local TeX install. It uploads the resume to Overleaf, so it
only fires when you click it. Note that Chrome refuses `data:` URIs typed into the
address bar; only the in-page controls work.
The status dropdown writes straight back to the tracker, so the MCP tools and the
UI stay in sync; hit *refresh* after Claude saves something new.

It reads the same SQLite file and tailored directory as the MCP server, and binds
to loopback only — it serves your resume and application notes, so do not expose
it on a LAN interface. The MCP tool `open_web_ui(port)` starts the same UI on a
background thread inside the running server.

## Register with Claude Code

```bash
claude mcp add jobfinder -- /Users/juansalzar/Documents/git/jobfinder/.venv/bin/jobfinder-mcp
```

Or add it to `~/.claude.json` / `.mcp.json` manually:

```json
{
  "mcpServers": {
    "jobfinder": {
      "command": "/Users/juansalzar/Documents/git/jobfinder/.venv/bin/jobfinder-mcp"
    }
  }
}
```

For Claude Desktop, the same block goes in
`~/Library/Application Support/Claude/claude_desktop_config.json`.

## Tools

| Tool | Purpose |
| --- | --- |
| `search_jobs(query, location, tags, source, limit)` | Search listings across all sources, ranked by where the terms hit |
| `get_job_details(job_id)` | Full listing text for an id from a search or the tracker |
| `save_job(job_id, notes)` | Save a listing from the last search into the tracker |
| `add_job(title, company, description, location, url, salary, tags, notes, job_id)` | Add a job found elsewhere by pasting its details |
| `set_job_description(job_id, description)` | Replace a saved job's description with pasted text |
| `list_saved_jobs(status, limit)` | List tracked jobs, newest first |
| `update_job_status(job_id, status, notes)` | Move a job through the pipeline |
| `delete_saved_job(job_id)` | Remove a tracked job |
| `pipeline_summary()` | Counts by status |
| `add_resume(name, text, path)` | Store a resume from pasted text or a `.md/.txt/.pdf/.docx/.tex` file |
| `list_resumes()` | Stored resumes, with the default marked |
| `get_resume(name, raw)` | Show a resume; `raw=true` returns the original LaTeX source |
| `set_default_resume(name)` | Pick the resume used when a call names none |
| `delete_resume(name)` | Remove a resume and its original file |
| `tailor_resume(job_id, resume)` | Build the tailoring brief for a job |
| `save_tailored_resume(job_id, content, extension)` | Store the tailored CV as LaTeX |
| `get_tailored_resume(job_id)` | Read back the tailored CV |
| `tailored_resume_link(job_id)` | The tailored CV as a base64 `data:` URI |
| `list_tailored_resumes()` | Every tailored CV written so far |
| `open_web_ui(port)` | Start the local browser UI and return its URL |

Resource `jobfinder://saved` returns every tracked job as JSON.
Prompt `tailor_application(job_id, resume_summary)` drafts a cover letter; with no
`resume_summary` it uses the default stored resume.

Statuses: `saved`, `applied`, `interviewing`, `offer`, `rejected`, `archived`.

## Tailoring a resume

The server does not call an LLM. `tailor_resume` returns a brief — the listing's
must-have terms, its most frequent terms, and which of them the resume already
evidences — and the model driving the client writes the CV from it. Nothing is
sent anywhere, and there is no API key or per-call cost.

```
add_resume(name="main", path="~/Documents/cv.pdf")
search_jobs(query="python backend", location="remote")
save_job(job_id="remoteok:123456")
tailor_resume(job_id="remoteok:123456")      # -> brief; the model writes the CV
save_tailored_resume(job_id="remoteok:123456", content="\\documentclass...")
```

PDF, DOCX and LaTeX files are converted to text for matching, and the original is
kept next to it. A scanned, image-only PDF extracts to nothing; paste the text
with `add_resume(name, text=...)` instead.

### LaTeX output

Tailored resumes are always written as `.tex`. `tailor_resume` ends its brief
with the LaTeX to write into:

- the stored resume **is** a `.tex` file → its source is included, to be rewritten
  in place with the preamble, macros and layout left alone;
- otherwise → a plain `article`-class template is included for the model to fill.

`save_tailored_resume` rejects content with no `\documentclass` or
`\begin{document}`; pass an explicit `extension` to store another format anyway.
There is no LaTeX toolchain in this project — compile the result with
`pdflatex`/`tectonic`, or upload it to Overleaf.

To use your own layout instead of the built-in template, drop a `.tex` file at
`~/.jobfinder/resume_template.tex` or point `JOBFINDER_LATEX_TEMPLATE` at one.

Term matching is deterministic string work: known multi-word phrases plus
non-stopword tokens, normalized through an alias table so `k8s` in a listing
matches `Kubernetes` in a resume. It surfaces gaps; it does not judge fit.

## Sources

- **remoteok** — RemoteOK's public JSON feed. No API key. Cached 15 minutes.
- **local** — set `JOBFINDER_FEED=/path/to/jobs.json` to read your own listings.
  The file is a JSON list of objects with at least `id`, `title` and `company`;
  `location`, `url`, `salary`, `tags`, `description` are optional.

Add another source by writing a `fetch_*` function in `sources.py` that returns
normalized job dicts, then registering it in the `SOURCES` map.

## Configuration

| Env var | Default | Meaning |
| --- | --- | --- |
| `JOBFINDER_DB` | `~/.jobfinder/jobs.db` | SQLite file for saved jobs |
| `JOBFINDER_FEED` | unset | JSON file for the `local` source |
| `JOBFINDER_HOME` | `~/.jobfinder` | Base directory for resumes and tailored output |
| `JOBFINDER_RESUME_DIR` | `$JOBFINDER_HOME/resumes` | Where stored resumes live |
| `JOBFINDER_TAILORED_DIR` | `$JOBFINDER_HOME/tailored` | Where tailored resumes are written |
| `JOBFINDER_LATEX_TEMPLATE` | built-in template | `.tex` skeleton used for tailored output |

TDQS

A3.7/5.0

Scored across 20 tools

Disambiguation4/5

Most tools have distinct purposes, but save_job and add_job both create a saved job, which could cause confusion. The descriptions clarify the source, so an agent can differentiate with careful reading.

Naming Consistency4/5

Most tools follow verb_noun naming (search_jobs, get_job_details, save_job). However, pipeline_summary and tailored_resume_link are noun-phrase names, breaking the pattern slightly.

Tool Count3/5

At 20 tools, this is on the heavier side. The dual purpose (job tracking and resume tailoring) justifies many tools, but it borders on overwhelming for an agent.

Completeness4/5

The job lifecycle (search, save, update, delete, summaries) and resume tailoring workflow (add, tailor, save, retrieve) are well covered. Missing a dedicated delete for tailored resumes is a minor gap.

Maintenance

ActivityStale
ResponsivenessNo issues