job-search-mcp
{"answer": "This job-search MCP server helps you analyze job descriptions against your resume and track fit-analysis results — it does not generate or tailor resumes.\n\n### Key capabilities\n\n- Match a job description to your resume (match_job): Embeds a job description and retrieves the most relevant resume/experience chunks from a vector store (Qdrant) via similarity search. Returns a heuristic retrieval_score (top-match cosine similarity), the retrieved evidence chunks with individual similarity scores, and the original job description. No internal LLM calls — the calling assistant reasons over the evidence using the provided job-fit://rubric resource.\n\n- Push a fit verdict to your tracker (push_to_tracker): Writes a FitVerdict (Strong/Good/Possible/Weak/Not a Fit, with domain match, scope match, preference severity, gate failures, red flags, rationale, and demotion count) to an existing row in your Notion tracking database. Only touches tool-populated fields defined in tracking_schema.yaml, skips misconfigured fields with warnings, and supports dry_run to preview the payload without writing.\n\n- Find or create a tracked application (find_or_create_application): Look up an existing row by exact company+role match, or create a new one, returning {job_id, created}.\n\n- Update application status (update_status): Set the Status field on an existing tracked row for lifecycle events (Applied, Rejected, Interviewing, Offer, etc.), completely decoupled from FitVerdict.\n\n- List tracked applications (list_applications): Retrieve tracked jobs with job_id and tool-populated fields (status, fit rating, notes). Optionally filter by exact case-sensitive status (e.g., \"Not yet applied\") to quickly surface jobs in a specific stage.\n\n- Ingest your resume: Accepts local text/markdown, PDF, or DOCX files, which are split into section/role-sized chunks, embedded with a local sentence-transformers model, and stored in the vector store for retrieval.\n\n### Design highlights\n\n- Schema-driven tracking: Which fields are read/written is controlled by a user-maintained tracking_schema.yaml, not hardcoded to a specific Notion database layout.\n- Adapter-based architecture: Flexible backends for vector storage (Qdrant), tracking stores (Notion), and resume sources.\n- Job ID ownership verification: push_to_tracker and update_status verify the job_id belongs to your configured Notion database before writing.\n- Misconfigured fields are skipped with warnings, not hard failures — check the warnings field in results."}
Planned ResumeSource implementation for retrieving resume/experience content from Google Drive (listed on the roadmap, not yet shipped).
Provides tracking store integration for persisting and listing job fit-analysis results in Notion, including writing FitVerdict data to tracked rows and listing applications with their status and fit ratings.
Provides a zero-dependency local tracking store implementation for persisting fit-analysis results, reading the same configurable tracking field schema as the Notion backend.
Click on "Install 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-mcpmatch this job description to my resume and show me the top evidence"
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-search-mcp
An MCP server that retrieves resume/experience evidence relevant to a job description via RAG, and tracks fit-analysis results in a tracking store.
Analysis and tracking only — this project does not generate or tailor
resumes. See docs/adr/0007-analysis-only-v1-no-resume-generation.md.
Design
The server is adapter-based rather than locked to any specific vendor. Three interfaces define the boundaries:
VectorStore(src/job_search_mcp/vector_store/) — embedded resume/experience chunk storage and similarity search. Default implementation:QdrantVectorStore(Cloud or self-hosted-in-Docker — a connection detail, not an interface difference).TrackingStore(src/job_search_mcp/tracking_store/) — persistence for fit-analysis results.NotionTrackingStoreis the store the server wires up today (see Setup).SQLiteTrackingStoreis a zero-dependency local implementation that also exists but isn't yet selectable via config — see Roadmap. Both read a user-declaredtracking_schema.yaml(docs/adr/0011-configurable-tracking-field-schema.md) for which fields to write and read, rather than a mapping hardcoded to one specific Notion database.ResumeSource(src/job_search_mcp/resume_source/) — retrieval of resume/experience content. Implementation:FileResumeSource(local text/markdown, PDF, and DOCX parsing).
Rationale for each of these decisions is recorded in docs/adr/.
Embeddings (src/job_search_mcp/embeddings/) default to a local
SentenceTransformersEmbedder (all-MiniLM-L6-v2) — see
docs/adr/0006-embeddings-choice-open.md.
Related MCP server: Claude Works
Stack
Python, dependency management via
uvjustas the task runner (seejustfile)Qdrant (vector store), Notion (tracking store)
MCP Python SDK for the server itself
MCP tools
match_job(job_description, source_url=None)— embeds the job description, retrieves the most relevant resume/experience chunks from the configuredVectorStore, and returns a heuristicretrieval_score(top-match cosine similarity) plus the retrieved evidence. Namedretrieval_score, notfit_score— it's a retrieval confidence signal, not a fit judgment, and the two can diverge (seedocs/adr/0008-resume-chunking-strategy.md). It does not synthesize a fit verdict itself — no internal LLM call — so the calling assistant is expected to reason over the returned evidence, applying thejob-fit://rubricresource returned alongside it. Sincejob_descriptionis often scraped/pasted web text, the result also includes it wrapped in an explicit data delimiter with a do-not-follow-instructions notice (docs/adr/0017-delimit-job-description-in-match-job-result.md).Note:
docs/adr/0009-caller-agnostic-reversal.mdcalls for this judgment step to move server-side into a newevaluate_fittool, so fit-bucket assignment isn't left to whichever assistant happens to callmatch_job. That tool is designed (docs/evaluate_fit_schema.md,docs/adr/0010-layer-split-design-evaluate-fit.md) but not yet implemented — today, the calling assistant still constructs theFitVerdictpassed topush_to_trackeritself.push_to_tracker(job_id, verdict, dry_run=False)— writes aFitVerdict(seedocs/evaluate_fit_schema.md) to the configuredTrackingStore. Updates an existing tracked row by Notion page ID — never creates a new row or searches for one. Only the fields yourtracking_schema.yamlmarks tool-populated are touched; every manual field on the row (company, comp range, source, work arrangement, etc.) is left as-is (docs/adr/0011-configurable-tracking-field-schema.md). A misconfigured field (e.g. anotion.propertythat no longer exists on your database) is skipped with a warning rather than failing the whole write — check the result'swarnings.dry_run=Truereturns the mapped properties payload without writing.find_or_create_application(company, role, source_url=None, dry_run=False)— finds an existing tracked row by exact company+role text match, or creates a new one if none exists. Requirescompanyandrolefields with anotion.propertydeclared intracking_schema.yaml. Returns{"job_id": ..., "created": bool}. Seedocs/adr/0013-find-or-create-application.md.update_status(job_id, status, dry_run=False)— sets the Status field on an existing tracked row directly, for candidate-reported lifecycle events (Applied, Rejected, Interviewing, Offer, ...). Decoupled fromFitVerdict/push_to_trackerentirely — seedocs/adr/0014-update-status-tool.md.
push_to_tracker and update_status both verify the target job_id
actually belongs to your configured Notion database before writing to it
— see docs/adr/0016-job-id-ownership-check.md.
list_applications(status=None)— lists tracked jobs from the configuredTrackingStore, each withjob_idplus whatever tool-populated fields your schema declares (typicallystatus,fit_rating,notes). Passstatusto filter to an exact (case-sensitive) match, e.g."Not yet applied"— useful for questions like "what am I waiting to hear back on" without opening Notion.
Resume text is split into light, section/role-sized chunks before
embedding (src/job_search_mcp/chunking.py) rather than embedded whole —
see docs/adr/0008-resume-chunking-strategy.md.
Tracking field schema
push_to_tracker and list_applications don't hardcode a field list —
they read tracking_schema.yaml (path overridable via
TRACKING_SCHEMA_PATH, gitignored like .env; copy
tracking_schema.example.yaml to get started). Each field is declared as
either:
manual: true— the tool never reads or writes it (company, comp range, source, work arrangement, ...). Documentation only.derived_from: <status_fixed | fit_rating_from_bucket | key_notes>— a tool-populated field, computed from aFitVerdict. These three are the only values aFitVerdictcan currently be turned into; the schema says which of them your tracker wants and under what property/column name, not how to compute them.
A tool-populated field also needs a backend location:
notion.property/notion.type for Notion, sqlite.column for
SQLiteTrackingStore (which only tracks fields that declare a
sqlite.column at all — it has no concept of Notion's manual fields).
Someone with a simpler tracker than the author's just lists fewer
fields. A misconfigured individual field (an unrecognized
derived_from, or a notion.property that doesn't exist on the live
database) is warned about and skipped rather than failing the whole
write; only an unreadable or structurally invalid schema file itself is
a hard failure. See docs/adr/0011-configurable-tracking-field-schema.md
for the full design.
Setup
just installCopy
.env.exampleto.envand fill in your Qdrant and Notion connection details.Copy
tracking_schema.example.yamltotracking_schema.yamland edit the field list to match your own tracker (see Tracking field schema, above).Ingest a resume:
uv run python -m job_search_mcp.ingest path/to/resume.pdfRegister with Claude Code (project-scoped):
claude mcp add job-search-mcp -- uv run --directory "$(pwd)" job-search-mcpIn a Claude Code session in this project, ask it to call
match_jobwith a real job description, thenpush_to_trackeragainst a row you already track in Notion.
Roadmap
Shipped: match_job (retrieval), push_to_tracker, list_applications,
find_or_create_application, and update_status against
NotionTrackingStore, the YAML-configurable tracking field schema
(docs/adr/0011-configurable-tracking-field-schema.md), and the ingestion
pipeline (ResumeSource → chunking → embedding → VectorStore).
Planned next:
evaluate_fit— move fit-bucket judgment (the rubric indocs/job_fit_scoring_algorithm.md) inside the server via an internal LLM call, so it no longer depends on the calling assistant applying the rubric itself (docs/adr/0009-caller-agnostic-reversal.md,docs/adr/0010-layer-split-design-evaluate-fit.md)Wire
SQLiteTrackingStoreup as a selectable backend (it exists, reads the same tracking schema, and is tested, but the server currently always constructsNotionTrackingStore)Google Drive-backed
ResumeSourceimplementationRevisit the embeddings choice if local sentence-transformers quality proves insufficient (
docs/adr/0006-embeddings-choice-open.md)
Deferred, low priority (no design commitment beyond the note below):
Bulk re-evaluation when
candidate_profile.yamlchanges —target_floor/title_mapping_noteare documented as editable-but-stable, so a value changing after postings are already tracked is a foreseeable case. Open question: whetherlist_applications+ re-runningevaluate_fitis sufficient once that tool exists, or whether the originaljob_descriptiontext needs to be persisted somewhere (it currently isn't) to make re-evaluation possible later.Duplicate/near-duplicate JD detection — companies commonly post near-identical reqs for genuinely different underlying roles (same title, similar boilerplate, different team/contract). Neither
match_job/evaluate_fitnorfind_or_create_application(docs/adr/0013-find-or-create-application.md, which only matches exact company+role text) currently detect this. Direction: a lightweight hash/fuzzy-match check against previously-ingested postings, surfaced as a warning rather than blocking.
Both items above were raised alongside specific anecdotes (a mid-search
target_floor change; two near-duplicate "Skylight" postings) that a
transcript-search check found no independent record of — see
docs/adr/0015-prompt-injection-defense-for-job-description-text.md's
Verification section for the same check applied to a related claim from
the same source. Both items stand on the general reasoning above
regardless of that.
Development
just install # uv sync
just test # uv run pytest (unit tests only; integration needs QDRANT_URL)
just lint # uv run ruff check .
just run # uv run job-search-mcp (stdio MCP server)CI (.github/workflows/ci.yml) runs lint and the unit test suite on
every push to main and every pull request.
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- Alicense-qualityBmaintenanceAn 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.Last updated81AGPL 3.0
- AlicenseAqualityAmaintenanceAn MCP server that exposes a perpetual, honest job-application pipeline as typed tools an LLM agent can call, with fit scoring, verified resume building, and a submission planner enforced by code, not prompts.Last updated16MIT
- Flicense-qualityAmaintenanceA local-first, open-source MCP server that analyzes jobs, matches your CV, tailors documents, and tracks applications — all on your machine with no data uploaded.Last updated
- Alicense-qualityCmaintenanceAn 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.Last updatedMIT
Related MCP Connectors
MCP server for AI job search — find jobs, track applications, get alerts. Claude, ChatGPT, Cursor.
GetJobzi MCP server for job search, application tracking, and career forecasting.
Local-first RAG engine with MCP server for AI agent integration.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/jsundquist/job-search-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server