AI-powered job application pipeline as an MCP server for Claude — scan boards, score fit, compose every answer; you review and submit
Moonlighter has 7 tools with complete descriptions (194 - 350 chars, within baseline 34 - 392 range) and clear input schemas. Naming follows verb_noun convention (scan_*, list_*, get_*, add_*, verify_*, archive_*). However, schemas lack explicit type declarations and descriptions for most parameters. For example, 'keywords' in scan_and_evaluate has no type annotation visible, and 'phase' lacks an enum constraint despite accepting only 4 values. Parameter descriptions are present but minimal (10 - 30 chars). Output schemas are not documented. Error handling is absent, no recovery guidance, no categorization of retryable vs fatal errors. Tools are well-composed (each does one thing) and accept human-friendly identifiers (company names, job URLs), but lack pagination support for list_jobs despite potentially returning many results.
Evaluates a manually provided job and saves it to the database. Useful for LinkedIn postings, job posts, or any source not supported by the automatic scanner. For Greenhouse and Recruitee URLs, any missing 'company', 'title', or 'description' is auto-filled from the ATS's own API. For everything else, if 'description' is not provided, tries to fetch the URL via HTTP (doesn't work for pages requiring authentication, like LinkedIn) — 'company' and 'title' are still required in that case.
Detect and archive (status='archived', closed_at set) jobs that disappeared from their source. Checks jobs currently in new/reviewed/applying/needs_review against the source's current listing (Greenhouse/Lever/Ashby API, or a LinkedIn page revisit). A company whose check fails (network error, malformed response) is reported explicitly and left untouched — never silently archived by mistake.
Get full details of a job posting.
List jobs from DB filtered by status. Ordering is rejection-aware (see priority.py): a company that recently rejected an application sinks toward the end and carries a warning badge — the persisted score is untouched and nothing is hidden.
Scan job boards, evaluate with LLM, return new jobs above threshold. By default scans only phase 1 (priority BR companies) to save tokens. Use phase='phase2', 'phase3', or 'all' to explicitly scan more companies.
Parameter schemas lack explicit type declarations and enums. 'phase' in scan_and_evaluate accepts only 4 values but is defined as free-form string; 'source' in scan_company should be an enum (greenhouse|lever|ashby|workable|recruitee|smartrecruiters); 'status' in list_jobs should be an enum. Free-form strings invite hallucinated values.
Output schemas are not documented. LLMs cannot plan downstream calls or extract required fields (e.g., job_id from list_jobs for use in verify_job). Baseline: 100% of A+ tools document return types.
No error handling or recovery guidance. Tools lack categorization of retryable vs fatal errors, actionable error messages, or suggestions for next steps. E.g., if scan_company fails due to network error, LLM has no guidance on retry strategy.
Inferred effective spec: <=2025-11-25.
| Scored | Grade | Overall | Spec posture | Rubric |
|---|---|---|---|---|
| 2026-09-23 | C | 61 | <=2025-11-25 | v2 |
Scan every open posting at ONE company right now and evaluate the new ones. Does not touch company_list.yaml — use it for ad-hoc checks ("what is open at trm-labs on Ashby?") without editing config.
Re-evaluates a job that couldn't be scored automatically because its description was empty, using text you copy from its real page. Use this when list_jobs(status="needs_review") shows a pending job: open its URL, select the whole page (Cmd+A), copy it (Cmd+C), and pass that text here. The job is scored with the same evaluator as any other job and moves to 'new' or 'archived' depending on the configured threshold.
list_jobs lacks pagination support (no limit default, no offset/cursor, no total count). Baseline: tools returning lists should accept page/offset and limit. Without pagination, large result sets blow context window.
get_job has minimal description (50 chars, below baseline 194 avg). 'Get full details of a job posting' lacks context on when to call it vs list_jobs, what fields are returned, or whether it includes related IDs needed for downstream tools.