Case study

jobops: a job search pipeline as an MCP server

51 tools, three planes, one SQLite file. How a full job search (scanning, scoring, rendering, outreach) became a self-hosted MCP server any chat client can drive.

status · Published on npmrepo ↗package ↗stack · TypeScript, Node.js, SQLite, Playwright, MCP, LaTeX

Highlights

The core split

A job search is a pipeline: source postings, score them against what you actually want, tailor materials, reach out, track everything. Most of that reasoning is exactly what a chat model is good at, and most of the grunt work (scraping portals, rendering PDFs, keeping state) is exactly what it is bad at. jobops splits along that line. The chat client is the brain: it scores JDs against an editable rubric and drafts materials. The server is the hands: it scans ATS portals with Playwright (Greenhouse, Ashby, Lever, Workday), normalizes and dedupes postings, renders resumes and cover letters (LaTeX to PDF, plus .tex and .docx), runs a SQLite tracker with a live dashboard, and hands every artifact back as a link.

Three planes, one process

The MCP plane serves tools over stdio or streamable HTTP. The HTTP plane serves rendered files, the tracker dashboard, and health endpoints on the same port. The data plane is a single SQLite database in WAL mode. HTTP mode is first-class shared mode: one long-running process serves Claude Desktop, Claude Code, Cursor, and LibreChat concurrently against one database, so work done in any client is instantly visible in all of them. Each HTTP request gets a fresh MCP protocol instance (a shared one would cross-route responses between overlapping clients), reads run concurrently under WAL, and every write serializes through a single lock.

Behaviour lives in editable files

Six markdown files (scoring rubric, report format, tailoring rules, outreach tone, negotiation playbook, career packet template) ship as defaults but are copied into the project root where user edits win and hot-reload without a restart. The career packet, the authoritative superset of every claim the system may make about you, is versioned in the database with edits possible from either direction: chat edits are marked and protected from being silently rebuilt over, and a sync tool writes them back to the source files.

Rules enforced in code

No tool auto-submits an application or sends a message; everything stops at a draft or preview. Outreach drafts are validated against hard rails (character caps, no clichés, no emojis) before they persist. Scoring failures stay visible: a bad LLM response leaves the score null, never a silent zero. And binding to anything other than localhost without an auth token refuses to boot, because a server holding your resume and network data should be default-deny.

← Back to projects