job-apply-agent-mcp
MCP server that automates applying to jobs on Lever and Greenhouse ATS systems. It fills forms from a candidate profile, verifies fields, and supports dry-run or live submission, with optional chat integration via Hermes.
README
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
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
-
Python 3.10+
-
Install dependencies
pip install -r requirements.txt playwright install chromiumplaywright installdownloads the actual browser binary - this is a separate step frompip install, easy to forget. -
An LLM API key. Stagehand still launches the browser and is the fallback for widgets Playwright cannot target. Copy
.env.exampleto.envand fill inMODEL_API_KEY(Anthropic, OpenAI, or Azure OpenAI all work). Playwright-first fixture tests do not need a model key. -
Generate the dummy resume once:
python data/generate_dummy_resume.py -
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
--liveagainst 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
--liveagainst that.
-
(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_KEYblank 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 leveror--ats greenhouseexplicitly. - 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 Stagehandact()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, plusextra_answersfor job-specific questions that are not in the schema. Existing profiles still load; new sections default to empty. Dummy values live indata/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_answersinstead 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.timeoutat 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.unknownmeans 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 whilemissingis 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 asfill_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.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。