Skip to main content
Glama

Server Configuration

Describes the environment variables required to run the server.

NameRequiredDescriptionDefault

No arguments

Instructions

Guidance the server publishes about itself, which clients place ahead of the tool catalog so the model reads it before choosing anything.

This server publishes no instructions, or was last inspected before Glama recorded them.

Capabilities

Features and capabilities supported by this server

Protocol revision2025-11-25

CapabilityDetails
tools
{
  "listChanged": false
}
prompts
{
  "listChanged": false
}
resources
{
  "subscribe": false,
  "listChanged": false
}

Tools

Functions exposed to the LLM to take actions

NameDescription
list_base_resumesA

List base resumes (slug, display name, summary). By default only the ones that can be picked; include_archived=true adds the archived ones, each with its archived_at date (archiving hides a resume from scoring and the pickers without deleting it).

get_base_resumeB

Get a base resume's full data JSON, render status, and metadata.

list_jobsA

List stored jobs as a thin summary array, newest first, paged by limit (default 50, max 500) and offset; a page shorter than limit is the last. Each item is a slim projection (company, title, role_category, level, employment_type, work_mode, location, salary, work_authorization, opt_accepted, best_ats_score: the best stored base-resume score, null when unscored) of roughly 0.8k characters, with no raw_text or extracted_json; use get_job for full detail. Optionally only jobs without an application. Each job carries ownership (owned_here, owner, handover, pending_requests), identifying replicas and jobs being handed over.

get_jobB

Get a job with its most recent application. The nested job carries ownership (owned_here, owner, handover, pending_requests), identifying replicas and jobs being handed over.

get_applicationB

Get one application's full detail: the tailored resume (customized_json) and the joined job. Use compare_ats for before/after scores.

list_referralsA

List referral contacts (company, careers URL, contact name, notes, applications_count).

list_qa_entriesB

List an application's screening answers and cover letters, newest first: the ones the app's model generated and the ones a caller stored.

run_health_checkA

Run the JD-independent resume health check on a base resume or a tailored application. kind is 'base' (then key is the base-resume slug) or 'application' (then key is the application id). Classifies every bullet on the evidence ladder, checks structure/content gates, and returns {score, grade, tier, next_grade, gates, counts, findings, judged} ranked by what each defect costs. Concrete qualitative results can earn full credit. An ask/fix finding carries question, ask_kind (measure|detail|reword), measure_target, alt_question, gain and evidence. Notes score zero: the evidence.no_numbers flag and language.* wording notes. Disputes and the word bank are web-only for now. judged is {done, pending, by}: bullets a judge has read, bullets waiting, and the judges' model ids. When Maestro's own model does not judge (no model key is set, or the user's settings say the agent app writes), no model is called: only stored judgments count (any judge, current rubric version); score, grade and next_grade are null while a scored bullet is pending, and judging is {rubric_version, pending_ids, prompt}, where prompt is the app's own evaluator prompt filled with the pending bullets; judge_health_bullets stores answers in that prompt's output shape. create_tailoring_session returns 409 when the base resume's latest, current report has an unwaived failing fatal gate; this call produces that report.

get_health_reportA

Fetch the most recent stored health report for a base resume or application without re-running it. kind is 'base' or 'application', key is the slug or application id. Returns {score, grade, tier, next_grade, gates, counts, findings} (fields as in run_health_check), or an error if no report exists yet (run_health_check creates one).

judge_health_bulletsA

Stores evaluations of a resume's pending bullets while Maestro's own model does not judge them (no model key is set, or the agent app writes), then rebuilds its health report. kind is 'base' or 'application', key the slug or application id. classifications is the classifications list in the output shape that judging.prompt from run_health_check states, one entry per pending id. Each entry gets the checks a model's answer gets: level is one of direct, analogue, adjacent, implied, unaddressed; analogue and direct need a verbatim quote of three or more words from the bullet in evidence, else the level is stored as adjacent; a measure question survives only when its target is in the bullet's own words. An id that is not pending (already judged, or its text changed) is rejected. Judgments are stored under this client's name and count while Maestro's own model does not judge. Returns the rebuilt report (fields as in run_health_check) plus accepted [{id, level as stored}] and rejected [{id, reason}]. 409 while Maestro's own model judges: a model key is set and Maestro's AI writes for agent apps.

waive_health_gateA

Escape hatch for the fatal-health-gate 409 from create_tailoring_session. Waiving bypasses a safety gate, so the stored waiver records an explicit user decision: reason is the user's stated reason for bypassing the named gate. The server stores the waiver as given and does not verify who made the decision. kind is 'base' or 'application', key is the resume slug or application id. The 409 names each failing gate by its label; the gate_id to pass is that gate's gates[].id in get_health_report.

unwaive_health_gateA

Remove a health-gate waiver and restore the gate's protection. Deletes the stored waiver for this gate (a no-op if none exists), so the gate blocks create_tailoring_session with a 409 again. The counterpart waive_health_gate records the user's decision to bypass a failing gate.

store_extracted_jdA

Store a job description the caller already extracted into JSON (the backend makes no LLM call).

extracted_json must match the app's JobExtraction schema: company, title,
role_category, level: "intern"|"entry"|"mid"|"senior"|"lead" (other spellings
map to these; any other level is kept in its own words), employment_type,
work_mode: "remote"|"hybrid"|"on_site" (stored as that key), city/state/country
(the job also stores country_code, the one ISO code they name),
location_raw, salary_min/max, salary_period, salary_currency (ISO 4217; a bare "$"
is no currency, so the job takes its country's, else the home one, and
salary_currency_assumed is true),
work_authorization: "sponsorship_available"|"no_sponsorship"|"citizen_or_gc_required"|"unstated",
opt_accepted: "yes"|"stem_opt_ok"|"no"|"unstated",
years_experience_min/max, skills[{skill_name, skill_category,
requirement_level: "required"|"preferred"|"mentioned"}],
responsibilities[], qualifications[]. ANY other work_authorization or
opt_accepted value silently normalizes to "unstated" with no error.
source is "user" (default) or "agent"; source="agent" marks agent-hunted jobs,
which need an open proposal before the apply helpers act on them. A job
stored with source="user" is not proposal-gated: the server skips the
open-proposal check for any non-agent source. Returns the stored job.

A posting already saved is returned unchanged with already_existed=true,
unless update=true: then this extraction corrects it and the job carries
correction. Already saved means the same raw_text (with none, the same
extraction or the same posting at source_url) or the same company and
requisition_id.
get_setup_statusA

Where this user's Maestro setup stands, for an agent starting fresh. Returns ai_key (whether Maestro has its own AI key), writer (who writes for agent apps: "agent", the default, means the agent app writes, so the tools where Maestro's own AI writes the text refuse (409) and the agent writes it and saves it with the matching store or add tool; "maestro" means Maestro's AI writes it with the user's key, which needs ai_key), demo_bases (slugs of the shipped demo resume, never the user's own), next_step (the first setup step an agent can do that is not done, {step, tool_set, says}; null when none is left), tool_sets (each set's name, use_when and holds, in journey order) and steps (the full setup status: each step's done and detail, form-answer readiness, suggested bases, PDF engines). The AI key, the writer and the default resume design are set by the user in the app and are never a next step.

get_tailoring_optionsA

The user's Quick tailor presets, as data an agent can explain or work around. Returns presets ([{key, on, means}]: each switch, whether it is on, and its plain meaning), instruction (an extra instruction the user gave for every tailor, may be empty) and persona (the user's own words on tone, may be empty). create_tailoring_session with use_presets follows these presets; a custom tailor walks the gaps with the user however the agent talks with them and saves the decisions with resolve_gaps. Either way the server keeps its rules: an unproven skill enters only a skills list, resume lines stay in the owner's implied first person, and saved keywords land. The presets are the user's, set in the app; no tool changes them.

get_job_search_briefA

Read the server-composed job-search brief: profile constraints (location, relocation, work-auth VERBATIM plus warnings[] when values are contradictory or missing — never guessed), persona text (may be empty, in which case the analytics-derived targets are the stronger signal), active base-resume summaries, role mix, top required skills, build areas (per-skill tier plus category_label, the plain-words label for the raw category key), referral careers pages (company + careers_url + has_contact), and counts of jobs captured in the last 30 days by role category. profile.work_auth_by_country holds those yes/no answers for each market the user applies in, keyed by market ("UK", not "GB"); profile.work_auth is the home market's set, named by work_auth_market and kept for one release; unless the user lives and applies in one country, each warning names its country. last_runs maps each automation to when its newest run that did not fail finished (UTC ISO; a failed run does not count; absent when none has). roles is the role catalog as {key, label}. It is the entry point of the agentic job-search workflow (playbook: docs/agentic-job-search.md), which covers capture, scoring and proposals; applying is outside its scope. sync carries enabled and role (home, remote, or null when sync is off). applying.answer_level is the Screening answers level (facts, history, queued).

sync_nowA

Run one sync round on the always-on copy. Returns its outcome (ok, transient, needs_person) and per-step counts, or a skip reason. Forces a round through the backoff window; an attempt within the last 30 seconds is skipped with 'Synced moments ago.'. Sync off returns "Sync isn't set up."; home returns "This is your laptop's copy; your bot runs the sync."

get_career_contextA

Read the user's career record from the Career KB, as format="json" (the default) or format="markdown".

json: `resume`, the approved-points resume view assembled from the profile and the
non-archived entities, and `memory`, the beyond-the-resume string: identity facts and
entity notes that don't belong on a resume. `resume` carries approved points only;
`memory` also includes the profile summary and notes and the non-archived entities'
notes.

markdown: the exact portable career.md generated from the current Career KB, as
deterministic Markdown text, the file the web's career export downloads.

Intended as grounding for writing about the user's career (LinkedIn posts, outreach
drafts), optionally beside a base resume for its voice. It never invents facts,
metrics, employers, or dates; anything absent from it (and from a fetched base resume)
is not part of the user's recorded career. Neither format carries IDs; the profile
set's career history tools return the IDs needed to correct anything. Read-only.
kb_list_entitiesA

List Career KB entities with their IDs (get_career_context returns prose with no IDs; this tool returns the IDs needed to edit). Optionally filter by kind (experience|project|education|certification|extra) or status (ongoing|completed|archived). Each item carries point_count, draft_count and approved_count (the bullets a resume can use; retired ones are in point_count only, except a bullet combined into another, which is counted nowhere) and used_in_bases, the base resumes it is on (archived included, deleted excluded).

kb_get_entityA

Full detail for one Career KB entity: dates, notes, its points (each with id, text and state), attached documents, and the activity timeline; also used_in_bases, the base resumes it is on (archived included, deleted excluded).

kb_list_pointsA

List Career KB points across all entities, optionally by state (draft|approved|retired), oldest first. An unfiltered call returns the first limit points (default 50, max 100); offset pages on. Each row carries id, entity_id/entity_title/entity_kind, text, state, origin, tags and dates; the per-use port history and merge provenance are omitted (kb_get_entity).

kb_groom_entityA

Similar bullets on one Career KB entity, clustered, with a proposed keeper. Deterministic and zero LLM calls: the pinned local embedder scores every pair of the entity's active points (drafts included, retired and user_cannot_confirm excluded) and joins pairs at 0.80 cosine and above, minus pairs the user marked "Not the same" at /career. Proposes only; nothing is written. Each cluster carries score, keeper_id, members [{id, text, state, origin, provenance, usage_count, best_score}] and pairs. The keeper rule is approved over draft, then most recently updated, then longest text. compared_wording is false when the embedder could not run; clusters is then empty. Acting on a cluster is kb_combine_points (one claim in other words: the others become the keeper's wordings) or kb_retire_points (a similar but stale one).

kb_captureA

Capture free-text career news into DRAFT points. Maestro's AI matches it to an existing entity or proposes a new one, with the user's API key (with none set it returns an error); pass entity_id to force placement. Refused (409) when the user's writer setting (get_setup_status) says the agent app writes; kb_add_points stores bullets the caller already wrote, with no LLM call. Points land as drafts for the user to approve at /career. The text is stored as the user's own words and facts; the server does not verify it.

kb_add_pointsA

Add bullets the caller wrote to an existing Career KB entity as DRAFT points, with no LLM call (kb_capture with entity_id, minus the model that splits and places free text). Drafts appear on no resume and in no career export until approved with kb_approve_points (or at /career). The text is stored as the user's own words and facts; the server does not verify it.

Store-time rules: en and em dashes become a hyphen in a range
("2020-2023") and a comma elsewhere, and one opening a bullet is dropped.
Resume text is implied first person:
a bullet where I, he or she opens a clause, his/her/my opens one or follows
its verb ("Grew his team"), or the owner's name appears returns 400 naming
the lines and stores nothing; they/their and pronouns about other people
are stored. A bullet already on the entity in any state
(case and spacing ignored), or repeated in the call, is skipped, so a
repeated call adds nothing; a rephrasing is added as a new draft
(kb_groom_entity reports similar bullets).

Returns {entity_id, entity_title, point_ids, skipped}: the new DRAFT point
ids and the skipped texts. An unknown entity_id is a 404; kb_list_entities
returns the ids, and kb_create_entity makes a new entity.
kb_edit_pointA

Reword a Career KB point or retag it. Changing the text sends the point back to DRAFT, so it leaves the composed resume and career export until it is re-approved (kb_approve_points or /career). Cannot approve, retire or delete a point. New text gets the kb_add_points rules: en and em dashes become a hyphen or a comma, and a third-person line is a 400 naming it.

kb_create_entityA

Create a Career KB entity — kind is experience|project|education| certification|extra. Writes immediately and is not draft-gated: the entity, with the dates and wording supplied, is stored as the user's own career record, and no duplicate is blocked (possible_duplicates hints are returned). detail is a free-form dict (conventional keys: tech list[str]|str, link, field); kind="extra" REQUIRES section_key (lowercase slug), section_type ("entries"|"bullets"), and section_title nested inside detail — no top-level params exist. A certificate PDF still has to be uploaded from the web UI.

kb_edit_entityA

Fix an entity's dates, title, org, notes or lifecycle status (ongoing|completed|archived). Omitted fields are left alone. Writes immediately with no draft gate; each supplied value replaces the stored one. Kind cannot be changed here — that is a rebuild, not a correction.

kb_edit_profileA

Update the Career KB profile: contact block, career summary, skill groups, notes. Omitted fields are left alone; a supplied field REPLACES its stored value wholesale (current values are in get_career_context), so changing one list item means sending the full merged field. Writes immediately with no draft gate.

kb_ingest_resumeA

Persist one caller-parsed resume into the Career KB. No in-house LLM.

Points land as DRAFTS and appear on no resume until approved with
kb_approve_points (or at /career), which records the user's approval.

HONESTY: `data` is stored as the user's own resume content, so a faithful
transcription is expected. The server does not detect embellishment,
rewording or invented metrics (same trust model as caller-authored tailor
ops); approved points become part of the career record.

ENTITY NAMES: matching is two-pass. Exact identity-key first (experience:
company+role+start_date; projects: name; education: institution+degree;
certs: the string), then a near-identity pass for experience/projects/certs
(token-subset; projects allow ≤2 extra tokens). Education and extra are
identity-key-only. Different spellings can merge on the second pass;
names outside its coverage still fork.

Re-running the same resume_key is a merge: no duplicate entities; a bullet
already on the entity (even retired) is not re-created. resume_key must
match ^[a-z0-9][a-z0-9_]*$.

`data` is ResumeData (only contact.name and contact.email required).
Sections: contact, summary, skills[{category, items}], experience, projects,
education, certifications[str], extra_sections. Experience:
{company, role, location?, start_date?, end_date?, bullets[str]}.
extra_sections are NOT stored in the KB (report warns); they can be added to
a base resume after it exists. skills/contact/summary go to the KB profile
(no draft state). Profile seeding is first-write-wins, so the first resume
ingested determines the profile.

Response {report, next}. report: created/matched entity ids, DRAFT point
ids, counts, warnings. next is a hint (null when suppressed/unavailable); brief=True
(multi-resume loops) suppresses it before any settings read. 422 on a
ResumeData validation failure or duplicate source key (atomic).
kb_approve_pointsA

Batch-approve Career KB points. Approved points are the only ones that appear on composed resumes and the career export; this is the single approval path available over MCP, and kb_retire_points retires points. The approval written is the user's decision about the listed points; the server does not check that the user reviewed them (a convention, not enforcement).

1-500 ids; repeats collapse to one result row. Per-id results are honest: unknown ids
return ok=false detail="not found" and the rest still proceed; a failed id fails the
same way until corrected (kb_list_points returns the current ids).

Response is {results: [{id, ok, state, detail}], next}. next is a hint
(null when suppressed or unavailable).
kb_combine_pointsA

Fold Career KB points into one: the keeper stays the bullet, and every point in point_ids (the others, not the keeper) is retired and recorded as one of the keeper's wordings. Resumes that use any of them keep their text; combining never edits a resume. Reversible from /career (Make its own bullet). The grouping written is the user's decision about the listed points; the server does not check that the user reviewed them (a convention, not enforcement, the same gate as kb_approve_points). Same entity required (400); a retired or user_cannot_confirm keeper, a user_cannot_confirm loser, and a loser the user stopped (retired but not folded) are refused (400), and so is a keeper that is itself folded into another; a loser already folded elsewhere moves to this keeper. Returns {keeper, repointed}: the keeper with its wordings, and any combined claims the fold moved from a loser onto it. 1-50 ids.

kb_merge_entitiesA

Fold one Career KB item into another of the same kind, the cure for a duplicate an import split off: merge_id goes, keep_id stays. Everything on merge_id moves to keep_id: its bullets in every state (approved, drafts, retired, and the wordings folded into a bullet), its evidence documents, and its record of which resumes its bullets were copied into.

keep_id keeps its own title, dates, status and notes. A field keep_id has blank (org,
start or end date, notes) is filled from merge_id, except the end date of an ongoing item,
and a detail key keep_id lacks is taken from merge_id. Lost: merge_id's title, its status,
its creation record, and each value of it where keep_id already holds a different one.
Bullets are not de-duplicated: one both items carried is there twice afterwards
(kb_combine_points folds them into one). Resumes are not edited. There is no unmerge.
The merge written is the user's decision about the two items; the server does not check
that the user agreed (a convention, not enforcement).

Refused: the same id twice, items of different kinds, `extra` items from different
sections, and an archived keep_id (400; an archived merge_id is fine), an unknown id
(404), and a merge of the same item running at once (409). Returns keep_id's detail,
kb_get_entity's shape.
kb_retire_pointsA

Batch-retire Career KB points: they stop being offered to resumes and the career export; resumes that already use them keep their text. Reversible from /career (Use again). The state written is the user's decision about the listed points; the server does not check that the user reviewed them (a convention, not enforcement, the same gate as kb_approve_points).

1-500 ids; repeats collapse to one result row. Per-id results are honest:
unknown ids return ok=false detail="not found" and the rest still proceed.
Response is {results: [{id, ok, state, detail}], next}; next is a hint
(null when suppressed or unavailable).
kb_sync_baseA

Sync one base resume into the Career KB.

Deterministic: zero LLM calls. New material lands as DRAFT points
(origin=base_sync) for the user to approve at /career. A bullet that
rephrases an existing point on the same item (0.80 cosine and above) is
kept as that point's wording, not added as a second point; only a new
claim lands as a draft. Never edits resumes. Safe to rerun — a second
call is a no-op when already in sync. compared_wording is false when the
embedder could not run: a rephrased bullet was then added as new rather
than kept as a wording (exact text still matches).
create_base_resume_from_kbA

Compose a new base resume from selected Career KB entities. LLM-free.

entity_ids is REQUIRED. An empty list composes nothing and 422s — that is
deliberate; omitting the argument is not allowed because it would have to
mean "the whole KB". A role_label or role_category is required (no slug
argument). role_category is strict: an unknown value 422s naming the
closest categories, no normalization. An unmatched role_label maps to
role_category "other" and slug `other`/`other_2`…; display_name never
affects the slug, which is `base.slug` in the response. Only APPROVED
points under the selected entities compose —
points from kb_ingest_resume are DRAFTS and contribute nothing until
kb_approve_points (or the user, at /career) approves them.
skills and contact arrive in full from the KB profile regardless of entity
selection (the KB has no per-entity skill data), so skills may need trimming
on the resulting base. The
whole-career summary is dropped unless include_summary, or pass a reviewed
summary. A selected job or project with no approved point is left out, and
a 422 "None of the items you picked has approved bullets." means none has any.

Response is {base, next}. next is a hint (null when suppressed or when its
settings read fails; the base is created either way, and repeating the call
mints a second slug, <slug>_2).
get_autofill_profileA

Read the user's application-form autofill data: profile (personal contact + address, work-authorization answers (work_auth: the job's country's, none without one; work_auth_by_country: per market key, e.g. UK; a question naming a country takes its), preferences like salary/notice/relocation, education, languages — each a language with read/speak/write level Basic/Intermediate/Fluent and separate native and fluent booleans — custom Q&A presets), plus employment blocks and skills from the selected resume when application_id or base is given. Response is labeled source: "profile" and includes canonical_identity (legal_first, legal_last, preferred) mapped only from stored autofill personal fields; it is the source of truth for form identity fields and takes precedence over ATS-parsed values. It is the feed the Chrome extension's deterministic fill uses. EEO/demographic answer values (profile.eeo) are returned ONLY when Profile standing consent is active (eeo_consent.enabled) and are stripped otherwise; when returned they are the user's stored answers, intended for Playwright/agent fill verbatim with no inference or model-authored values. eeo.veteran_status is no, yes or decline: one question, "Are you a veteran?" (a no also answers "not a protected veteran"; a yes alone never says which protected class applies, so leave a protected-class list to the user). eeo.gender is male, female, non_binary, self_describe or decline; with self_describe, eeo.gender_self_describe is the user's own words for a form's self-describe box. A form lacking a matching option is left to the user. Missing EEO values are completed by the user in Profile or in the browser; consented answers are not pasted or re-dictated through chat. A value absent here (a county) is not in the profile and is not guessable. WOTC, signatures and terms are human-only.

set_autofill_profileA

Save the user's application-form answers: the profile get_autofill_profile reads and the Companion fills forms from. Only the sections passed change. Inside personal, eligibility, preferences and each country's work-permit answers only the keys passed change, and a null removes one; education and languages replace the stored list whole (get_autofill_profile returns them). Each custom answer replaces the saved one with the same question (case and spacing ignored) and the same only_for and only_for_kind, or is added: for an answer held for one company or city, send its only_for and only_for_kind as get_autofill_profile shows them, or a second scoped answer is added. A saved answer is never removed here, so one the Companion learned meanwhile stays (removing one is the user's, in Profile). A work-permit answer is per country and a form question without one is asked on that form, never defaulted; setup counts the answers of each country in apply_in (set_markets). Diversity (EEO) answers have no field here: they and their consent are the user's, in Profile. An unknown section, key or market key writes nothing. Returns {profile, setup}: the saved profile without eeo, and setup status's form-answer readiness (readiness 0 to 1, groups, blocking, work_auth_by_country counts), null when that read fails.

set_job_preferencesA

Save the jobs the user wants, which get_job_search_brief reads and setup counts as done once any is set. Only the fields passed change; [] clears a list and "" clears notes. favored_roles holds at most 25 roles, duplicates dropped. A role the catalog does not know (the refusal names the closest), a currency that is not ISO 4217 or more than 25 roles is a 422 that saves nothing. Returns the stored preferences, with role_categories derived from favored_roles.

set_marketsA

Save where the user lives and applies. home (a market key, "UK" not "GB") sets the date format and, while home_currency is unset, the currency pay is shown in. apply_in is every market the user applies in, each asking its own work-permit answers; [] reads as home alone, and a market dropped from it keeps its stored answers. home alone leaves apply_in as stored. home_currency is ISO 4217; "" follows home again. Only the fields passed change, all in one save: a refused field (an unknown currency) writes none of them. No field passed reads the current values. Returns {home, apply_in, home_currency (the resolved one), setup}: setup is setup status's form-answer readiness, null when that read fails.

set_personaA

Replace the user's persona: their direction, strengths and working style in their own words. Every writing prompt (tailoring, application answers, cover letters) reads it for tone and emphasis, never as a source of facts. The whole text is replaced; "" clears it, and setup counts the step done while it is not empty. The app's own persona draft needs Maestro's AI key and has no MCP tool. Returns {key, value}.

find_job_by_urlA

Answer "is this posting already captured?", so an extraction can be skipped: posting-equality source_url lookup returning {found, job, application_exists, application_id}. Matching ignores query strings and fragments (?utm_*, ?gh_src=, #apply) and treats the posting's own sub-paths (/apply, /application) as the same posting, so tracking parameters need not be stripped from the input. A search-results or employer-index URL still matches nothing. found=False means the posting is new; store_extracted_jd still dedupes authoritatively via already_existed.

update_base_resumeA

Replace a base resume's data (the full ResumeData object, required). Optionally also update display_name. PUT replaces the whole resume body.

set_base_resume_identityA

Set what a base resume is written for. Only the fields passed change. countries (ISO codes) restricts which jobs the resume is scored and recommended for: a job in another country skips it, and a resume sent to one is flagged. role_category/role_label name the role, company and focus are free-text emphasis hints for tailoring (at most 80 characters each), and display_name renames the resume. [] clears countries and "" clears company or focus. color_hue (0-359) sets the colour the app shows beside the resume's name; it never changes the PDF. An unknown country, an over-long value or a hue outside 0-359 is a 422 that saves none of the fields. Resume content, versions and the PDF are never touched.

edit_base_resumeA

Apply typed edit ops to a base resume. replace_entry changes a field on one entry (dates, title, company, project name, link, tech) given that entry's index and the full edited entry; update_base_resume instead replaces the whole resume body (PUT). ExtraSection shape is on ops (ATS-neutral).

Indices are 0-based into the FULL array (incl. enabled:false), as returned by get_base_resume, never PDF order. Response applied echoes each op. Kinds:

  • {"kind":"replace_summary","value":str|null,"expected_content_hash"?:str}

  • {"kind":"toggle_entry","section":"experience"|"projects","index":int,"enabled":bool}

  • {"kind":"replace_bullet","section":,"index":int,"bullet_index":int,"value":str,"expected_content_hash"?:str}

  • {"kind":"replace_skills_group","category":str,"items":[str]} (case-insensitive)

  • {"kind":"add_skill_item","category":str,"item":str}

  • {"kind":"add_bullet","section":"experience"|"projects","index":int,"text":str}

  • {"kind":"add_entry","section":,"value":} (appends)

  • {"kind":"replace_entry","section":,"index":int,"value":} (one indexed entry only)

  • {"kind":"remove_entry","section":,"index":int}

  • {"kind":"remove_bullet","section":,"index":int,"bullet_index":int}

  • {"kind":"replace_contact","value":}

  • {"kind":"replace_certifications","items":[str]}

  • {"kind":"add_extra_section","value":}

  • {"kind":"replace_extra_section","section_key":str,"value":} (key may not change)

  • {"kind":"remove_extra_section","section_key":str}

  • {"kind":"move_extra_section","section_key":str,"to_index":int} =experience|projects|education (toggle_entry/add_bullet: experience|projects). section_key is ExtraSection.key. Bad index/category/key → error. Re-renders PDF. Every response carries render_error: non-null means the automatic re-render failed and the existing PDF is stale.

create_base_resumeC

Create a new base resume. Slug must be lowercase alphanumeric/underscores, not 'master'.

duplicate_base_resumeC

Clone an existing base resume into a new slug.

list_resume_versionsA

List append-only resume snapshots for one target, newest last.

kind is "base" (key = base-resume slug) or "application" (key = application
id). This is NOT render_pdf's target_type vocabulary — there is no
"base_resume" kind here. Each row is a version summary (number, label,
created_at); use get_resume_version for the snapshot and diff.
get_resume_versionA

Fetch one resume version including its snapshot and the diff vs its parent.

kind/key as on list_resume_versions: "base"+slug or "application"+id.
number is the version number from that list.
restore_resume_versionA

Copy a past version's snapshot into the live resume.

Restore goes through the standard versioned write path — it is itself a
new version (history is append-only; nothing is deleted). kind/key as on
list_resume_versions: "base"+slug or "application"+id. For applications,
the old PDF is cleared; render_pdf produces a new one. A "base" restore
re-renders, so the response carries render_note (see render_pdf: non-null
only when a TeX-less backend rendered a LaTeX template through a Typst
one) and, if that render failed, render_error — the restore landed either
way, only the PDF is stale.
archive_base_resumeA

Hide a base resume from pickers without deleting it.

Sets archived_at (reversible timestamp, not a delete). Archived bases drop
out of list_base_resumes' default view; JSON, PDF, TEX and version history
stay. Undo with unarchive_base_resume.
unarchive_base_resumeA

Restore an archived base resume to list_base_resumes' default view.

Clears archived_at. The resume is again selectable; nothing else changes.
edit_applicationA

INCREMENTAL FIX: apply typed edit ops on top of an existing application's CURRENT tailored resume (customized_json), preserving every prior edit.

Falls back to the base resume only when the application has no customized_json
yet (nothing to preserve). Suited to refining or retrying a tailored resume
after inspecting PDF previews — swap a bullet, tweak wording, fix one thing —
without losing the rest of what's already been tailored and without creating
duplicate applications.
Accepts the FULL op vocabulary documented on edit_base_resume — every
kind. replace_entry changes an entry's dates, title, company, project name
or tech list; add/replace/remove/move_extra_section handle the custom
Publications/Awards/Volunteer sections.

Op field names (these exact keys, no synonyms):
{"kind": "replace_bullet", "section": "experience", "index": 0,
 "bullet_index": 2, "value": "New bullet text"} — `kind` (not op/type),
`index` (not entry_index; 0-based into the FULL array incl. disabled rows),
`value` (not text).
Index is 0-based into the full customized_json array (including enabled:false),
as shown by get_application, not PDF ordinals. Response `applied[].name`
echoes the entry each op touched.
Clears pdf_path/tex_path (the old PDF is stale); render_pdf produces a new one.
update_applicationA

Update an application's tracking fields (status, applied_at, notes, referral_id).

Only the arguments passed are sent to the backend — the PATCH endpoint is
driven by which fields were actually set on the request, so an argument left
out leaves that field untouched. `clear` empties notes or unlinks the
referral (an explicit null on the request).

Allowed statuses: draft, applied, interviewing, offered, accepted, rejected,
withdrawn, skipped (anything else is rejected). `skipped` is the user's own
"not applying to this one". Setting status to applied, interviewing, offered
or accepted also closes any open proposal on the job, recorded as a declined
"applied manually" consent event; skipped closes it as a plain skip.

applied_at sync rule: if you change `status` without also passing `applied_at`,
the backend keeps applied_at in sync automatically — entering "applied" stamps
it with the current time (only if not already set), moving back to "draft"
clears it, and every other status (interviewing/offered/accepted/rejected/
withdrawn/skipped) preserves whatever applied_at already holds. An explicit
`applied_at` overrides that sync and must carry a UTC offset
(e.g. `2026-08-18T14:32:11+00:00`) — a naive value is a 422.

Every status change is logged with its date and who made it (this client's
name). Pass `from_email=True` when you read the change in the user's job
email (the mail-status automation): the log then says it came from mail.
save_referralA

Add a referral contact, or edit one when referral_id is given (list_referrals has the ids). A referral is a company the user can apply to with someone's referral: its careers page, the contact's name and notes. A new referral needs company and careers_url (an http or https address); contact_name and notes are optional, and each call without referral_id adds another referral. An edit changes only the fields passed, and company is optional there: a field left out keeps its saved value, and a field passed replaces it (referrals keep no history of earlier values). An unknown referral_id is a 404 and nothing is written. Returns the saved referral {id, company, careers_url, contact_name, notes, applications_count, created_at, updated_at}.

generate_qa_answersA

Maestro's AI writes a batch of screening-question answers for an application, with the user's API key (with none set it returns an error). questions is the complete list of questions to answer in this batch. Outreach/cold messages are asked as free-form questions here (e.g. "Write a short LinkedIn DM to the hiring manager") — answers are grounded in the resume and the application's linked referral contact when one exists. Refused (409) when the user's writer setting (get_setup_status) says the agent app writes; store_qa_answers stores answers the caller wrote, with no LLM call.

store_qa_answersA

Store screening-question answers the caller wrote for an application, with no LLM call (generate_qa_answers has the app's configured model write them). One entry per question, shown in the job's Apply step history and in list_qa_entries. A question the application already holds (the same words, case and spacing ignored) has its answer replaced and the others are added, so a repeated call changes nothing. Answers are stored as plain form text, as generated ones are: markdown bold, headings, bullet markers and backticks are removed, and en and em dashes become a hyphen in a range ("2020-2023") and a comma elsewhere, line breaks kept. The text is stored as the user's own words; the server does not verify it. An answer that is empty once its formatting is removed is a 400 that stores nothing. Returns the stored entries [{id, kind, prompt, answer, model_used, created_at}]; model_used names the agent.

generate_cover_letterA

Maestro's AI writes a cover letter for an application in the requested tone, with the user's API key (with none set it returns an error). This replaces the application's prior generated cover-letter entry. Refused (409) when the user's writer setting (get_setup_status) says the agent app writes; store_cover_letter stores a letter the caller wrote, with no LLM call.

store_cover_letterA

Store a cover letter the caller wrote for an application and render its PDF, with no LLM call (generate_cover_letter has the app's configured model write it). It replaces the application's cover letter and that letter's PDF (an application holds one); the new one shows on the job's Apply step and in list_qa_entries. The text is stored as the user's own letter; the server does not verify its facts. As with a generated letter, en and em dashes become a hyphen in a range ("2020-2023") and a comma elsewhere, and one opening a line ("— Jane") is dropped; the rest is stored as written, line breaks and paragraphs included, up to 20000 characters. The PDF puts the base resume's contact block above the letter, in the engine of the application's resume template.

Returns the stored entry {id, application_id, kind, answer, model_used,
pdf_path, render_note, render_error, created_at}; model_used names the
agent. render_error non-null means the letter is stored and its PDF could
not be made (pdf_path is null). render_note is non-null only when a TeX-less
backend rendered a LaTeX template through a Typst one.
render_pdfA

Render a PDF. target_type is 'base_resume' (target_id=slug) or 'application' (target_id=application id). Optional template_id selects a template (either engine; must be 'ready'; omit for the default). Response reports resolved_template_id / resolved_engine (the template actually used); template_fallback=true only when an explicit template_id was passed and the resolved id differs — a substitution of the persisted choice is reported by render_note, never by the flag. render_note is non-null only when the backend has no TeX and a LaTeX template was rendered through a Typst template instead; it names both. Returns the render result incl. pdf_path (an application render also has copy_path: where the user finds the PDF in their resumes folder, null when off), plus a next key: for an 'application' render this is the terminal apply-readiness hint (autofill_ready, incomplete/blocking groups, a conditional browser-handoff offer); for a 'base_resume' render it is always null — a base-resume preview isn't part of the score->tailor->render arc, so there is nothing to offer.

Without keep, template_id is a one-off and the saved design is unchanged.
With keep=true the template_id and/or formatting are saved on the resume
first, its later renders use them, and the response adds kept
{template_id, formatting}: what the resume now keeps.
get_rendered_pdfA

Save a rendered PDF locally and render one PNG per page without inlining images. target_type 'base_resume'(slug)/'application'(id) returns the last rendered resume PDF; 'template'(id) returns the sample preview PDF. Returns {filename, path, size_bytes, mime_type, page_count, page_images, em_dash_found, em_dash_pages, artifact_dir?}. Requires an existing render (render_pdf, or validate_template for a template preview). Paths are local to where the backend/MCP server run; browser uploads use the copy staged by prepare_application_pdf_upload. The PDF and per-page PNGs ({id}.pN.png, ~120 dpi) are written under $MAESTRO_CS_PDF_DIR (or a temp dir). This slim response never includes image base64; get_rendered_pdf_page_image returns the visual bytes of exactly one page. em_dash_found reports whether the PDF contains an em dash (U+2014), which rendered resumes in this product are required not to contain; em_dash_pages lists the page numbers. (page_count is None / page_images empty if the PDF can't be read.)

get_rendered_pdf_page_imageA

Return one rendered PDF page as an explicit opt-in visual payload. page_number is 1-based. max_dimension_px (default 1024) is the longest side of the re-rendered PNG; larger values grow the payload, and encoded payloads over ~1MB are rejected (a smaller max_dimension_px fits). Returns target_type, target_id, page_number, page_count, filename/path/ size/mime metadata, and page_image_b64 for only the requested PNG. Out-of-range page numbers are rejected. Does not change get_rendered_pdf's pre-rendered preview PNGs.

prepare_application_pdf_uploadA

Stage a disposable copy of an application's canonical rendered PDF for direct Playwright upload: its tailored resume by default, or its cover letter (document). If the PDF has not been rendered, render it and retry once. Writes atomically under $MAESTRO_CS_UPLOAD_DIR/<application_id>/, defaulting to the repository's .playwright-mcp/uploads directory (pair Playwright --output-dir with the parent .playwright-mcp tree). Returns upload_path, an absolute path usable directly by Playwright's file chooser. The staged copy is the only location intended for browser file uploads; the canonical server artifact (under applications/artifact_dir) is preserved and is not an upload source. Also returns filename/path integrity and PDF lint metadata. This preparation tool does not authorize or submit an application.

list_templatesA

List resume templates — both engines (id, display_name, status['draft'|'ready'], engine, is_default, last_error). parse_certified false = a 'ready' template that still loses word boundaries under strict extraction — see validate_template. engine_available=false marks a LaTeX template on a backend with no TeX: still pickable, renders through Typst with a render_note.

get_templateA

Get a template incl. its full source (Jinja2+LaTeX for engine='latex'; raw .typ text for engine='typst') plus engine, engine_available (see list_templates), and supported_fmt_keys. A template of either engine opts into look knobs by referencing fmt.* (LaTeX via the Jinja namespace, Typst via sys.inputs.fmt; defaults reproduce Classic): font_size (11), side_margins (0.4in), top_bottom_margin (0.3in), line_spacing (1.0), section_spacing (6pt), entry_spacing (0pt), justify (false), hide_divider (false), bullet_icon ("bullet"|"dash"), header_align ("center"|"left"|"right"), skills_layout ("inline"|"bulleted"), education_order ("degree_first"|"institution_first"), date_format ("verbatim"; feeds |format_date). For engine='typst', supported_fmt_keys always includes date_format (applied server-side) even if the source never names it.

create_template_draftA

Create a DRAFT template. engine defaults to 'latex'. Pass engine='typst' for Typst — then source is REQUIRED: raw .typ text (no Jinja, no escape filters; read via json(bytes(sys.inputs.resume)) / json(bytes(sys.inputs.fmt)); engine is immutable after creation).

Both engines consume the same resume JSON (contact, summary,
skills[].{category,items}, experience/projects/education, certifications,
extra_sections). Typst reads r.contact.name etc.; optional fields arrive
as `none` — guard with `!= none`. LaTeX uses resume.* with |latex_escape
on every value.

Typst constraints: dates arrive PRE-FORMATTED server-side (fmt.date_format
is not read). Package imports (`#import "@preview/..."` or any `@...`
import) are REJECTED (no external packages). Fonts: only
vendored XCharter plus typst's embedded defaults (e.g. Libertinus Serif);
any other family compiles WITHOUT error and silently substitutes. A template
that does not render r.extra_sections (both `entries` and `bullets` shapes)
hard-fails on resumes with custom sections; get_template('typst-classic')
shows the block.

LaTeX: omit `source` to start from the canonical starter. Given source
must be a complete compilable document (\documentclass + preamble) as
Jinja2 with blocks ((* *)), variables ((( ))), comments ((# #)). The source
is raw LaTeX: bare `&` for tabular cells, `\&` for a literal ampersand;
HTML entities such as `&amp;` are not decoded. Includable:
((* include '_header.tex.j2' *)). Spacing and look can be wired to `fmt.*`
(full knob list on get_template) instead of hard-coded values;
get_template('default') is a starting point.

validate=True test-compiles in the same call (status/last_error). A LaTeX
draft cannot validate on a backend with no TeX (list_templates →
engine_available); engine='typst' works there. Not usable until validation
succeeds. `id` is a slug: [a-z0-9_-].
update_template_draftA

Edit a draft template; changing the source resets it to 'draft'. Pass validate=True to save + test-compile in one call (returned status/last_error show the result); otherwise re-validate separately. The active default template cannot be edited through these tools (the backend rejects it).

validate_templateA

Test-compile a template against a built-in sample resume. On success it becomes 'ready' (usable via render_pdf template_id) and a preview is available via get_rendered_pdf('template', id). On failure returns {ok: false, error} with the engine's compile error (Typst errors carry file:line:col). Returns {ok, error, parse_certified} — parse_certified is true when the template's compiled PDF round-trips through a strict text extractor with word boundaries intact, false when it drops text, null when not assessed. Also returns parse_report: {missing, headers_missing, extra_sections_supported, extra_sections_missing} — missing lists the exact sample phrases the PDF dropped; extra_sections_missing non-empty means the template will hard-fail rendering any resume that has custom sections.

explore_trendsA

The saved jobs and their match scores as one chart's rows, one view per call: top_skills, heatmap, role_mix, fit_distribution, ats_over_time or tailoring_lift (what each returns is on view). role_category, level and employment_type filter the jobs for every view but fit_distribution; limit (1-100) caps the skills top_skills and heatmap return. A filter or limit the view does not take is refused, and nothing is read. Skills that recur as gaps are explore_gap_frequency.

explore_gap_frequencyA

Skills that recur as gaps across saved JDs (best-scoring base resume vs JD, before tailoring), ranked by how many jobs want them and their potential match score gain. This is RECURRING DEMAND, not a "you lack this" or learn-next list: a skill can recur because the evidence lives on a different base resume, is stale, or is merely mis-placed on this one. The learn-vs-move distinction is the per-skill tier on build_areas (build = no resume evidence AND nothing in the Career KB, the only genuinely learn-it rows; surface = the material exists, port or strengthen it) — get_job_search_brief carries build_areas, or GET /api/explore/build-areas. Hygiene mirror_wording gaps are excluded here: the resume already matches those at full keyword credit, so the literal JD token moves no score and is not demand. Each row also carries category_label, the plain-words form of category (e.g. "Add an example in a job or project"); the raw keys (dual_place, missing_skills) and the build_areas tier values are internal identifiers, and category_label / the tier word is their user-facing wording. Optionally filtered by role/level/type; limit caps the number of skills returned.

list_applicationsA

List your applications as a thin paginated summary array. Filter by status/role_category; page with limit/offset. Rows name their base resume (base_resume_name) and the stored score (ats_score, null when unscored). Use get_application(id) for the full record and compare_ats for scores.

score_atsA

Deterministic hybrid match score (no LLM): deterministic lexical layers + anchored pinned-model semantic matching + section-level semantic_fit. Omit target to score ALL base resumes (fast).

target_type "base_resume" (target_id = slug) or "application" (target_id = id).
Same inputs always give the same score; use it to compare bases and to
verify an edit actually moved the number.

Response is {scores, recommendation, countries, next}. `scores` is the raw per-target
list: composite (0-100), per-layer subscores (incl. semantic_fit), gate
warnings, and a per-skill diagnostic table with fix_hints. `recommendation`
is a deterministic ranking of the base-resume rows (order, recommended slug,
margin, close_call, reasons, coverage_warning) — always present, cheap, and
useful during triage even with hints off. Bases set for other countries are
not scored for this job unless `include_other_countries` is true; the
`countries` block ({job_country, fallback, skipped}) names them and is
omitted when a target_id is given. `next` is an optional next-step
hint (null when suppressed or unavailable): in a set with the tailoring tools,
a gap analysis of the recommended base, filled from the saved presets or
walked by hand.

brief=True suits scoring many jobs in a triage/mass-capture loop: it
suppresses ONLY `next` (never `recommendation`) and is checked before any
settings lookup, so a twenty-posting triage loop pays no extra HTTP
round-trip for a hint nobody will read.
compare_atsA

Before/after match score comparison for an application: composite and per-layer deltas plus a per-skill diff (absent→matched, skills-list-only→dual, decayed→recent). Computes any missing phase row on demand.

create_tailoring_sessionA

Start the gap-analysis tailoring workflow for a job + base resume.

Calling again for the same job+base SUPERSEDES the open session (saved
resolutions become unreachable); list_tailoring_sessions shows existing
sessions. A failing fatal health gate on the base is a 409.

Scores the base (deterministic ATS engine, persisted as the 'before' score) and
returns a session whose gaps_json.categories list every gap in fix-cost order:
missing skills, wording mismatches, placement upgrades, stale evidence, adjacent
skills, title/structure. The intended flow is: gather the user's answer for each
gap, save them with resolve_gaps, then tailor_session. Each gap carries its
allowed actions. A missing skill MAY use add_keyword ONLY to add the keyword to
a skills category (placement_target.section="skills"): that inserts an
unverified skills-list item, never a fabricated bullet. The evidence-backed
alternatives are a user_input resolution (the user's answer to
enrichment.elicitation_question), attach_project, or skip.

use_presets=True also fills every gap the user's Quick tailor presets decide
(get_tailoring_options explains them), with no AI; undecided gaps are saved
as skip. When that fill fails, the error names the session it started.

enrich=True has Maestro's AI add display-only explanations to each gap (one
fast-model call with the user's key; no score, gap or career-history match
changes). Refused (409) when the user's writer setting (get_setup_status) says
the agent app writes: run it without enrich, the default, which calls no model.

Response is the session (filled, with use_presets) plus a `next` hint (null
when hints are off or unavailable). `base_anchors` are the base's emphasis
hints (countries, role, company, focus), not evidence; the anchor company is
the resume's own target, not necessarily this job's employer.
list_tailoring_sessionsA

List all tailoring sessions for a job, newest first.

A lost session id can be rediscovered here (create_tailoring_session would
supersede the open one). Each item is a summary (id, job_id, base_resume,
status, application_id, timestamps, gap_count, resolution_count); the gap
list and saved resolutions are on get_tailoring_session. Status is one of:
open (in-progress gap walkthrough, resumable via get_tailoring_session),
tailored (finished — tailor_session already ran), superseded (a newer
session for the same job+base replaced it), or abandoned (explicitly closed
via close_tailoring_session without tailoring).
get_tailoring_sessionA

Fetch a tailoring session to resume a half-done gap walkthrough.

`tailoring_session_id` is the id returned by create_tailoring_session. Returns
the session with its frozen gap list (gaps_json), any resolutions saved so far
(resolutions_json), and status (open/tailored). resolutions_json may already
contain SYSTEM-PLANNED auto-resolutions stamped at creation (their payload
carries `provenance`, e.g. library_auto/kb_auto/kb_profile/wording_auto —
hand-made resolutions never do): verified evidence the resolver pre-applied.
To retract one, resend its gap_id with action "skip" via resolve_gaps.
Unresolved gaps are those in gaps_json with no entry in resolutions_json;
tailor_session consumes the saved resolutions. `base_anchors` are the base's
emphasis hints (countries, role, company, focus), not evidence; the anchor
company is the resume's own target, not necessarily this job's employer.
close_tailoring_sessionA

Abandon an OPEN tailoring session without tailoring it.

`tailoring_session_id` is the id returned by create_tailoring_session. This is
the explicit exit for a "changed my mind" flow — e.g. the user walks through
some gaps, then decides to use the base resume as-is and does not run
tailor_session. Marks the session's status "abandoned" so it stops looking like
unfinished work. Errors (409) if the session isn't currently open (already
tailored, already superseded, or already abandoned).
resolve_gapsA

Save gap resolutions on a tailoring session (merge by gap_id, idempotent). The batch is validated as ONE unit — one invalid item saves NOTHING (existing saved resolutions untouched).

`tailoring_session_id` is the id from create_tailoring_session. Each item
is {gap_id, action, payload}. Actions: add_keyword, user_input,
attach_project, skip, enable_entry, port_kb_point, cannot_confirm.
Payload shapes live on the `resolutions` parameter schema.

enable_entry and port_kb_point carry verified evidence and are exempt
from the add_keyword missing-skill rule (unverified additions stay in
skills). Re-sending a gap_id overwrites that gap; other saved resolutions
stay — including system-planned autos (payload.provenance; see
get_tailoring_session). May be called repeatedly; tailor_session consumes
the saved resolutions. Merge-only: omitting a gap_id never removes its saved resolution — resend
that gap_id with action "skip" to retract.

Response is the session plus a `next` hint (null when hints are off or unavailable).
tailor_sessionA

Run the tailor pipeline for a session: saved resolutions -> edit ops -> the job's tailored application, scored and rendered.

Three ways to produce the edits:
- ops omitted: Maestro's AI writes them, only when the user's writer setting
  (get_setup_status) is Maestro's AI; otherwise refused (409).
- ops: the caller's own typed edit ops, the shapes edit_application takes
  (its ops schema lists each kind), carrying only the changed fields; no
  model call.
- no_new_text=True: no new wording and no model call, under either setting.
The ops and no_new_text parameters say which saved answers the server
applies; answers_left_to_ops lists the ones that need wording.

Caller-supplied ops are applied as authored: the server validates structure
only, so nothing gates fabricated skills, experience, metrics, or dates. A
skill the candidate cannot evidence goes only in the skills list
(add_skill_item), not in a bullet; the compare view audits any edit beyond
the saved resolutions. Resume text is implied first person: an edited line
in I/he/she voice or naming the owner is a 400 naming the lines.

Optional user_prompt adds free-form tailoring guidance. Writes the job's ONE
application from the base plus the edits: it rebuilds the draft (version
history keeps the old draft) and renders its PDF; pdf_ready is false when
that render failed. compare (before/after match score deltas) may be null
with compare_error set: the tailor still succeeded. unbacked_skills names
missing skills this tailor put on the resume that career history does not
back. kb_writeback_skips derives from each user_input resolution's STORED
placement_target from resolve_gaps, not from the ops, so ops that disagree
with the stored target produce a spurious wrong_section skip. `next` (null
when off or unavailable): render_pdf if pdf_ready is false, else apply
readiness; no_new_text's answers left come first.
export_jobsA

Export jobs, newest first, with extracted fields and nested skills, for open-ended analysis. Paged by limit (default 10, max 50) and offset; a page shorter than limit is the last. Rows run a few thousand characters (the skills list is most of it) and omit raw_text and extracted_json, which get_job returns for one posting. since is an ISO date (YYYY-MM-DD); skill is a case-insensitive substring.

propose_applicationA

File an agent-hunted application proposal for user review.

If an open proposal already exists for the job, returns that proposal (idempotent)
instead of 409. Pass application_id after tailoring to late-link when the open
proposal is still unlinked (409 if already linked to a different application).
Refused (409) when the company is blocklisted or when THIS posting was
previously declined (declines are posting-scoped and permanent; other roles
at the same company are unaffected).
plan may include "company_note" (1-2 sentences: what the company is/does,
direct employer vs staffing, any legitimacy doubt) — the proposals page
shows it as "About the company" for triage.
list_proposalsA

List application proposals, newest first, as {items, total}. status is one status or a comma-separated set; limit (default 20, max 50) and offset page through total. A row is a couple of thousand characters (fit and job).

status 'accepted' means the user queued the proposal for an apply run (via
the /proposals page or record_triage); 'pending_review' proposals are awaiting
the user's triage. Batch apply runs work 'accepted' proposals by convention
(the agent-apply playbook); the server also permits preparation, evidence and
consent from 'pending_review'. Each item's proposed_by names who filed it: an
MCP client's name, "you" for the web app's queue, or null. Reading expires
overdue pending_review and needs_decision proposals (terminal 'expired').
get_proposalB

Get proposal detail including job facts, application summary, QA entries, and evidence manifest. Reading expires an overdue pending_review or needs_decision proposal (terminal 'expired').

record_decisionA

Record user decision when proposal is in needs_decision state. Resolves back to pending_review. application_id may be omitted; the backend then links the job's application on fit.chosen_base when there is one, else the job's one application (409 only when the job has no application); an automatic link sets fit.chosen_base to the linked application's resume.

request_decisionB

Escalate an unlinked/ambiguous proposal to needs_decision. Idempotent if already needs_decision.

resume_proposalA

Return a needs_human proposal to pending_review so preparation can continue. Releases any pre-click daily-cap reservation. Refused for submission_uncertain.

get_final_reviewA

Compact final-review bundle: job summary, chosen base/match score delta, PDF readiness, QA answers, blocked/manual items, evidence manifest. approval_recorded is the user's yes on this proposal; eeo.consent_recorded is their standing EEO consent (no values). Requires an existing proposal — a 404 usually means propose_application was not called (it accepts application_id to late-link) or an application id was passed where a proposal id belongs. duplicate_submitted=true means a same-company+title proposal was already submitted (relevant to the user's approval decision). flags lists the job's recorded form answers worth a second look (record_filled_answers): question, source, answer (an EEO one carries eeo_answered instead) and each flag's reason. base_country.eligible is false when the resume being sent is set for other countries than the job's.

record_filled_answersA

Record what was filled into this job's application form, one call per form page: each field's question, answer and source. Per question the latest call wins; the job page's What was submitted tab shows the record. An EEO answer is kept only while the user's standing EEO consent is recorded, and no MCP read returns its value. Returns {id, application_id, flag_count, flags}: each flagged field by its index in fields, with guessed_screening (a screening question answered by inference or composed prose), ticked_everything (a multi-select with every one of 3+ options ticked), differs_from_profile or eeo_without_saved_answer, each with a one-line reason. Flags warn; nothing is blocked, and get_final_review lists the job's flags again. step names the page: pass its URL path (location.pathname), the Companion's own key, so an agent row and a Companion row for one page fold together; a page number is still accepted. application_id links the record now; without it the record links, for the application of base_resume (the base resume's slug), when that application is created, marked applied or submitted.

record_runA

Stores one finished automation run. The Automations page lists the newest run of each automation under Recent runs (its counts, digest and job links) and each card shows when its automation last ran; the Agent inbox says so when the run proposed jobs. automation is the card id (mail-status, job-hunt, referral-pages, tailor-run, apply-session) or a custom automation's own name, 40 characters at most. outcome: ok, partial (some of the work failed) or failed (none of it was done). The newest 200 runs are kept. Returns the stored run.

record_consentA

Record the user's explicit approve/reject decision for this proposal as an append-only audit row (channel = where the user said it, note = their words). The stored decision is attributed to the user; the server cannot verify it came from them. approved is accepted only when the proposal has final_review evidence, the linked application is not already applied, and a daily-cap slot is free, and it reserves that slot. rejected is a posting-scoped decline and is terminal. auto is the agent's own yes in full automation mode: accepted only for approved and only while full automation is on.

get_job_site_loginA

The email and password the user saved for job-site accounts, for creating an account or signing in on this proposal's application site. Available only while full automation is on and only for a queued or approved proposal; each call is recorded (never the value). The value passes through the agent's AI provider, which is why it is a password the user keeps for job sites alone. Returns {email, password}.

attach_evidenceA

Attach an evidence image to a proposal, from exactly one of image_base64 (the image, base64, supplied inline in the call) or file_path (an image file already on this machine's disk, e.g. a browser tool's saved screenshot; its bytes are read and uploaded server-side, so they never enter the model context). Both or neither is refused, and nothing is uploaded. kind must be step | final_review | submission_receipt.

mark_submittedA

Flip an approved proposal to submitted (terminal; links the application to applied). Requires submission_receipt evidence, or user_attested=True, which records the user's own statement that they submitted it (or that a submission_uncertain proposal went through, e.g. a confirmation email or portal check) as an attested consent event with their words in note. user_attested substitutes for receipt evidence and the server does not verify it; it is the user's attestation, not the caller's. In full automation mode, channel auto records the agent's own word that the application went through, with note naming what confirmed it (the confirmation page or a confirmation email); no receipt is needed, and it is accepted only while full automation is on.

record_triageA

Record the user's TRIAGE decision over one or many proposals — accept queues them for the next apply run; decline drops those unique postings (posting-scoped, never the company). The decision is the user's own, stated directly or as a selection rule they gave. One append-only consent row is written per proposal and the result is a per-id ok/error report (a mixed batch partially succeeds). Accept is NOT submit consent: the per-application approval is recorded separately by record_consent.

report_failureA

Report execution failure. From pending_review/accepted/approved, transitions to needs_human (resumable). reason='submission_uncertain' (for a submit click that could not be verified) moves the proposal to the terminal submission_uncertain status: resume_proposal is refused for it. The user's attestation, or the agent's (channel auto) in full automation mode, through mark_submitted moves it on.

Prompts

Interactive templates invoked by user choice

NameDescription

No prompts

Resources

Contextual data attached and managed by the client

NameDescription

No resources

TDQS

B3.4/5.0

Scored across 94 tools

Disambiguation2/5

The set contains several overlapping clusters: base-resume editing (edit_base_resume/update_base_resume/restore_resume_version), application editing (edit_application/tailor_session ops), proposal state transitions (record_triage/record_decision/request_decision/record_consent/resume_proposal), and KB ingestion (kb_capture/kb_add_points/kb_ingest_resume/kb_sync_base). Individual descriptions are detailed, but at 94 tools an agent must reason through many subtle boundaries and may misselect.

Naming Consistency4/5

Nearly all tools use snake_case verb_noun phrasing, often with clear domain prefixes such as kb_ and get_/set_/list_/record_. Minor deviations exist around the kb_ namespace and some state-machine verbs, but the conventions remain predictable.

Tool Count1/5

94 tools is an extreme count for any MCP server, even one covering a broad career workflow. The surface should be consolidated into fewer higher-level tools or split into separate servers.

Completeness4/5

The surface covers a wide lifecycle: jobs, applications, proposals, tailoring, KB, templates, rendering, health checks, referrals, autofill, and export. There are minor gaps such as explicit deletion/update paths for jobs and referrals, but core workflows are well represented.

Maintenance

ActivityActive
ResponsivenessNo recent community issues