job-search-mcp
An MCP server that exposes job-search and application-management capabilities to compatible AI clients, enabling discovery of vacancies, drafting of tailored application materials, and coordinated human-approved submissions.
README
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
┌─────────────────────────────────────────────────────────┐
│ Windows Task Scheduler (nightly) │
│ └─► Docker readiness check ─► container start │
├─────────────────────────────────────────────────────────┤
│ Scan stage career-page fetchers (per-company) │
│ Filter stage dedupe · already-applied · lateral- │
│ move rules · 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 fallback │
│ 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.
- 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.
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 for the nightly automation as shipped (PowerShell wrapper, Task Scheduler, toast notifications). The Python core is OS-agnostic — porting the wrapper to cron/systemd is a small exercise.
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.py
It 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.
The agent's operating rules — triage behavior, the approval gate, the
zero-match seed-expansion rule — live in CLAUDE.md / AGENTS.md.
Roadmap
- Response/funnel tracking: tag every reply, interview, and offer by channel so the pipeline measures conversion, not just throughput
- Pluggable fetchers for common ATS platforms (Comeet, Greenhouse, Lever)
- 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.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.