MCP server that searches job listings from Taiwanese job boards and returns normalized results.
Supported sources:
- 104 — uses
curl_cffiChrome TLS impersonation; no login required - Yourator — uses Playwright headless browser; no login required
- CakeResume — uses
curl_cffiChrome TLS impersonation; no login required - LinkedIn — uses the public guest job-search API via
curl_cffi; no login required
Removed source: Meet.jobs was supported until the service permanently shut down on 2026年06月30日 (the site now serves only a closure announcement). The adapter was removed in July 2026.
python -m venv .venv source .venv/bin/activate pip install -e . playwright install chromium
To also enable Chrome cookie injection for Yourator (improves result quality on accounts with browsing history):
pip install -e ".[cookies]"Add to your .mcp.json:
{
"mcpServers": {
"job-source": {
"type": "stdio",
"command": "/path/to/.venv/bin/job-source-mcp"
}
}
}Health check. Returns {"ok": true}.
Returns readiness of each source. All sources work without login.
Search job listings across one or more sources.
{
"keyword": "golang backend",
"source": "all",
"page": 1,
"limit": 20,
"location": "台北市"
}source accepts: "all", "104", "yourator", "cakeresume", "linkedin".
Response:
{
"keyword": "golang backend",
"source": "all",
"count": 12,
"jobs": [
{
"source": "104",
"id": "abc123",
"title": "Golang Backend Engineer",
"company": "Acme Corp",
"location": "台北市信義區",
"salary": "80,000–120,000",
"url": "https://www.104.com.tw/job/abc123",
"posted_at": "20260601",
"tags": ["Go", "Kafka", "Redis"],
"description": "..."
}
],
"rate_limited": [],
"errors": []
}rate_limited lists any source that returned HTTP 429 even after backing off (each entry: {"source", "retry_after"}). It exists so a throttled source is never confused with one that simply found nothing: if count is 0 and rate_limited is empty, the search genuinely matched no jobs; if a source appears in rate_limited, its 0 results mean "couldn't fetch", not "no matches". Rate-limited sources also appear in errors with "type": "rate_limited" (other failures use "type": "error").
104 — Direct API call to https://www.104.com.tw/jobs/search/api/jobs using curl_cffi with impersonate="chrome110". This bypasses Cloudflare bot detection by presenting a real Chrome TLS fingerprint. No login or session cookie required.
Yourator — Playwright launches a headless Chromium browser, navigates to https://www.yourator.co/jobs?term=<keyword>, and intercepts the backend API response (GET /api/v4/jobs?term=<keyword>). The browser's persistent profile is stored in ~/.config/job-source-mcp/profiles/yourator/ so it is reused across runs.
If browser-cookie3 is installed, Yourator also injects cookies from your local Chrome profile, which may improve result relevance for logged-in users.
CakeResume — Fetches the search results page https://www.cakeresume.com/jobs?q=<keyword> (filtered to zh-TW) with curl_cffi using impersonate="chrome110", then parses the embedded Next.js __NEXT_DATA__ JSON blob to extract listings. No login or session cookie required. CakeResume caps each page at ~10 results. Returned url uses the cakeresume.com/jobs/<slug> path; the canonical clickable form is cake.me/companies/<company-slug>/jobs/<slug>.
LinkedIn — Calls the public guest job-search endpoint https://www.linkedin.com/jobs-guest/jobs/api/seeMoreJobPostings/search with curl_cffi (impersonate="chrome110") and parses the returned HTML job cards with BeautifulSoup. No login or session cookie required. Pagination uses an offset (start = (page - 1) * 10); each request yields ~10 cards. When location is omitted it defaults to Taiwan. Guest cards do not include a job description (left empty) and rarely include salary. LinkedIn is the most rate-limit-sensitive source, so requests use an adaptive low-frequency limiter (see Rate limiting).
| Environment variable | Default | Description |
|---|---|---|
JOB_SOURCE_DIR |
~/.config/job-source-mcp |
Base directory for Playwright browser profiles |
The 104, Yourator, and CakeResume adapters include a random delay (1.5–4 s) per request to simulate human browsing speed.
LinkedIn uses an adaptive process-wide rate limiter (job_source_mcp/throttle.py). It serializes outbound requests with a minimum spacing (≥ 8 s, plus jitter) so the server never calls this API at high frequency. When a source returns HTTP 429, the limiter escalates the interval and keeps it escalated (doubling, up to 120 s for LinkedIn) and backs off before a single retry — the response to throttling is to call less often, not to retry harder. The interval only relaxes gradually after sustained success.
When searching multiple keywords, call search_jobs sequentially rather than in parallel.
Install with the dev extras, then run the linter and tests:
pip install -e ".[dev]" ruff check . # lint pytest # tests (no network access required)
The test suite mocks the network layer (curl_cffi sessions) and drives the
adapters' parsers with fixtures, so it runs offline in well under a second. CI
(.github/workflows/ci.yml) runs the same ruff check + pytest across Python
3.11–3.13 on every push and pull request.