An MCP job agent tool call schema is the JSON contract that tells an AI assistant exactly what arguments it must pass to submit a job application on a user's behalf, and what structured response it gets back. It's the same idea as any other Model Context Protocol tool definition, just applied to the messiest API surface in software: employer ATS forms.
If you've built an MCP server before, you know the pattern: a tool has a name, a description, an input schema, and an output. The hard part isn't the JSON Schema syntax. It's that a job application touches a dozen systems you don't control, each with different field names, different validation rules, and different failure modes. Get the schema wrong and your agent either submits garbage applications or silently fails a hundred times before anyone notices.
This piece is for the developer sitting where I used to sit: staring at a tool definition, trying to decide whether resume_url should be a string or an object, and whether the apply call should be synchronous or fire-and-forget. If you're the exhausted IT consultant trying to wire an agent to actually submit on your behalf rather than just read docs, start with how to connect Cursor or Windsurf to an MCP job agent first, then come back here for the schema internals.
What problem does the MCP apply function actually solve?
Before MCP, "auto-apply" tools were browser extensions that clicked buttons and hoped the DOM hadn't changed. That's brittle, and it breaks every time a company changes its careers page template. MCP flips the model: instead of the AI pretending to be a human clicking a mouse, it calls a defined function with structured arguments, and a backend service (the job agent) handles the actual submission, whatever form that takes on the other end.
In plain terms: the schema is the menu card. The AI assistant doesn't need to know how a Workday form differs from a Greenhouse form. It just needs to know what the apply tool expects as input, and the job agent server translates that into the right fields for whichever ATS is on the other side.
What does a minimal MCP apply tool definition look like?
Every MCP tool is declared with a name, a human-readable description (this matters more than people think, since the LLM reads it to decide when to call the tool), and a JSON Schema for inputs. A stripped-down apply tool looks roughly like this:
{
"name": "apply_to_job",
"description": "Submit a job application to a specific posting using the candidate's stored profile and resume. Call this only after confirming the job_id matches a posting the user approved.",
"inputSchema": {
"type": "object",
"properties": {
"job_id": { "type": "string", "description": "Internal GiraffyReach job identifier" },
"candidate_profile_id": { "type": "string" },
"resume_variant": { "type": "string", "description": "Which resume version to attach, e.g. 'cloud-security-v2'" },
"cover_letter": { "type": "string", "description": "Optional. Generated or user-supplied." },
"answers": {
"type": "object",
"description": "Key-value pairs for ATS screening questions, e.g. work authorization, salary expectation",
"additionalProperties": true
},
"dry_run": { "type": "boolean", "description": "If true, validate without submitting" }
},
"required": ["job_id", "candidate_profile_id", "resume_variant"]
}
}
That's the skeleton. The real complexity lives in three places: the answers object, error handling on the response side, and idempotency. Let's take each.
Why is the answers field the hardest part of the schema?
Screening questions are where every ATS diverges. Workday wants structured enums for work authorization. Greenhouse often uses free-text custom questions. Lever mixes both. A rigid schema with named fields for every possible question across every ATS would be unmaintainable, so most job agent schemas treat answers as an open key-value map and push the mapping logic server-side, not into the tool call itself.
The principle: keep the tool call schema stable and generic; push ATS-specific volatility into the agent backend, not into the LLM-facing contract. If you redefine the schema every time an ATS changes a field label, your AI assistant's cached tool definitions go stale and start failing silently.
- Use a flat
additionalProperties: truemap for screening answers rather than a rigid enum list. - Let the backend normalize question text (e.g. "Are you legally authorized to work in the US?" vs "US work authorization?") to a canonical key before storage.
- Version the canonical key set, not the tool schema, so you can evolve mappings without breaking the MCP contract.
How should the apply function handle responses and errors?
This is where most homegrown implementations fall apart. A naive design returns { "success": true } or throws. That's not enough information for an AI assistant to decide whether to retry, escalate to the human, or move to the next job. A production-grade response schema needs at least four states: submitted, queued, needs_input, and failed.
| Response status | What it means | What the agent should do next |
|---|---|---|
submitted | ATS confirmed receipt | Log it, move to next job |
queued | Submission accepted by job agent, ATS confirmation pending | Poll or wait for webhook, don't re-apply |
needs_input | A required screening question has no stored answer | Surface the exact question to the user or the calling assistant, then retry with answers filled |
failed | ATS rejected the submission (expired posting, duplicate, captcha) | Return a machine-readable reason code, not just a string, so the agent can branch |
One sentence takeaway: if your schema only has pass or fail, you've built a coin flip, not an agent. The needs_input state in particular is what separates a schema that handles real ATS forms from one that only works on demo postings. A good number of postings that look like clean one-click applies turn out to be ghost listings or honeypots that never confirm receipt at all, so your error schema needs a status for "submitted but unconfirmable" too, not just success and failure.
How do you keep the apply function idempotent?
An LLM-driven agent can call a tool twice. Maybe the model times out and retries, maybe the user re-triggers a batch run. Without idempotency, that means duplicate applications to the same employer, which looks worse to a recruiter than no application at all.
- Generate a deterministic idempotency key from
job_id + candidate_profile_idbefore the call reaches the backend. - Check for an existing submission record matching that key before touching the ATS.
- Return the prior result (not an error) if a duplicate call arrives within a defined replay window.
- Expose a
force_resubmitflag for the rare legitimate case, like reapplying after a profile update. - Log every call attempt independent of outcome, so you can audit what the agent tried versus what it says it did.
This matters more in MCP contexts than typical REST APIs because the caller is a language model, not a deterministic script. Treat every tool call as if it might arrive twice.
How does the apply schema fit into the bigger MCP job agent workflow?
The apply function is the last step in a chain, not a standalone tool. A full MCP job agent server typically exposes several tools working together: one to search fresh postings, one to pull candidate profile data, one to generate or select a resume variant, and the apply tool itself. The schema design only works if these tools share consistent ID types, for example the same job_id format returned by the search tool is what the apply tool expects as input.
Cause and effect: because job postings disappear or get flooded with applicants within hours of going live, the search tool has to surface fresh listings before the apply tool is even useful, which is why detection speed and schema design are really one engineering problem, not two. For the detection half of this, see how GiraffyReach detects a fresh posting before it hits LinkedIn search. For the compute cost question developers always ask next, there's a separate breakdown on who pays for AI compute during auto-apply.
What should you validate before shipping your own apply tool?
- Dry-run mode works end to end. If
dry_run: truedoesn't exercise the same validation path as a real submission, you'll ship schema bugs straight to production employers. - Every required field has a clear error message the LLM can parse and relay, not a raw stack trace.
- Resume variant resolution is explicit. Don't let the backend silently pick "most recent" resume if
resume_variantis omitted; fail loud instead, since the wrong resume on a submitted application can't be undone. If you're unsure which variants matter, this is directly tied to what keywords actually belong on a software engineer resume in the first place. - Multiple concurrent agents don't race on the same profile. If your users run more than one assistant against the same job agent, check how multiple MCP job agents running at once are expected to coordinate, or you'll get double submissions from two different clients instead of one.
Building toward something that actually submits
A clean schema is necessary but not sufficient. The thing that actually separates a working apply function from a toy demo is what happens behind it: does the job agent detect the posting fast enough to matter, does it handle the hundred ATS variants correctly, does it know when to pause and ask for human input instead of guessing on a screening question. GiraffyReach built MCP Agent Connect around exactly these constraints, so any AI assistant can call a stable apply tool while the detection, ranking, and ATS-translation work happens server-side. If you're designing your own integration, see how the production schema and agent pipeline are structured before you lock yours in.