Job Matcher
Finds job postings on public Greenhouse job boards for the companies you add, enabling automated job discovery without a browser or LinkedIn scraping. Discovered postings (title, company, description, apply URL, location, remote, compensation) feed into profile-based evaluation and ranking.
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 Matcherrun my daily job search and show me the ranked matches"
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.
Job Matcher
A local MCP server (and JSON CLI) that gives your AI agent (Claude Code, Claude Desktop, Cursor, Codex) a memory and a judgment for your job search:
Profile once. Your resume, target roles, location, salary and dealbreakers, plus an agent-led interview about experience your resume leaves out. Stored locally in SQLite.
Find jobs on public job boards (Greenhouse, Ashby, Lever, SmartRecruiters, Recruitee, Workday) for the companies you add. No browser, no LinkedIn scraping.
Evaluate and rank postings against your profile with TypeSafe's Jev model, under a strictness dial you choose, with blockers and concerns spelled out.
Remember what you have seen, saved, applied to or dismissed, so nothing resurfaces.
Daily run: one tool call that finds new postings, evaluates them and returns a ranked report.
Browsing, scheduling and applying stay with you and your agent.
You need Python 3.12+, uv and a TypeSafe API key. Evaluation is billed to your key (roughly 5,000 input tokens per job) and is the only step that sends anything off your machine; see Consent.
Note: the interview topics and qualification weights currently assume engineering
roles (people-management experience is weighted heavily) and are not yet role-aware;
see docs/open-source-readiness.md.
Pairs well with connections-finder,
whose data/02-boards.jsonl can be imported as job boards (import_job_boards).
Get started
git clone https://github.com/thefotes/job-matcher.git
cd job-matcher
uv sync --no-editable --extra dev
cp .env.example .envSet TYPESAFE_API_KEY in .env. An existing environment variable takes precedence.
The key is never stored in SQLite. No live TypeSafe call is made during onboarding.
Onboard interactively with a UTF-8 text version of your resume:
uv run --no-sync job-matcher init --resume /absolute/path/to/resume.txtOr onboard through JSON (also available as MCP tools):
uv run --no-sync job-matcher onboarding
uv run --no-sync job-matcher profile-save --input /absolute/path/to/profile.jsonStart with examples/profile.json (placeholder values only — replace everything),
put the resume in resume_text, and set your own preferences. The profile ships
empty: there are no built-in roles, location, salary or authorization assumptions.
No experience is invented. A profile can be saved as a draft, but evaluation refuses
until the required fields are present: resume_text, target_roles, and either a
location or remote_only=true. The error lists exactly what is missing; the same
gate applies to daily_run (nothing is evaluated until onboarding is complete).
PDF/DOCX extraction is delegated to the calling agent; this evaluator accepts plain
text.
The interactive wizard reviews name, location, salary, roles, preferences and dealbreakers. JSON/MCP onboarding exposes all fields, including remote requirements, authorization, currency and decision thresholds. Saving replaces the complete profile; read it first when making an update.
Related MCP server: career-agent
Consent: what leaves your machine
Evaluation is the only step that sends personal data anywhere. It sends your saved profile (name, resume, confirmed interview answers, target roles, salary floor and target, location, remote and authorization requirements, preferences, dealbreakers) and the job posting to TypeSafe, billed to your own key (roughly 5,000 input tokens per job in testing). Strictness, job history, statuses and job boards never leave your computer; job-board discovery calls only public job-board APIs and sends nothing personal.
Evaluation refuses until you have consented. The init wizard asks after saving the profile;
otherwise run job-matcher consent grant (shows the disclosure and asks), or an agent shows
get_onboarding's consent.disclosure and, only if you agree, calls record_consent.
job-matcher consent show and consent revoke (MCP: revoke_consent) inspect or withdraw it;
revoking stops evaluation and keeps everything stored locally. Consent is stored with a
timestamp and disclosure version; if what is sent ever changes, you are asked again.
Evaluation results include the consent record they relied on, and the evaluate tools say
consent is required, which gives client-side safety reviewers (for example Codex auto-review)
the authorization evidence they look for.
Interview beyond the resume
Once the MCP is connected, give your agent your resume and use the onboarding interview prompt. It reads the saved profile, reviews your constraints, then asks focused questions about experience your resume may omit. Existing defaults are examples for engineering leadership; new users should review every preference rather than accept them silently.
get_onboarding returns a resumable interview plan with nine topics: engineering and
management tenure, leadership scope, hands-on work, technical scope, developer tooling,
AI-assisted development, production AI products, and domain experience. Your agent
records explicit statements or summaries you confirm through save_experience_answers.
Answers are stored separately from the resume, with user_reported provenance, and
can be reported, no_experience, or skipped. A missing/skipped answer never means
you lack that experience. Confirmation is the caller's attestation, not independent
verification by the server. No model infers interview facts during onboarding.
The tool merges by topic and preserves your resume/preferences; a new answer replaces
only that topic. Review prior detail when refining an answer. Resume readiness (ready)
and interview completion (interview.complete) are separate. Completion means every
topic was addressed or explicitly skipped; you can evaluate before completion.
For JSON CLI users, run onboarding for the plan, then save an array of confirmed
answers with experience-save --input answers.json. The basic init wizard does not
conduct the agent interview. Evaluation sends the resume and interview evidence
to TypeSafe; changing either invalidates cached rankings while preserving job statuses.
More evidence does not remove the current confidence gates or guarantee an apply result.
Evaluate and rank
evaluate accepts one job object. batch accepts an array of 1–100 jobs:
uv run --no-sync job-matcher batch --input examples/jobs.json
uv run --no-sync job-matcher evaluate < /absolute/path/to/job.json
uv run --no-sync job-matcher shortlist
uv run --no-sync job-matcher history --limit 100
uv run --no-sync job-matcher status JOB_ID shown
uv run --no-sync job-matcher status JOB_ID applied
uv run --no-sync job-matcher status JOB_ID dismissed --reason "Office only"A job requires title, company, and description. Optional fields: url, source,
source_id (requires source), and posted_at. The tool does not fetch URLs.
Job-board fields from find_jobs are also accepted: apply_url, location, remote,
compensation and updated_at. They are sent to TypeSafe as evidence with the description.
All command results and errors are JSON on stdout; interactive prompts use stderr.
A batch continues after individual failures and returns errors with their input indices;
its exit status is nonzero if any item failed. --force bypasses cached evaluations.
--data-dir /absolute/private/path goes before the CLI subcommand.
Each evaluation includes:
recommendation:apply,investigate, orskip.fit_percentandqualification_strength_percent: ranking scores, not a probability of getting an interview or offer. Hiring probabilities need outcome data and validation.stretch, confirmeddealbreakers, and material uncertainties ininvestigate.Per-dimension signals, model confidence, raw distributions, model name, token usage, profile hash, and scoring version.
job_id, status, duplicate indicator, and cache indicator.
Jev answers ten independent questions: role, remote arrangement, geographic eligibility, annual base salary, authorization, dealbreakers, leadership experience, technical/domain experience, seniority, and preferences. Descriptions are evidence, never instructions. See TypeSafe's API and atomic decision guidance.
Qualification strength weights leadership 40%, technical/domain 35%, and seniority 25%. Fit weights qualification strength 80%, salary satisfaction 15%, and preferences 5%. Scores use five ordered levels (0–4), normalized to 0–100. Salary preference values: below floor 0, overlapping floor 50, meets floor 75, meets target 100, unknown 50. The unknown value is neutral for ranking; it does not prove salary compatibility.
Strictness: how much benefit of the doubt
Jev measures; strictness decides. Each user picks one of five stops (stored in their profile,
asked during the interview, changed any time with set_strictness or job-matcher strictness):
Stop | Qualification bar (stretch / strong) | A doubtful job is… | Details the posting omits |
| 40 / 60 | applied, with notes | notes |
| 45 / 65 | applied, with notes | notes |
| 50 / 70 | investigated | notes |
| 60 / 72 | investigated | count as doubt |
| 75 / 75, no stretches | hidden | count as doubt |
A doubt is a material chance of a hard conflict, a posting silent on remote work for a
remote-only candidate, an unresolved dealbreaker, or a qualification rating Jev has not settled
within one adjacent level. Stricter stops also treat smaller conflict probabilities as doubt
(50% at wide_net down to 10% at strict) and need less certainty before a likely conflict
counts as confirmed. Every knob moves the same way, so a stricter stop never upgrades a
recommendation (this is property-tested). Confirmed conflicts are skipped at every stop.
Details a posting omits (salary, eligible states, authorization) and a salary range spanning
your floor are returned as checks to resolve with the recruiter. Individual knobs can be
overridden (set_strictness with overrides, e.g. {"apply_threshold": 80}).
Strictness is never sent to Jev. Jev sees the resume, interview evidence and preferences
(Profile.evidence()); the dial is applied afterward to Jev's saved answers. Stored
evaluations are re-scored whenever they are read, so changing the dial, or the scoring rules,
takes effect immediately with no new API calls. The reported confidence is the minimum of the
dimension confidences, not a calibrated confidence in the combined recommendation. Score
formula and stop values are initial heuristics; refine them with real jobs and outcomes.
History and deduplication
SQLite stores the profile, job identities, status and evaluation history. Default storage
is .job-matcher/ in the checkout (or the working directory for an installed package).
Set JOB_MATCHER_DATA_DIR to an absolute path for stable MCP storage.
Identity uses source ID, canonical URL (including LinkedIn job ID), and normalized exact company/title/description content. Tracking parameters are removed. A changed job under the same URL or source ID is reevaluated. Distinct job IDs with identical text can collapse into one entry; vague semantic similarities are deliberately not used to merge separate openings. Conflicting existing identities require review instead of silently merging records.
Cache keys include substantive job text, full profile, requested model and scoring version.
URL tracking changes do not incur another API call. Changing profile preferences invalidates
the cache. jev-latest can change upstream; use --force or pin TYPESAFE_DEFAULT_MODEL when
freshness/reproducibility matters. Calls are sequential in v1, with SDK retry behavior.
Each job is in one state, derived from stored facts (see the matching-states design):
State | Meaning |
| Never shown to you |
| Shown at least once, no decision yet (every showing is logged) |
| Your decision, or imported from your application tracker |
| Your decision, with an optional reason |
| The posting is gone (an application stays |
set_job_status (or job-matcher status) records them: shown logs a showing and never
changes a decision; applied and dismissed set your decision; closed marks the posting
gone; new resets the job (decision, closure and showings cleared). The older names still
work: surfaced/saved mean shown, interview/in_progress mean applied. Databases
from earlier versions are migrated in place when first opened. History rows report the
state, when it began (state_since), and how often and when a job was shown.
Showing a job records a fact; it never hides the job. get_shortlist (CLI shortlist)
returns two views under the current profile and strictness, each sorted by fit: New
(never shown) and Still open (shown before, no decision, first shown within the
profile's open_window_days, default 30; include_stale=true drops the window). Applied,
dismissed and closed jobs never appear, and excluded counts everything left out and why.
Change the window with set_strictness overrides, e.g. {"open_window_days": 14}; like
strictness it is local and never sent to Jev. batch.shortlist ranks only that batch.
Historical imports never appear in the views. Profile changes preserve job states.
Import observed application history without a resume or TypeSafe calls:
uv run --no-sync job-matcher history-import --input /absolute/path/to/tracker.jsonThe input is an array of 1–100 entries, each with title, company, tracker_stage
(applied, interview, or in_progress), and a URL or source/source_id.
Description is optional. Repeated imports deduplicate and preserve full descriptions.
Every stage records the job as applied (the latest tracker_stage stays in the job data);
an import never overwrites your own decision.
Each failed entry returns its input index; successful entries are still saved.
A clicked Apply button maps to in_progress, never proof of submission.
See the recurring agent prompt for the daily routine, including the optional LinkedIn tracker import.
Find jobs on public job boards
Discovery reads the public job-board APIs that applicant tracking systems (ATS) publish for
their customers' careers pages, so no browser or login is needed. You save the boards of
companies you care about once, then find_jobs returns fresh postings with full plain-text
descriptions, ready for evaluate_jobs. Finding never evaluates: TypeSafe is called only
when you (or your agent) explicitly evaluate the results.
Provider | Board URL | Notes |
Greenhouse |
| full descriptions in one request |
Ashby |
| remote flag and compensation summary |
Lever |
| workplace type and salary range |
SmartRecruiters |
| one extra request per matching posting |
Recruitee |
| remote/hybrid flags and salary |
Workday |
| add by URL only; paged and capped |
Add boards by careers URL (any careers page, job page or API URL on those hosts), by company name, or by importing a file:
uv run --no-sync job-matcher board-add --url https://job-boards.greenhouse.io/example
uv run --no-sync job-matcher board-add --company "Example Co"
uv run --no-sync job-matcher board-add --company "Example Co" \
--url https://example.wd5.myworkdayjobs.com/en-US/Careers
uv run --no-sync job-matcher boards-import --input /absolute/path/to/boards.jsonl
uv run --no-sync job-matcher boards
uv run --no-sync job-matcher board-remove 3 # or --company NAME, --deactivateA company name is turned into likely board tokens and probed on Greenhouse, Ashby and
Lever, then SmartRecruiters and Recruitee. A response is not proof of ownership (a guessed
capital board is not Capital One), so a guess is saved only if the token is the exact
normalized company name on a trusted provider, or the board names itself as that company.
Otherwise the tool returns candidates with board names and sample titles, and the agent asks
you to confirm one by its careers URL. A careers URL you supply is trusted as your choice;
a board whose own name differs returns a warning. Workday tenants and custom career domains
are never guessed. No language model is involved in verification.
Imports accept JSON Lines, one board per line, with company, provider (or ats), token
(or board_token) and, for Workday, shard/site (or workday_shard/workday_site).
Rows with "verification": {"match": false} are skipped; other rows keep their prior
verification method. Imports make no network requests.
boards-from-history --limit N (MCP add_boards_from_history) adds boards for companies in
your application history, most recent first, skipping companies that already have a saved
board. It calls only public job-board APIs and sends nothing personal; only verified boards
are stored, and unverified candidates and unknown companies are reported back for you.
Boards that answer HTTP 404 or 410 on every fetch are retired automatically: three
consecutive misses deactivate a board (it stays listed, but find_jobs skips it), any
successful fetch resets the counter, and other errors such as timeouts or HTTP 500 never
count. The boards deactivated in a run are listed in daily-run coverage as
deactivated_boards; re-add a board with board-add to revive it.
uv run --no-sync job-matcher find --since-days 7 --keyword "Engineering Manager" \
--keyword "Director of Engineering" --remote-only --location "United States"find_jobs (CLI find) reads all active boards concurrently (six boards and eight requests
at a time, 20-second timeouts, one retry on rate limits or server errors) and keeps:
postings published (or, without a publish date, updated) within
since_days; undated postings are kept and listed last;titles containing every word of any keyword, in any order (
Engineering ManagermatchesManager, Software EngineeringandSr. Eng Manager); keywords default to the profile'starget_roles, or no title filter without a profile;with
remote_only(default: the profile's setting), postings the ATS or location text marks remote, plus postings with no location at all; on-site, hybrid and city-only postings are dropped;with
location_keywords, postings whose location names any keyword (or has none).
Results are newest first, up to limit (default 25, at most 100), each with its job, the
board_id, and known: the history entry (job_id, status, historical, evaluated)
when the posting matches a saved job by the same source ID, URL or exact-content identity
used for deduplication. exclude_known drops those. Board failures are listed in errors
and do not stop other boards; postings without a usable description are counted, not
returned. Listings are cached in SQLite for 6 hours (refresh bypasses this) and per-posting
details for 72 hours. SmartRecruiters and Workday descriptions are fetched only for
postings that pass the title and date filters, at most 40 per board per call
(details_deferred reports the rest; the next call continues). Workday listings stop paging
once a whole page is older than since_days, and never exceed 500 postings per board.
Privacy: discovery calls only public, unauthenticated ATS endpoints with a generic
job-matcher user agent. Requests contain board tokens and posting IDs, never your resume,
profile, keywords or history; all filtering happens locally. Boards and cached postings live
in your local SQLite database alongside your history.
Daily run
daily_run (CLI daily) is the whole daily routine in one call, so a scheduled agent does
not have to improvise it:
uv run --no-sync job-matcher daily --since-days 7 --max-evaluations 15 --top 10Runs
find_jobson your saved boards with your profile's roles and remote setting.Skips postings already in history with any status other than
new. A posting from another source with the same company and normalized title as a history record within 45 days is a likely duplicate: it is not evaluated, and is listed underduplicateswith its board link asemployer_url. The records are not merged, but the link is kept on the record, so later runs count that posting as known instead of listing it again.Evaluates new postings, newest first, up to
max_new_evaluations. Without recorded consent nothing is evaluated: the postings come back asunevaluated, with the consent disclosure and what to do next.Checks each board job against the listing it just read. A posting no longer listed is recorded as closed (
gone); one that reappears is reopened (reopened). Your decisions are never changed.Returns two views as compact summaries under your current strictness: up to
top_nNew matches (never shown, whenever found) and up toopen_nStill open ones (shown before, no decision yet). When nothing new qualified it says so instead of padding the list. A job seen on LinkedIn and on a board carries both links.Returns
report_markdown(both views as tables with the posted salary range, and LinkedIn and employer links) and coverage: boards read and failed, discovered, skipped, duplicates, evaluated, errors, deferred, input tokens, strictness andexcludedcounts.
It never records showings itself: the agent marks shown_ids as shown and then puts
report_markdown in its final reply (see next_step).
The recurring agent prompt is built around it.
MCP
Nineteen tools. Profile: get_onboarding, save_profile, save_experience_answers,
set_strictness, record_consent, revoke_consent. Evaluation: evaluate_job,
evaluate_jobs, get_shortlist, get_history, set_job_status,
import_application_history. Discovery: add_job_board, add_boards_from_history,
list_job_boards, remove_job_board, import_job_boards, find_jobs. Routine:
daily_run. Tool input
schemas and read/write/external annotations are included. Evaluation sends the resume,
user-reported interview evidence and job text to TypeSafe (never the strictness policy).
Discovery calls only public job-board APIs (see above). get_history, get_shortlist and
get_onboarding are compact by default; pass detail=true or
include_resume=true/include_schema=true when you need the full records (for example
include_resume=true before a full save_profile so the resume is preserved).
Registering the MCP server
Replace /path/to/job-matcher with the absolute path of your checkout everywhere below.
Your key stays in .env; registrations store only its file path.
Claude Code:
claude mcp add job-matcher \
--env "JOB_MATCHER_ENV_FILE=/path/to/job-matcher/.env" \
-- /path/to/job-matcher/.venv/bin/job-matcher-mcp --data-dir /path/to/job-matcher/.job-matcherClaude Desktop, Cursor, and other clients that accept mcpServers JSON — add to
the client's MCP configuration (Claude Desktop: Settings → Developer → Edit Config):
{
"mcpServers": {
"job-matcher": {
"command": "/path/to/job-matcher/.venv/bin/job-matcher-mcp",
"args": ["--data-dir", "/path/to/job-matcher/.job-matcher"],
"env": {
"JOB_MATCHER_ENV_FILE": "/path/to/job-matcher/.env"
}
}
}
}Codex (CLI and desktop app share ~/.codex/config.toml):
codex mcp add job-matcher \
--env "JOB_MATCHER_ENV_FILE=/path/to/job-matcher/.env" \
-- /path/to/job-matcher/.venv/bin/job-matcher-mcp --data-dir /path/to/job-matcher/.job-matcherIn the Codex desktop app, open Settings → MCP servers and restart the server to load
the new configuration; use /mcp in a new chat to check its connection. No HTTP
server or tunnel is needed for a local stdio connection. See the
official MCP setup guide.
For local HTTP:
uv run --no-sync job-matcher-mcp --transport streamable-http --port 8765Endpoint: http://127.0.0.1:8765/mcp. It binds to loopback only and has no public OAuth
layer. Use stdio locally or a private Secure MCP Tunnel for ChatGPT. Do not publish this
personal server through an unauthenticated public tunnel: it exposes profile/history and
profile/status mutations. A public deployment needs authentication first.
Hosted ChatGPT connection is a separate setup step. Current official documentation describes
Developer mode and either a public HTTPS endpoint or Secure MCP Tunnel; it does not imply
that a Claude-style local mcpServers configuration can be pasted into ChatGPT.
Account/workspace access must be checked when connecting:
Connect and test and
Secure MCP Tunnel.
No tunnel, deployment, account connection, or scheduled task has been created.
Data and development
After changing source code, reinstall with uv sync --no-editable --extra dev --reinstall-package job-matcher so CLI/MCP subprocesses use the new code.
The .env, local database and virtual environment are ignored by Git. SQLite is not
encrypted; keep its directory private. The default data directory is created with owner-only
permissions and the database file is set to 0600. MCP clients can read the saved profile
and history. Avoid SDK debug logging because SDK request logs may contain resume/job text.
This is a single-user tool; it has no multi-tenant isolation.
uv run --no-sync pytest -q
uv run --no-sync ruff check src tests
uv run --no-sync ruff format --check src testsDiscovery tests cover every provider adapter, URL inference, board verification, imports, caching, filters and history marking with a stubbed HTTP transport; pytest makes no network calls. Tests exercise recommendation rules, missing evidence, deduplication, cache invalidation, status persistence, retry after provider failures, the actual TypeSafe SDK through a mock HTTP transport, CLI JSON output, and both MCP transports. Tests make no paid API calls. Live integration checks with the saved resume and TypeSafe key have also been run.
This server cannot be deployed
Maintenance
Related MCP Connectors
Persistent memory for AI agents. Search, store, and recall across sessions.
Persistent memory for AI agents. Search and store durable facts, preferences and decisions.
Persistent work tracking for AI agents: tasks, status and history that follow you across machines
Persistent memory and knowledge management for AI agents with semantic search and 50+ tools.
Related MCP Servers
- AlicenseNot gradedqualityAmaintenanceEnables AI agents to discover, filter, and track job openings based on the user's local resume, without uploading data to the cloud.24MIT
- FlicenseNot gradedqualityBmaintenanceEnables Claude Desktop to manage a job search end-to-end: find and score job listings, tailor resumes, generate application messages, and track application history, while leaving final external actions to the user.-
- AlicenseAqualityBmaintenanceEnables AI agents to search job openings across free sources, prepare application materials using known experience, and handle authorized email sending and follow-ups.913MIT
- AlicenseNot gradedqualityBmaintenanceEnables an MCP-capable coding agent to run a local-first job search: recording verified opportunities in a private SQLite tracker, tracking follow-ups and outcomes, and preparing evidence-based resumes, application answers, and outreach drafts. It keeps all personal data on the user's machine and stops before submitting applications, uploading documents, or contacting anyone.1MIT