A job application automation server that searches and applies to jobs on LinkedIn and Indeed with stealth and human-like behavior simulation.
JobPilot is an HTTP-based job automation MCP server with 7 tools spanning job search and application workflows. However, the implementation has significant definition quality gaps. Most tools lack parameter descriptions, only platform, query, location, job_url, resume_path, and dry_run have descriptions; email and password parameters in login/login_indeed have no descriptions. Input schemas are present but minimal. Tool descriptions are present but generic (e.g., 'Search for jobs on the specified platform (LinkedIn or Indeed) based on query and location' for search_jobs is only 92 chars and does not explain WHEN to use it vs search_jobs_indeed, what it returns, or dependencies). Error handling is absent, no recovery guidance, no categorization of retryable vs fatal errors. The codebase shows defensive patterns (e.g., check if bot.page exists before closing) but these are not surfaced in tool definitions. No tool is marked with readOnlyHint/destructiveHint annotations despite clear semantic differences (apply_to_job is WRITE, search_jobs is READ_ONLY). Naming follows verb_noun convention (search_jobs, apply_to_job, login) which is positive, but there is functional duplication: both search_jobs (platform-agnostic wrapper) and search_jobs_indeed (platform-specific) are exposed, forcing the LLM to choose between them. Similarly, login and login_indeed are both exposed. Output schemas are not documented, JobResult is defined in the router but not returned explicitly in tool definitions.
Trigger an application process to a job. If dry_run is True, it will stop before submission and save a screenshot.
Apply to an Indeed job.
Get the status of the MCP server.
Log in to the job platform (LinkedIn or Indeed) with provided credentials.
Log in to Indeed with stealth and delays.
Search for jobs on the specified platform (LinkedIn or Indeed) based on query and location.
Search for jobs on Indeed.
Functional duplication: both search_jobs and search_jobs_indeed are exposed as separate tools, forcing the LLM to choose between a platform-agnostic wrapper and Indeed-specific implementation. This violates the composition principle 'Avoid multiple tools that do the same thing differently'.
Parameter descriptions missing or incomplete: email and password parameters in login and login_indeed tools have no descriptions. LLMs cannot infer whether these are user credentials, OAuth tokens, or system secrets.
Tool descriptions are too generic and lack actionable context. For example, search_jobs description (92 chars) does not explain when to call it vs search_jobs_indeed, what structure is returned, or whether pagination is supported. No guidance on prerequisites (e.g., must login first?).
| Scored | Grade | Overall | Spec posture | Rubric |
|---|---|---|---|---|
| 2026-09-22 | F | 48 | 2026-07-28+ | v2 |
| 2026-03-09 | F | 39 | - | v1 |
Output schemas not documented in tool definitions. JobResult model is defined in router but not exposed in MCP tool schema. LLMs cannot plan downstream calls or extract required fields (title, company, url, platform).
No error handling guidance. Tools can fail (e.g., 'Unsupported platform', browser initialization timeout, authentication failure) but descriptions do not explain recovery paths or error categorization. LLM cannot retry intelligently.
No tool annotations (readOnlyHint/destructiveHint/idempotentHint). apply_to_job and login are WRITE operations but not marked as destructive/dangerous. search_jobs is READ_ONLY but not marked as such. LLMs cannot reason about side effects.
Dry-run not enforced safely. apply_to_job and apply_to_job_indeed accept dry_run parameter, but descriptions do not clarify what 'stop before final submission' means exactly, whether it is guaranteed not to modify state, or how the LLM retrieves the screenshot for review.
No parameter constraints documented. platform parameter accepts 'linkedin' or 'indeed' but no enum or validation rule is stated in descriptions. LLM could pass 'linkedin_jobs', 'LINKEDIN', or other variants, causing failures.