mcp-job-search
This MCP server automates job-search mechanics for an AI assistant: searching job boards, fetching a posting, rendering/linting CV PDFs, and managing an application registry.
search_jobs: Search Arbeitnow, Adzuna, and JSearch for jobs by keywords, location, sources, and remote-only; results are cached to protect API quotas.fetch_job_posting: Extract readable text from one job-posting URL you already found, including boards without a public search API.render_pdf: Convert a CV/cover-letter HTML file to PDF via headless Chrome and verify the output page count.lint_cv: Run configurable YAML rules against a CV HTML/PDF file — banned phrases, skills-section bans, overclaims, title/GDPR/link checks, and page limits.log_application: Append one application row to a Markdown registry, withdry_runpreviewing.list_applications: Read and filter the registry by portal and/or company, useful for checking duplicates.update_application_status: Rewrite one matching row's status cell in place, withdry_runand ambiguity protection.Configure API keys, lint rules, registry layout, Chrome path, and cache settings via environment variables; tools degrade gracefully when optional config is unset.
It does not draft or fact-check CVs, submit applications, or bypass CAPTCHA/bot-detection walls.
Provides job search capabilities through JSearch on RapidAPI, enabling the search_jobs tool to query job listings using a configured RapidAPI key and cache results to protect the free-tier quota.
mcp-job-search
An MCP server that automates the repetitive mechanics around job-hunting with an AI coding assistant, while deliberately not automating the parts that need a human: drafting your CV/cover letter, fact-checking claims against your real history, and submitting an application.
It answers the question that keeps eating a job search's time: "this CV draft looks fine, but did it quietly fabricate something, and is this one specific posting even worth reading?"
Status: early stage. Built for personal use, published because the configurable anti-fabrication linter and application-registry tooling are generically useful beyond one person's job search. All seven tools are working. See
CHANGELOG.mdfor what changed and what was verified live.
How this differs from other job-search MCP servers
A few open-source MCP servers already do job search + CV tailoring
(not-boring-resume-mcp, resume-mcp, Workopia MCP, IAM MCP Server,
LinkedIn MCP Server, among others). This one targets what they don't:
It won't scrape past a bot-detection wall, even quietly. NoFluffJobs, JustJoin.it and similar Polish IT boards have no public search API —
fetch_job_postingreads one URL you already found instead of indexing their listings, and on boards confirmed to sit behind an active Cloudflare challenge (Pracuj.pl, Indeed), it fails loudly rather than trying to solve the challenge automatically. See the verified coverage table below — this is a documented boundary, not a silent gap.The anti-fabrication linter is a configurable rules engine, not hardcoded prose. None of the surveyed alternatives ship a way to enforce your own banned-phrase list, achievement-weight rules, or required clauses —
lint_cvreads them from a YAML file you control, so the mechanism is reusable even though the facts it checks against are personal (see "Why a configurable linter" below).It doesn't draft or fact-check for you, on purpose. Writing the CV/cover-letter text and adversarially verifying every claim traces back to something true stays with whatever AI assistant calls these tools — this server is a mechanics layer (search, fetch, render, lint, log), not a weaker bundled LLM call duplicating judgment the calling assistant already does better with full context.
What this server doesn't pretend to be: it doesn't cover every job
board — only ones with a real API, or a single posting URL a human already
found. It never submits anything on your behalf. And a clean lint_cv run
doesn't mean a CV is honest, only that it passes the specific rules you
wrote — semantic fabrication (two true facts combined into a false-sounding
one) still needs a human or an independent adversarial review to catch.
Related MCP server: cv-job-assistant
Install
From PyPI (published as mcp-job-search-mp — mcp-job-search was already
taken by an unrelated project; the command stays mcp-job-search):
pip install mcp-job-search-mp
# or, for an isolated tool install:
uvx mcp-job-search-mpFrom source, for development:
git clone https://github.com/michalper/mcp-job-search.git
cd mcp-job-search
python3.12 -m venv .venv
.venv/bin/pip install -e ".[dev]"Connecting to Claude Code
{
"mcpServers": {
"job-search": {
"command": "/absolute/path/to/mcp-job-search/.venv/bin/mcp-job-search",
"env": {
"LINT_RULES_PATH": "/absolute/path/to/your/lint_rules.yaml",
"REGISTRY_CONFIG_PATH": "/absolute/path/to/your/registry.yaml"
}
}
}
}Configure via environment variables — all optional, every tool degrades gracefully when something is unset:
Variable | Used by | Default |
|
| unset — Adzuna skipped |
|
|
|
|
| unset — JSearch skipped |
|
|
|
|
| bundled |
|
| bundled |
|
| auto-detected (macOS app bundle, Linux |
|
|
|
|
|
|
Adzuna and JSearch free-tier limits
Adzuna's free tier is 25 one-time test calls, then a paid plan (£8/month,
5,000 calls) — budget calls accordingly once past testing. JSearch's free
tier is 200 requests/month; search_jobs caches results by query
(SEARCH_CACHE_DIR/SEARCH_CACHE_TTL_SECONDS, default 6h) specifically to
protect that quota from repeated identical queries — pass use_cache=False
to force a fresh one when you actually need it.
Sites behind active bot-detection
fetch_job_posting will never try to solve a CAPTCHA or bot-detection
challenge automatically, on any site, regardless of how narrow the request
— confirmed via response headers that Pracuj.pl and Indeed (pl.indeed.com)
both sit behind an active Cloudflare challenge (cf-mitigated: challenge /
401 with a Cloudflare bot-management cookie), not just a missing-JavaScript
issue. A real, human-driven browser session passes both normally — for a
page like this, read it with a real browser instead of this tool.
Example: catching a CV the way lint_cv sees it

The CV draft here (tests/fixtures/sample_cv_with_violations.html) was
seeded on purpose, and lint_cv is pointed at the bundled generic example
rules (src/mcp_job_search/examples/lint_rules.example.yaml), not a real person's facts:
{
"clean": false,
"findings": [
{"severity": "error", "message": "generic corporate jargon — replace with a concrete claim"},
{"severity": "error", "message": "unsupported superlative — ground it in a specific result or cut it"},
{"severity": "error", "message": "example: list cloud/K8s here only if you have real production experience"},
{"severity": "error", "message": "a .gitignore edit is hygiene, not an achievement"},
{"severity": "error", "message": "a commit count is not an achievement on its own"},
{"severity": "error", "message": "example: flag a title claim that contradicts your real specialization"},
{"severity": "note", "message": "link rendered as <a href> in body (https://example.com/portfolio) — use plain text"}
]
}clean is false because real rules fired, not a guess — and every message
came from the YAML file, not from code that knows anything about a specific
person's career.
Tools
Tool | Status | Description |
| working | real search across Arbeitnow (free, no key), Adzuna and JSearch (API keys) — verified live against the Polish PHP market on both paid sources (10 real listings each, recognizable employers); results cached to protect JSearch's free quota |
| working | single-URL extraction via |
| working | headless-Chrome HTML→PDF with the exact flags that avoid known hangs/silent failures; cross-platform |
| working | configurable mechanical linter — banned phrases, skills-section-only bans, achievement-weight overclaims, title/GDPR/link checks, page-count limits, all driven by a YAML file you control |
| working | appends one row to a Markdown application registry in the correct section and date position, with a |
| working | read-only query over the registry (filter by portal/company) — use before |
| working | rewrites one row's status cell in place; refuses to guess when more than one row matches |
Verified job-board coverage (fetch_job_posting, 2026-10-19)
Site | Result |
JustJoin.it | ✅ clean extraction |
NoFluffJobs | ✅ clean extraction |
Bulldogjob | ✅ clean extraction |
theprotocol.it | ✅ clean extraction |
Solid.Jobs | ✅ clean extraction |
CzyJestEldorado.pl (aggregator) | ✅ clean extraction |
LinkedIn ( | ✅ clean extraction — the single-posting view is public |
Jooble | N/A — redirects to the source site (e.g. JustJoin.it); nothing of its own to fetch |
Pracuj.pl | ❌ blocked — active Cloudflare challenge, not attempted |
Indeed ( | ❌ blocked — active Cloudflare challenge, not attempted |
Why a configurable linter, not a hardcoded one
The original motivation: a CV draft once conflated two separate true facts
into one false-sounding claim, and it shipped before anyone caught it. A
grep-based mechanical check can catch a banned phrase; it can't catch
semantic conflation — that still needs a human or an independent adversarial
review. What a mechanical linter can do reliably: enforce your own
banned-phrase list, page limits, and required clauses, consistently, every
time, without you re-checking the same things by hand. lint_cv is that —
generic, so anyone can point it at their own facts.
Development
.venv/bin/python -m pytest --cov # tests + coverage
.venv/bin/ruff check . # lint
.venv/bin/mypy src # types
.venv/bin/bandit -c pyproject.toml -r src # security
.venv/bin/pip-audit # dependency vulnerabilitiesRun .venv/bin/pre-commit install once to run all of the above (except
pip-audit, which needs network access and stays CI-only) on every commit.
One manual end-to-end check (not run in CI — needs live network and a real
Chrome install), also documented in CHANGELOG.md:
search_jobs(keywords="PHP", remote_only=True, sources=["arbeitnow"])against the live Arbeitnow API.fetch_job_posting(url=...)on a URL from step 1.render_pdf(html_path=...)on a sample CV, confirmsuccessandpage_count.lint_cv(html_path=...)on a file with a seeded violation, confirm it's reported.log_application(..., dry_run=True)against your own registry, confirm the previewed row and insertion point look right before droppingdry_run.
See CHANGELOG.md for what changed in each release.
License
MIT — see LICENSE.
Available Tools
7 toolsfetch_job_postingA
Fetches ONE job-posting URL you already found (any job board, including ones with no public API) and extracts its readable text generically. This is for a single posting a human found manually — never call it in a loop to enumerate a site's listings.
| Name | Required | Description | Default |
|---|---|---|---|
| url | Yes | A single job-posting URL. |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the burden and does well on scope: it works on any job board including ones without a public API, and it discloses the anti-enumeration constraint, which is genuine behavioral guidance beyond structured data. It is silent on failure modes (paywalled/blocked pages), auth needs, and rate limits, so it stops short of full disclosure.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two sentences, both earning their place, front-loading the scope ('ONE ... URL') before the anti-loop prohibition. No filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return values need no explanation, and there is only one required parameter. Scope, input constraint, and the misuse case are all covered; nothing an agent needs to invoke it correctly is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the baseline is 3, but the description adds real meaning: the URL must be a single posting page (not a listing or search page) and must already have been found by a human. That constrains valid inputs beyond the schema's generic 'A single job-posting URL.'
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb (fetches/extracts) and resource (one job-posting URL, its readable text), and scopes it with 'ONE' and 'generically'. The wording clearly separates it from search_jobs, which is the enumeration-style tool.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicit when-to-use ('a single posting a human found manually') and explicit when-not ('never call it in a loop to enumerate a site's listings'). The alternative behavior is named, so an agent can route correctly without inference.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
lint_cvA
Read-only — never modifies html_path, pdf_path, or any other file. Lints a CV/cover-letter HTML file against YOUR OWN configurable rule set (banned phrases, skills-section bans, achievement-weight overclaims, title/GDPR/link checks, page-count limits), read from LINT_RULES_PATH (an env var, not a parameter here; defaults to a bundled generic example). Mechanical matching only — cannot catch semantic fabrication (two true facts combined into a false-sounding one), which still needs a human or independent review. Call before render_pdf while iterating on a draft (cheap, no Chrome, HTML-only checks); call again after render_pdf with pdf_path to also check page count and staleness — render_pdf itself runs none of this.
| Name | Required | Description | Default |
|---|---|---|---|
| pdf_path | No | Optional. Omit to run only the HTML-based checks (content, skills, weight, title, links). Pass the already-rendered PDF's path to ALSO check its page count against your configured page_limits and confirm it isn't older than html_path — this tool never renders a PDF itself; call render_pdf first if pdf_path doesn't exist yet. | |
| html_path | Yes | Path to the HTML file to lint, as currently saved on disk. |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the full burden and does so: it declares read-only/non-mutating behavior, discloses that rules come from LINT_RULES_PATH rather than a parameter, states the tool is cheap and HTML-only (no Chrome), and openly scopes the limitation to mechanical matching that cannot catch semantic fabrication.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loads the most important guarantee (read-only) and every sentence carries information: purpose, config source, limitation, and call timing. It is dense and somewhat run-on across five clauses, but no sentence is filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists so return format need not be explained; the description covers everything else an agent needs — mutation safety, configuration source and defaults, the draft-vs-post-render workflow, and the honest boundary of what linting can detect.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the schema baseline is 3; the description adds real value by explaining what omitting pdf_path loses, what passing it enables (page count vs configured page_limits, staleness vs html_path), and by documenting the out-of-band LINT_RULES_PATH configuration that the schema cannot express.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb ('Lints') and resource (a CV/cover-letter HTML file), then enumerates the rule categories checked (banned phrases, skills bans, overclaims, title/GDPR/link, page limits). It is clearly distinguishable from siblings like render_pdf, which it explicitly says runs none of this.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives explicit when-to-use sequencing: call before render_pdf while iterating on a draft, and again after render_pdf with pdf_path to add page-count and staleness checks. It also names render_pdf as the complementary tool and clarifies it does not itself lint.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_applicationsA
Read-only — never modifies the registry file. Returns rows from your application registry, optionally filtered by portal (exact, case-insensitive section heading) and/or company (case-insensitive substring match). Call this before log_application to check whether you've already applied to a company — the registry's own '## ' structure and column layout are returned as-is via each entry's fields dict, so this also works against registries with custom/non-English column names.
| Name | Required | Description | Default |
|---|---|---|---|
| portal | No | Exact portal section name, e.g. 'justjoin.it'. Omit for all. | |
| company | No | Case-insensitive substring match on the company field. |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full load and does well: it declares the operation is read-only and never modifies the registry file, and explains how results are shaped ('returned as-is via each entry's fields dict') and that custom/non-English column names still work. It doesn't cover failure modes or empty-result behavior, keeping it out of the top tier.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three sentences, each doing work, with the safety constraint and the primary routing hint placed first. The trailing clause about custom/non-English column names is somewhat dense and could be tightened, but nothing is filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return values needn't be spelled out, and the description still sketches the entry shape via the `fields` dict. For a two-optional-param read tool, an agent has everything it needs to select and invoke it correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the baseline is 3, but the description adds value by characterizing the filters as combining with 'and/or' and by clarifying that portal is an exact case-insensitive section heading rather than a fuzzy match. The company substring semantics largely restate the schema, so the addition is incremental rather than substantial.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb+resource ('Returns rows from your application registry') plus the filtering scope, which is enough to separate it from siblings like search_jobs and log_application without opening a schema. The read-only claim is front-loaded and unambiguous.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives an explicit pre-call condition — 'Call this before log_application to check whether you've already applied to a company' — which names the sibling and the situation that selects this tool. It lacks any negative guidance (e.g. when search_jobs is the better choice), so it stops short of a full when/when-not treatment.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
log_applicationA
Mutates the registry file — appends one new row, it does not check for an existing one first. Call list_applications(company=...) beforehand if you need to avoid logging a duplicate. Target file and column layout come from REGISTRY_CONFIG_PATH (default: the bundled generic example). Never invents text for free-text fields — only places exactly what you pass. Use dry_run to preview the row and insertion point without writing. To change an existing row's status later, use update_application_status instead of calling this again.
| Name | Required | Description | Default |
|---|---|---|---|
| date | Yes | ISO date (YYYY-MM-DD) of the application. | |
| mode | No | Work mode, e.g. 'Remote'. | |
| rate | No | Rate/salary as free text. | |
| role | Yes | Role/title applied for. | |
| level | No | Seniority/contract level, e.g. 'Senior B2B'. | |
| portal | Yes | Section heading to log under, e.g. 'justjoin.it'. | |
| company | Yes | Company name. | |
| dry_run | No | Preview without writing to the file. | |
| status_note | No | Free-text status/notes — placed verbatim. |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden and does well: it discloses that the append is unconditional (no duplicate check), where the target file and column layout come from (REGISTRY_CONFIG_PATH, defaulting to the bundled example), that free-text fields are written verbatim rather than invented, and that dry_run previews the row and insertion point. It stops short of error/permission behavior and what happens if the configured file is missing or malformed.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The most consequential fact — that this mutates the file with no dedup check — is front-loaded, followed by the config source, the verbatim-writing rule, the dry_run escape hatch, and the alternative tool. Five short sentences, none redundant.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return values need not be explained, and the description covers mutation semantics, config resolution, dedup avoidance, preview, and the sibling alternative for updates. It leaves minor gaps around failure modes (missing/unwritable registry file, config parse errors) that a mutation tool would ideally disclose.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the baseline is 3, but the description adds real meaning beyond the schema: it explains dry_run's preview-and-insertion-point semantics and clarifies that free-text fields (status_note, rate) are placed exactly as passed with no embellishment. It does not document the portal/company/date fields further, which is acceptable since the schema covers them.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Opens with a specific verb and resource: 'Mutates the registry file — appends one new row.' It explicitly distinguishes itself from siblings by naming list_applications for duplicate checking and update_application_status for later status changes, so an agent can route correctly without opening any schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives explicit when-to-use and when-not-to-use guidance: call list_applications(company=...) beforehand to avoid duplicates, use dry_run to preview without writing, and use update_application_status rather than this tool to change an existing row. All alternatives are named with the condition that selects them.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
render_pdfA
Renders one CV/cover-letter HTML file to PDF via headless Chrome, using the exact flags known not to hang or silently drop output. Checks both the exit code and the actual PDF page count — call this once per file, never in a batch loop on the same Chrome profile.
| Name | Required | Description | Default |
|---|---|---|---|
| pdf_path | No | Output PDF path; defaults to the HTML path with .pdf. | |
| html_path | Yes | Path to the source HTML file. |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden and delivers real behavioral context: the exact flags are chosen to avoid hangs and silently dropped output, and the tool verifies both exit code and actual PDF page count rather than trusting the process. The concurrency restriction (no batch loop on a shared Chrome profile) is a genuine side-effect warning, though permissions and failure/retry semantics remain unstated.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two sentences with no filler; the operation and its mechanism are front-loaded, and the constraint sentence follows immediately. Every clause earns its place.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return values need no explanation. For a simple 2-parameter render tool, the description supplies the reliability and concurrency context an agent actually needs to call it correctly without over-explaining.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so both parameters are already documented in the schema, including the pdf_path default. The description adds only the implicit reminder that html_path expects a single CV/cover-letter file, which is marginal value over structured data. Baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb (renders), a specific resource (one CV/cover-letter HTML file to PDF) and the mechanism (headless Chrome). No sibling tool overlaps this operation, so no disambiguation is required for an agent to select it.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives explicit operational guidance: one call per file, never in a batch loop on the same Chrome profile. That is a clear when/how-not constraint, though it names no alternative tool for the batch case.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
search_jobsA
Searches real job-board APIs (Arbeitnow — free, always on; Adzuna and JSearch — only if their API keys are configured via environment variables). Returns normalized jobs plus a list of source errors (e.g. a source skipped because its key isn't set) — never raises on a single source's failure. Results are cached by query for SEARCH_CACHE_TTL_SECONDS (default 6h) to protect JSearch's free quota; pass use_cache=False to force a fresh query. This does NOT scrape or search sites without a public API (e.g. NoFluffJobs, JustJoin.it, Pracuj.pl) — use fetch_job_posting for a specific URL on those instead.
| Name | Required | Description | Default |
|---|---|---|---|
| sources | No | Limit to these source names, e.g. ['arbeitnow']. Default: all. | |
| keywords | Yes | Keywords to match, e.g. 'PHP Magento backend'. | |
| location | No | Free-text location filter, e.g. 'Poland'. | |
| use_cache | No | Reuse a cached result. False forces a fresh query. | |
| remote_only | No | Only return remote-flagged postings. |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden and does so well: it discloses conditional source availability (Adzuna/JSearch only with env-var keys), graceful degradation (never raises on a single source failure, returns source errors), and a caching policy with TTL default 6h tied to protecting JSearch's free quota.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three front-loaded sentences covering capability, failure/caching behavior, and exclusions; each earns its place. The middle sentence is dense with several clauses but remains scannable.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return-value shape needn't be spelled out, yet the description still summarizes the payload (normalized jobs plus source errors). For a multi-source, cached search tool this covers everything an agent needs to call it correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the baseline is 3, but the description adds the rationale behind use_cache (quota protection, TTL default) that the schema only states mechanically. It does not add meaning for location/remote_only/sources beyond the schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Opens with a specific verb+resource ('Searches real job-board APIs') and immediately names the concrete backends it hits (Arbeitnow, Adzuna, JSearch). It explicitly distinguishes itself from fetch_job_posting by declaring what it does NOT do (scrape sites without a public API).
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives explicit routing rules in both directions: use this for keyword search across API-backed boards, use fetch_job_posting for a specific URL on sites like NoFluffJobs/JustJoin.it/Pracuj.pl. It also states the use_cache=False escape hatch for forcing a fresh query.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
update_application_statusA
Mutates the registry file — rewrites one row's status cell in place, leaving every other cell and the file's row order untouched. Finds the row by portal (exact) + company (case- insensitive substring); if more than one row matches, nothing is written and the error lists the matching line numbers — pass role to disambiguate instead of guessing which one was meant. Use dry_run to preview old_row/new_row without writing. Call list_applications first if you're not sure a row exists.
| Name | Required | Description | Default |
|---|---|---|---|
| role | No | Case-insensitive substring match on the role field, to disambiguate. | |
| portal | Yes | Exact portal section name, e.g. 'justjoin.it'. | |
| company | Yes | Case-insensitive substring match on the company field. | |
| dry_run | No | Preview without writing to the file. | |
| new_status | Yes | Replacement text for the Status cell, placed verbatim. |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden and does well: it discloses in-place single-cell rewriting, preserved row order, the abort-on-ambiguity rule with line numbers in the error, and a non-destructive dry_run preview. It stops short of stating what happens on zero matches or any file-permission/locking failure mode, which are the remaining behavioral unknowns for a mutating file operation.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with the mutation semantics, then matching rules, then failure handling, then optional flags. Dense but zero filler; each clause removes a decision the agent would otherwise have to guess.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return values need no explanation, and the description covers the mutation contract, matching rules, ambiguity handling, and preview mode. For a 5-parameter write tool with no annotations, an agent has everything needed to call it correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the baseline is 3, but the description clarifies matching semantics beyond the schema: portal is exact while company and role are case-insensitive substrings, and role exists specifically as a disambiguator when the company match is ambiguous. It adds real operational meaning rather than restating field docs.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a precise verb and target ("rewrites one row's status cell in place") plus the exact scope of the mutation (other cells and row order untouched). This distinguishes it from log_application (adds rows) and list_applications (reads) without needing to open their schemas.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives explicit routing rules: pass role rather than guess when multiple rows match, use dry_run to preview, and call list_applications first when unsure a row exists. It names siblings and the conditions that select specific behaviors, not just the tool.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
3 tool updates
- Changed
lint_cv2 fields changed- changed
Input schema / properties / html_path / descriptionPrevious value: -"Path to the HTML file to lint."New value: +"Path to the HTML file to lint, as currently saved on disk." - changed
Input schema / properties / pdf_path / descriptionPrevious value: -"If given, also checks page count and freshness."New value: +"Optional. Omit to run only the HTML-based checks (content, skills, weight, title, links). Pass the already-rendered PDF's path to ALSO check its page count against your configured page_limits and confirm it isn't older than html_path — this tool never renders a PDF itself; call render_pdf first if pdf_path doesn't exist yet."
- Added
list_applications - Added
update_application_status
5 tool updates
v0.1.0- First observed
fetch_job_posting - First observed
lint_cv - First observed
log_application - First observed
render_pdf - First observed
search_jobs
TDQS
Scored across 7 tools
Each tool has a clearly distinct action: search_jobs queries APIs while fetch_job_posting grabs one known URL, the three registry tools map cleanly to create/read/update, and lint_cv vs render_pdf are separate phases. The only mild overlap is search_jobs vs fetch_job_posting (both job-finding), but the descriptions explicitly draw the boundary.
All seven tools follow a consistent snake_case verb_noun pattern (fetch_job_posting, lint_cv, log_application, list_applications, update_application_status, render_pdf, search_jobs). No camelCase or stylistic drift.
Seven tools is well-scoped for a job-search assistant covering search, capture, CV lint/render, and an application registry. Each tool earns its place with no redundant entries.
The workflow is largely covered: find jobs, fetch postings, lint and render a CV, and log/list/update applications. Gaps remain in registry lifecycle — no delete/remove row and no way to edit free-text fields beyond status — but these are minor workarounds.
Maintenance
Related MCP Connectors
Search jobs, prepare resumes and cover letters, retrieve PDFs, and track your applications.
Job search for your AI: save jobs, research companies, keep stories, track applications.
1Search AI-native jobs, inspect application forms, and fetch free interview-prep resources.
AI job search MCP — fact-checked jobs, application tracker, alerts. ChatGPT, Claude, Cursor.
Related MCP Servers
- FlicenseNot gradedqualityCmaintenanceEnables running a job search with Claude Code: parses CV, discovers roles, fetches exact application fields, drafts non-trivial applications (positioning, not autofill), and renders an offline dashboard for review.-
- FlicenseNot gradedqualityCmaintenanceEnables Claude to parse CVs, search job boards (Remotive, Arbeitnow, Adzuna, Greenhouse/Lever), tailor resumes and cover letters, and prepare application packages with direct apply links—without ever auto-submitting. It runs 100% locally and free, storing jobs and applications as JSON files.-
- AlicenseNot gradedqualityBmaintenanceExposes the entire job-search workflow as 13 chat-driven tools, letting users refresh a ranked feed of live postings from employers' public hiring APIs, view and score job descriptions, tailor and render truthful one-page LaTeX resumes, review skill gaps and screening answers, and log applications. It keeps a human in the loop, since nothing is ever submitted automatically.MIT
- AlicenseNot gradedqualityAmaintenanceEnables AI agents to discover postings from watched company boards ranked by keyword fit, tailor and fact-check resumes against a source-cited profile, render ATS-readable PDFs, submit applications through the user's own browser, and track replies and follow-ups. All ranking, claim checking and email classification run as plain code, so LLM tokens are spent only on writing.1Apache 2.0