Ffindmynew.work
Docs/Tool reference
Open dashboard
REFERENCE

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.

INPUT
{ "source": "hh" | "linkedin" | "getmatch" | "wellfound" }
OUTPUT
{
  "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.

INPUT
{ "source": "hh" | "linkedin" | "getmatch" | "wellfound" }
OUTPUT
{
  "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.

INPUT
{
  "source": "hh" | "linkedin" | "getmatch" | "wellfound",
  "ok":     boolean,
  "note"?:  "string"
}
OUTPUT
"recorded <source>=<state>"

check_login

Last known login status for one source. Read before starting a scrape.

INPUT
{ "source": "hh" | "linkedin" | "getmatch" | "wellfound" }
OUTPUT
{
  "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).

INPUT
{}
OUTPUT
{
  "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.

INPUT
{
  "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
}
OUTPUT
{
  "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.

INPUT
{
  "source": "hh" | "linkedin" | "getmatch" | "wellfound",
  "rows": [
    {
      "vacancyId": "string",
      "title": "string",
      "sourceUrl": "string",
      "baseInfo": "string?",
      "content": "string?",
      "location": "string?"
    }
  ]   // 1..100
}
OUTPUT
"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.

INPUT
{ "limit"?: number }   // 1..100, default 20
OUTPUT
[
  {
    "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.

INPUT
{ "limit"?: number }   // 1..20, default 5
OUTPUT
[
  {
    "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.

INPUT
{
  "vacancyId": "string",
  "ok": boolean,
  "message"?: "string"
}
OUTPUT
"recorded" | "vacancy not found"
MCP tool reference · findmynew.work · findmynew.work