Skip to main content
Glama
pragnakalpdev64

job-apply-agent-mcp

Job Apply Agent - Test Project

A minimal Stagehand + Playwright project that fills Lever and Greenhouse job application forms using dummy candidate data stored in data/candidate.json.

Defaults to dry-run mode: it runs the full flow (navigate, discover fields from the DOM, Playwright fill, verify) but stops before clicking submit, so you can safely point it at a real company's posting without sending a fake application.

Project structure

lever-apply-test/
├── data/
│   ├── candidate.json          # dummy candidate profile (edit this to test different data)
│   ├── generate_dummy_resume.py
│   ├── dummy_resume.pdf        # generated locally, gitignored
│   ├── users/                  # per-user MCP profiles (gitignored)
│   └── screenshots/            # form screenshots (gitignored)
├── hermes/skills/job-apply/
│   └── SKILL.md                # Hermes skill for Slack / chat apply flow
├── src/
│   ├── config.py               # env var loading
│   ├── schemas.py              # Pydantic models (candidate + form/audit schemas)
│   ├── candidate_loader.py     # loads/validates candidate.json
│   ├── user_store.py           # per-user MCP profiles under data/users/
│   ├── mcp_server.py           # MCP stdio server for Hermes
│   ├── base_agent.py           # shared Stagehand session + Playwright fill/audit
│   ├── playwright_form.py      # DOM discover / fill / typeahead / readback
│   ├── field_mapper.py         # label → profile mapping (aliases, then one LLM call)
│   ├── session_handoff.py      # CAPTCHA / OTP live-view handoff
│   ├── greenhouse_locators.py  # Greenhouse Apply/Next/Submit + classic IDs
│   ├── lever_locators.py       # Lever Apply/Submit locators
│   ├── ats_detection.py        # auto-detect Lever vs Greenhouse from URL
│   ├── apply_lever.py          # Lever automation logic
│   └── apply_greenhouse.py     # Greenhouse automation logic
├── tests/
│   └── fixtures/               # local Greenhouse/Lever HTML for Playwright fill tests
├── main.py                     # CLI entry point
├── start_mcp.sh                # run the MCP server from this repo
├── requirements.txt
├── pytest.ini
└── .env.example

Related MCP server: mcpforwork

Supported ATS systems

ATS

Example URL

Auto-detected

Lever

https://jobs.lever.co/company/posting-id

Yes

Greenhouse

https://boards.greenhouse.io/company/jobs/12345

Yes

Greenhouse (alt)

https://job-boards.greenhouse.io/company/jobs/12345

Yes

Embedded Greenhouse

https://careers.example.com/jobs/123

No — use --ats greenhouse

What you need to actually run a test

  1. Python 3.10+

  2. Install dependencies

    pip install -r requirements.txt
    playwright install chromium

    playwright install downloads the actual browser binary - this is a separate step from pip install, easy to forget.

  3. An LLM API key. Stagehand still launches the browser and is the fallback for widgets Playwright cannot target. Copy .env.example to .env and fill in MODEL_API_KEY (Anthropic, OpenAI, or Azure OpenAI all work). Playwright-first fixture tests do not need a model key.

  4. Generate the dummy resume once:

    python data/generate_dummy_resume.py
  5. A real job posting URL to test against. Dummy candidate data only gets you halfway - you need an actual live apply page for Stagehand to interact with.

    Important: don't run --live against a real company's posting - that sends an actual application under fake data to a real employer, which is both bad practice and likely a ToS violation. Two safer options:

    • Run in dry-run mode (the default) against a real posting - it fills and verifies but never submits, so it's safe to test the fill logic end-to-end.

    • Set up your own sandbox/test job if your team gets a trial account, and only use --live against that.

  6. (Optional) A Browserbase account if you want cloud browsers, session replay, or their captcha-handling infrastructure instead of running a local Chromium window. Leave BROWSERBASE_API_KEY blank to just run locally - simplest for early testing.

Running it

# Lever - auto-detected from URL (dry-run, default)
python main.py --job-url https://jobs.lever.co/some-company/some-posting-id

# Greenhouse - auto-detected from URL
python main.py --job-url https://boards.greenhouse.io/some-company/jobs/12345

# Custom-domain Greenhouse embed - explicit override
python main.py --job-url https://careers.example.com/jobs/123 --ats greenhouse

# Watch it work instead of running headless
# (set HEADLESS=false in .env, which is also the default)

# Only once you have your own sandbox posting to test against:
python main.py --job-url <your-sandbox-url> --live

# Test with different candidate data
python main.py --job-url <url> --candidate data/another_candidate.json

# Leave browser open after fill so you can manually verify fields
python main.py --job-url <url> --keep-open

# Skip the terminal prompt for fields still blank after the automatic retry
python main.py --job-url <url> --no-prompt

Set KEEP_BROWSER_OPEN=true in .env to make this the default behavior.

MCP server (Hermes / Slack)

The apply CLI is unchanged. src/mcp_server.py exposes the same flow as MCP tools so Hermes can call them from Slack or another chat app.

Run it locally (stdio):

python -m src.mcp_server

Point Hermes at it (~/.hermes/config.yaml or your active profile):

mcp_servers:
  job_apply:
    command: "/absolute/path/to/lever-apply-test/venv/bin/python"
    args: ["-m", "src.mcp_server"]
    timeout: 600
    env:
      PYTHONPATH: "/absolute/path/to/lever-apply-test"

Or run ./start_mcp.sh from this repo.

Start Hermes from this repo (or ensure the python module path can see it), then /reload-mcp.

Tools:

Tool

What it does

detect_ats

Lever vs Greenhouse from the job URL

get_candidate_profile

Load data/users/{user_id}/profile.json, or data/candidate.json if none

upsert_candidate_profile

Save a profile for that chat user_id

apply_to_job

Dry-run fill + verify, then attach form screenshot(s) for Hermes

provide_application_answers

Save extra field values from the Slack user (label → value)

resume_application

Retry the last dry-run job after extra answers are saved

submit_application

Live submit — only after reviewing the dry-run / screenshots

provide_otp

Submit an OTP while a live handoff session is open

close_captcha_handoff

Close a CAPTCHA live-view handoff

close_handoff

Close any open OTP / CAPTCHA handoff

Hermes must pass user_id (for Slack, the member id like U01ABC2DEF3). That is how the server tells users apart.

apply_to_job / resume_application / submit_application return JSON plus JPEG screenshots of the filled form (pre-submit, and post-submit when live). The JSON includes fill_audit (filled, missing, unknown, reason), fill_attempts, and needs_user_input / questions when fields are still blank. Screenshots are also saved under data/screenshots/.

CLI missing fields: after 1 automatic retry, the terminal asks for each blank field and fills them in the same browser session. Use --no-prompt to skip. --keep-open is still available for visual review.

Hermes missing fields: the browser session closes (Slack round-trips are too slow to hold Chromium). If needs_user_input is true, Hermes should ask the user for questions, call provide_application_answers, then resume_application. Extra answers are stored on the user profile as extra_answers and reused on later jobs.

Example Slack flow: save a profile → “apply to <job url>” → if asked, supply missing answers → review the screenshot(s) → “submit it”.

Notes

  • ATS auto-detection uses the job URL hostname. If detection fails (e.g. Greenhouse embedded on a custom careers domain), pass --ats lever or --ats greenhouse explicitly.

  • Greenhouse multi-step forms discover and fill each step, then click Next / Continue only if Submit is not visible. Optional EEO/demographic sections are skipped or answered with "Decline to answer" when possible.

  • Resume upload uses the held Playwright page (set_input_files) when CDP is available, then falls back to Stagehand act() if needed.

  • Location / city fields are treated as searchable typeaheads: Playwright types the value and clicks a matching suggestion, with Stagehand as fallback.

  • Candidate profile is nested: personal, answers, education, skills, documents, demographics, consent, plus extra_answers for job-specific questions that are not in the schema. Existing profiles still load; new sections default to empty. Dummy values live in data/candidate.json.

  • Custom / extended questions (education, skills, DOB, citizenship, relocate, salary, notice, consent flags) are filled from that structured profile. Anything still unmatched goes to CLI/Hermes extra_answers instead of being invented.

  • Label mapping matches DOM-discovered fields to the profile with aliases first (no LLM). Only unmatched labels use one off-browser mapping call. Values always come from the candidate profile. Playwright fills by selector / label; Stagehand act() is last resort for leftover widgets.

  • Form screenshots are captured after fill (and after live submit) so Hermes can show the filled application without opening a headed browser. This needs a CDP URL on the Stagehand session (typical with Browserbase; local mode may omit it). Keep Hermes job_apply.timeout at 600.

  • Pre-close fill audit: after fill, the agent reads real input.value / selected options from the DOM and logs filled vs missing vs unknown. unknown means the control was not on this step or could not be read — it is not the same as empty. If expected fields are still blank, only those lagging fields are retried once. Live submit is blocked while missing is non-empty. Unmapped fields are not invented: CLI prompts in-session, Hermes asks via chat then retries in a new session. The audit is logged again just before the session closes and is returned to MCP as fill_audit + fill_attempts.

  • CAPTCHA is not solved automatically. Live submissions may hit CAPTCHA on both Lever and Greenhouse; dry-run mode never clicks submit.

  • Login-required postings and account-creation flows are not supported in this version.

  • The exact Stagehand Python method signatures may drift slightly as the SDK evolves. If something doesn't match, check https://docs.stagehand.dev first.

F
license - not found
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

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

  • A
    license
    -
    quality
    D
    maintenance
    An 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.
    33
    1
    AGPL 3.0
  • A
    license
    B
    quality
    B
    maintenance
    MCP server for job search, enabling profile creation, job hunting, review, and supervised application preparation without auto-submission.
    41
    Apache 2.0
  • A
    license
    -
    quality
    C
    maintenance
    An 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.
    MIT
  • A
    license
    A
    quality
    C
    maintenance
    A privacy-first MCP server for locally managing job, fellowship, and graduate-school applications. It offers tools for tracking application status, analyzing role fit, generating LaTeX CV/cover letters, interview prep, and discovering public jobs from ATS APIs.
    8
    MIT

View all related MCP servers

Related MCP Connectors

View all MCP Connectors

Latest Blog Posts

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/pragnakalpdev64/job-apply-app_mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server