job-search-mcp
Uses Gmail as an email fallback for approved applications: creates drafts, tracks drafted/submitted status, and sends completion notification summaries.
Integrates with Greenhouse ATS job feeds to discover and evaluate vacancies, and supports submitting approved applications through ATS form automation.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@job-search-mcpfind new job openings in security engineering"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Career Ops Agent
An agentic career-operations system that discovers relevant vacancies, evaluates them against a target profile, drafts tailored application materials, and coordinates human-approved submissions.
The project combines:
an AI agent and skill layer that defines goals, decision rules, MCP tool usage, and approval boundaries;
an MCP (Model Context Protocol) server that exposes job-search and application-management capabilities to compatible AI clients;
a scheduled scanner that monitors company career pages;
persistent state for deduplication, application tracking, and interrupted session recovery;
a human approval gate before any application is submitted; and
ATS automation, an email fallback, and completion notifications for approved applications.
The MCP server is one component of the system, not the entire system. Deterministic work such as scheduled scanning and deduplication runs automatically, while the AI layer interprets results, drafts application materials, and guides the broader workflow. The system is therefore agentic rather than fully autonomous: consequential submissions remain subject to human approval.
I built this because I'm a security engineer and the job search is, structurally, a monitoring-and-response problem: noisy signal sources, a triage step, an enrichment step, and an automated response with a human-approval gate. I built it the way I'd build a detection-and-response pipeline.
Status: Personal project actively used in my own job search. The published version excludes credentials, personal profile data, application history, and the private company seed list. See Configuration.
Architecture
┌─────────────────────────────────────────────────────────┐
│ Task Scheduler (Windows) / systemd timer (Linux) │
│ └─► Docker readiness check ─► container start │
├─────────────────────────────────────────────────────────┤
│ Scan stage ATS JSON feeds (Greenhouse, Lever, │
│ Ashby, Comeet, Workday) · page scrape │
│ fallback · LinkedIn Easy Apply queue │
│ Filter stage dedupe · already-applied · seniority / │
│ lateral-move rules · job-board and │
│ false-positive URL rules │
│ Draft stage cover-letter generation from profile + │
│ job description │
│ Approval gate letters surface for human confirmation │
│ (MCP session, resume-capable) │
│ Submit stage ATS form automation ─► Gmail draft │
│ fallback (needs_email → drafted → │
│ submitted) │
│ Notify stage Windows toast + email summary │
├─────────────────────────────────────────────────────────┤
│ Shared data volume (single mount, scanner + MCP server)│
│ Seed list: 117+ companies · JSON state · run logs │
└─────────────────────────────────────────────────────────┘Design decisions worth calling out:
Human-in-the-loop where it matters, nowhere else. The pipeline pauses at exactly one gate: cover-letter confirmation. Everything upstream (rejecting already-applied companies, lateral moves, product-page false positives) is silent and automatic. Everything downstream (submission) fires immediately on approval. This mirrors how a good SOAR playbook treats analyst approval — one decision point, full context attached, no busywork.
Self-expanding coverage. A scan that produces zero genuinely new matches after filtering triggers a research step that adds five new target companies to the seed list before the run ends. The pipeline treats "no findings" as a coverage gap, not a success state.
Feeds over scraping. Most careers pages render listings with JavaScript or an embedded ATS widget, so scraping
<a>tags finds section headings, not postings.modules/ats.pyspots the ATS behind a page and reads the same postings from the vendor's JSON API; a seed entry can pin the ATS when detection misses.Drafts are tracked, not assumed. When ATS submission fails, the letter and addresses are kept in the history file and the agent creates a Gmail draft for review.
mark_draftedandmark_sentmove the job throughneeds_email → drafted → submitted, so an unsent draft never counts as an application, and a sent one never sits in limbo.Session resume. Long-running MCP sessions survive interruption via an
initialize_session/resume: truehandshake, so a nightly run that dies mid-approval can be picked up without re-scanning.
Related MCP server: trackly-cli
Lessons learned (the fun part)
These are the failures that shaped the current design — the kind of thing a README usually hides and an engineer actually wants to read:
UTF-8 BOM poisoning. Windows-edited JSON state files grew a byte-order mark that the parser silently choked on. Fix: normalize encoding on every read, never trust the editor.
Split-brain storage. The nightly script and the MCP server originally mounted different Docker volumes and disagreed about reality. Fix: one shared data directory, one source of truth.
Scheduler time limits. Windows Task Scheduler's default execution limit killed long runs mid-submission. Fix: extended limits + resume logic, so a kill is an inconvenience instead of data loss.
Em dash corruption. Encoding mismatches mangled punctuation in generated letters — a cosmetic bug with real cost, since a garbled cover letter reads as carelessness. Fix: end-to-end UTF-8 enforcement and an output lint pass.
Docker readiness races. The scheduled task fired before the Docker daemon was up. Fix: an explicit readiness probe with backoff before the run starts.
Prerequisites
Docker — Docker Desktop on Windows (the nightly wrapper auto-starts it), or any Docker engine if you only want the MCP server. Both halves of the system — the scheduled scan and the interactive MCP server — run in the same
job-search-mcp:latestimage and share thedata/volume.An MCP-capable AI client (Claude Desktop, or anything speaking MCP over stdio) for the interactive half: reviewing pending matches, approving letters, triggering submissions. Something like Docker's MCP gateway works too; the server itself is plain stdio, so any launcher that can run
docker run -ican host it.Windows or Linux for the nightly automation. Windows: PowerShell wrapper, Task Scheduler, toast notifications. Linux:
run_nightly.shwith the units insystemd/(or cron), plusmcp_launch.shto start the MCP server with SMTP credentials from.env.nightly.Optional: Claude in Chrome for
/linkedin-apply, which submits the queued LinkedIn Easy Apply jobs you approve (seeclaude/commands/).
Registering the MCP server
The server speaks stdio, so the client launches the container directly.
For Claude Desktop, add to claude_desktop_config.json (adjust paths):
{
"mcpServers": {
"job-search": {
"command": "docker",
"args": [
"run", "-i", "--rm",
"-v", "C:\\path\\to\\repo\\data:/data",
"-v", "C:\\path\\to\\repo\\config.json:/app/config.json",
"-v", "C:\\path\\to\\your\\cvs:/cvs:ro",
"job-search-mcp:latest"
]
}
}
}The -i flag and the shared data/ mount are both load-bearing: stdio needs
the former, and the nightly scan and the interactive session only agree about
reality because they read the same /data (see Lessons learned).
Configuration
The repo ships the engine; you bring the fuel. Docker and an MCP client
get the machine running, but it does nothing useful until config.json
contains your identity, your target roles (each mapped to a CV file), and
your seed list of companies worth scanning. That targeting is where the real
work of a job search lives — expect to invest in it, and expect the quality of
your matches to track the quality of your seed list. All of it stays in one
gitignored file; nothing personal ever belongs in the repo itself.
The fastest way in is the interview wizard:
python setup_config.pyIt walks you through identity, geography, roles, and seed companies, inherits
the non-personal defaults (careers-page patterns, ATS list) from
config.example.json, and writes config.json as UTF-8 without a BOM —
which matters, because a BOM on this file breaks the JSON parse inside the
container (ask me how I know; see Lessons learned).
Prefer doing it by hand? cp config.example.json config.json and edit. Either
way, then:
Put your CV PDFs in the directory pointed to by
JOB_SEARCH_CV_DIR(defaults to<repo>\cvs).Set
JOB_SEARCH_SMTP_USER/JOB_SEARCH_SMTP_PASSas user environment variables (Gmail app password) — seesetup_scheduled_task.ps1header.docker build -t job-search-mcp ., register the MCP server with your client, and runsetup_scheduled_task.ps1once as Administrator to create the nightly task. On Linux instead: put the SMTP variables in.env.nightly(see.env.nightly.example), edit the user and path insystemd/*.service, copy both units to/etc/systemd/system/, and runsudo systemctl enable --now job-search-nightly.timer.Optional, for
/linkedin-apply: copylinkedin_answers.example.jsontodata/linkedin_answers.jsonand fill in your own answers, and copyclaude/commands/linkedin-apply.mdto~/.claude/commands/.
The agent's operating rules — triage behavior, the approval gate, the
zero-match seed-expansion rule — live in CLAUDE.md / AGENTS.md.
Roadmap
Done since the first release: ATS JSON feeds (Greenhouse, Lever, Ashby, Comeet, Workday), a seniority/lateral-move title filter, job-board filtering in company discovery, Linux scheduling via systemd, the LinkedIn Easy Apply queue, and tracked Gmail-draft fallback.
Next:
Response/funnel tracking: tag every reply, interview, and offer by channel so the pipeline measures conversion, not just throughput
Structured detection-style rules for match filtering, replacing ad-hoc logic
Why this is on my CV
Everything in this repo — agent orchestration, human-approval gates, silent auto-triage, self-healing scheduled execution, encoding hygiene, state management across container boundaries — is the same engineering that security automation platforms are built from. I just happened to point it at job boards first.
This server cannot be deployed
Maintenance
Related MCP Connectors
Public MCP server for discovering open jobs. Search, filter, and get application links.
GetJobzi MCP server for job search, application tracking, and career forecasting.
Capability registry for the agentic economy. Semantic search over verified MCP server listings.
- UnifAPIOAuthcom.unifapi
Hosted MCP server for live public-data APIs and Skills for AI agents.
Related MCP Servers
- AlicenseNot gradedqualityAmaintenanceMCP server that exposes job search data from multiple boards, enabling clients to query and manage job listings via natural language.7MIT

trackly-cliofficial
AlicenseNot gradedqualityAmaintenanceMCP server for job search and application tracking, enabling AI agents to search jobs, get details, manage applications, and find contacts across 128K+ jobs and 1,900+ companies.847 npm3MIT- AlicenseCqualityCmaintenanceAn MCP server that enables AI-assisted job search workflows including job discovery, application tracking, resume evaluation, and cover letter generation, with support for multiple job sources and scheduled scraping.8312 npm1AGPL 3.0
- AlicenseNot gradedqualityDmaintenanceMCP server that aggregates and deduplicates job listings from multiple public sources, ranks them against a user's resume, and exposes tools for searching, viewing details, explaining fit, and tracking applications.MIT