MCP tool reference
All tools share the same auth (Bearer API key) and the same per-session userId binding. Every list/get is automatically scoped to your account.
prepare_profile
Returns the local profile dir + Playwright command for opening a Chromium window pointed at a source's login page. The command pre-creates the profile dir and passes a real Chrome user-agent / viewport / lang so first-load doesn't get flagged as a headless probe. Run setupCommand verbatim; it blocks until the user closes the window, then call report_login.
{ "source": "hh" | "linkedin" | "getmatch" | "wellfound" }{
"profileDir": "~/.fmwork/<source>-profile",
"setupCommand": "mkdir -p \"$HOME/.fmwork/...\" && npx playwright open --browser=chromium ...",
"setupCommandNote": "Run verbatim. Blocks until window closes. Don't write your own opener.",
"stealth": { "userAgent": "...", "viewport": "1440,900", "lang": "ru-RU" },
"notes": ["source-specific tips for the human signing in"],
"afterLogin":"call report_login(source=..., ok=true) when the npx process exits"
}The command uses $HOME instead of ~ on purpose: shells do not expand ~ after `=` in argv, so `--user-data-dir=~/foo` would create a literal `./~/foo` directory in the cwd. Agents must NOT translate this into a hand-rolled Playwright script — UA / viewport / locale / user-data-dir must match what verify_login and search_vacancies use later.
verify_login
Returns a recipe AND a ready-to-run bash command (executable) the agent can paste straight into a shell. The command opens the persistent profile headlessly, navigates to the verify URL, evaluates the logged-out signals, and prints one JSON line on stdout. The agent parses it and calls report_login with the verdict.
{ "source": "hh" | "linkedin" | "getmatch" | "wellfound" }{
"url": "https://...",
"loggedOutSignals": {
"urlContains": ["string", ...],
"selectors": ["css", ...]
},
"pacingMs": [min, max],
"executable": "cat > /tmp/fmwork-verify-….cjs <<'__FMWORK_…__'\n…\nnpx -y -p playwright node /tmp/fmwork-verify-….cjs",
"executableNote": "stdout: { loggedIn, checkedUrl, matchedUrl, matchedSelector }"
}If the agent has a browser/Playwright MCP it drives the recipe directly. If its only tool is Bash, it runs `executable` as-is and parses the stdout JSON line. Agents must NOT write their own Playwright script — custom scripts diverge from the server stealth profile (UA / viewport / locale), point at the wrong user-data-dir, and get the user banned.
report_login
Tell the server the result of a prepare_profile sign-in (or a verify_login pass). Updates the per-user / per-source loggedIn flag and timestamp so check_login can return it later.
{
"source": "hh" | "linkedin" | "getmatch" | "wellfound",
"ok": boolean,
"note"?: "string"
}"recorded <source>=<state>"check_login
Last known login status for one source. Read before starting a scrape.
{ "source": "hh" | "linkedin" | "getmatch" | "wellfound" }{
"source": "<source>",
"loggedIn": boolean,
"checkedAt": "ISO-8601" | null
}health
Cheap polling-friendly health snapshot — call it mid-scan to see whether the server is up and how far the pipeline is behind. serverOk + per-user counters (total vacancies, processing, pending apply, applied, failed, last insert timestamp).
{}{
"serverOk": true,
"user": {
"total": number,
"processing": number, // not yet through the pipeline
"pendingApply": number, // cover_letter_ready or marked_to_send
"applied": number,
"failed": number,
"lastInsertAt": "ISO-8601" | null
}
}search_vacancies
Build a search recipe AND a bash executable for one source. Recipe = URL + CSS selectors + pacing for agents with a browser MCP. Executable = a paste-into-Bash command that runs the whole pass headlessly against the persistent profile and prints one JSON line per scraped vacancy to stdout (plus a final {done:true,…} line). Pair with submit_search_results to persist the rows. Returns { needsLogin, next } with isError when the profile is signed-out — do NOT scrape anonymously.
{
"source": "hh" | "linkedin" | "getmatch" | "wellfound",
"query"?: "string", // free-text; defaults to a resume-derived query
"workFormat"?: "remote" | "hybrid" | "onsite",
"limit"?: number // 1..50; per-source default ranges 8..25
}{
"source": "hh" | "linkedin" | "getmatch" | "wellfound",
"url": "https://...",
"listLinkSelector": "css selector for each result card's <a>",
"authWallSelectors": ["css", ...], // non-empty match = profile not logged in
"detail": {
"title": ["css", ...],
"body": ["css", ...],
"company"?: "css",
"location"?: "css"
},
"pacing": {
"afterListLoadMs": [min, max],
"betweenVacanciesMs": [min, max],
"onDetailMs": [min, max]
},
"maxJobs": number,
"notes": ["string", ...],
"executable": "cat > /tmp/fmwork-search-….cjs <<'__FMWORK_…__' … npx -y -p playwright node /tmp/fmwork-search-….cjs",
"executableNote": "stdout: one JSON row per vacancy, then {done:true,...}"
}Empty query → server derives one from your resume + preferred location. Respect the pacing block; Wellfound/LinkedIn will hard-ban for ignoring it. Agents without a browser MCP run `executable`; agents with one drive the recipe directly. Never write a custom scraper that bypasses both paths.
submit_search_results
Upload an array of vacancies you scraped. Server skips dupes (per-user vacancyId), then the worker scores each row and drafts a cover letter for the matches.
{
"source": "hh" | "linkedin" | "getmatch" | "wellfound",
"rows": [
{
"vacancyId": "string",
"title": "string",
"sourceUrl": "string",
"baseInfo": "string?",
"content": "string?",
"location": "string?"
}
] // 1..100
}"inserted=N skipped=M"Hard-capped at 100 rows per call. Large scrapes: batch and call repeatedly.
list_pending
Vacancies awaiting client action — anything with apply status cover_letter_ready or marked_to_send for the calling user.
{ "limit"?: number } // 1..100, default 20[
{
"vacancyId": "string",
"title": "string",
"source": "string",
"score": number | null,
"applyStatus": "cover_letter_ready" | "marked_to_send"
}
]pull_apply_queue
Full apply payload for everything the user marked as marked_to_send. Returns each vacancy's URL, the drafted cover letter, and the tuned resume text.
{ "limit"?: number } // 1..20, default 5[
{
"vacancyId": "string",
"source": "string",
"sourceUrl": "string",
"title": "string",
"coverLetter": "string" | null,
"tunedResume": "string" | null
}
]pull does NOT mark anything sent; call report_apply_result with ok=true after the client actually submits.
report_apply_result
Record the outcome of a single application. Server flips applyStatus to applied/failed and writes an ApplyEvent.
{
"vacancyId": "string",
"ok": boolean,
"message"?: "string"
}"recorded" | "vacancy not found"