| list_base_resumesA | List all base resumes (slug, display name, summary). |
| 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 the generated screening answers and cover letters for an application, newest first. |
| 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} 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.
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). |
| 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, employment_type, work_mode, city/state/country,
location_raw, salary_min/max, salary_period,
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.
|
| 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. 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 full composed career context from the Career KB: resume (the
approved-points resume view assembled from profile + non-archived entities)
and memory (the beyond-the-resume string: identity facts and entity notes
that don't belong on a resume). Intended as grounding for writing about the
user's career (LinkedIn posts, outreach drafts), optionally with
get_base_resume for a specific resume's voice. resume carries approved
points only; memory also includes the profile summary and notes and the
non-archived entities' notes. 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. It carries no IDs: kb_list_entities / kb_get_entity / kb_list_points
return the IDs needed to correct anything. |
| get_career_exportA | Read the exact portable career.md generated from the current Career KB. Returns deterministic Markdown for copy-out, download-equivalent use, or
Markdown-oriented grounding. For structured resume and memory fields, use
get_career_context. Read-only; this does not alter tailoring behavior.
|
| 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)
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_captureA | Capture free-text career news into DRAFT points. A model matches it to an
existing entity or proposes a new one (this call uses the configured LLM
provider); pass entity_id to force placement. 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_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. |
| 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-set Career KB point state to approved or retired. Approved points are
the only ones that appear on composed resumes and the career export; this is
the single approval path available over MCP. 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, as with record_consent). state is approved|retired only; bulk-to-draft is impossible. 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_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. Never edits
resumes. Safe to rerun — a second call is a no-op when already in sync.
|
| 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, 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 422 "no resume content" means the selected entities have no
approved points.
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, typed work-authorization answers, 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. This is the
same feed the Chrome extension's deterministic fill uses, so it supplies
real stored values for ATS forms. 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 (e.g. an unstored county) is not in the profile and is not guessable.
WOTC, signatures and terms are human-only. |
| 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.
|
| tailor_applicationA | REBUILD FROM SCRATCH: apply typed edit ops to the BASE resume and store the
result as customized_json, REPLACING whatever was there before wholesale. `ops` carry only the changed fields (same op shapes as edit_base_resume; the
base is in get_base_resume) — the server starts from the stored base and
inherits every untouched field from IT, not from any prior tailored draft.
If an application already has tailored edits (from a previous tailor_application
or edit_application call), calling this again DESTROYS them — the new result is
computed fresh from the base, not layered on top. edit_application is the
in-place alternative that preserves the current tailored draft (refining
wording, swapping a bullet, retrying after a PDF preview).
Does NOT trigger AI suggestions. A job has one application: with no application_id,
the job's newest is updated in place and takes this base (still replaced wholesale);
pass application_id to target a specific application explicitly. `ops` accepts
the FULL vocabulary documented on edit_base_resume — every kind, not just
the bullet/summary ones: replace_entry is how you change an entry's dates, title,
company or tech list, and add/replace/remove/move_extra_section handle custom
sections, which are ATS-neutral.
|
| 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. tailor_application is the from-scratch alternative:
it recomputes customized_json off the BASE resume and discards any tailored
edits already on the application.
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 omitting an
argument leaves that field untouched. This tool cannot clear a field back to
null/empty (e.g. remove notes or unlink a referral): omitting an arg skips it
entirely rather than sending an explicit null, and clearing a field requires
the web UI.
Allowed statuses: draft, applied, interviewing, offered, accepted, rejected,
withdrawn (anything else is rejected). Setting status to applied,
interviewing, offered or accepted also closes any open proposal on the job,
recorded as a declined "applied manually" consent event.
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 later stage (interviewing/offered/accepted/rejected/
withdrawn) 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.
|
| generate_qa_answersB | Generate a batch of screening-question answers for an application.
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. |
| generate_cover_letterB | Generate a cover letter for an application in the requested tone. This
replaces the application's prior generated cover-letter entry. |
| 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. |
| 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. 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 `&` 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_top_skillsB | Most in-demand skills across stored JDs, optionally filtered by role/level/type. |
| explore_skill_heatmapC | Skill demand by role category (pct of jobs per skill x role). |
| explore_role_mix_over_timeB | Counts of jobs by role category per week. |
| explore_fit_distributionB | How each base resume's match scores (deterministic engine) are
distributed across jobs. Each row's display_name is the resume's own name
(null for a slug with no row); the slug is an internal identifier. |
| 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. |
| explore_ats_over_timeB | Average match score over time (weekly), split by phase (base vs tailored)
and role category. Optionally filtered by role/level/type. |
| explore_tailoring_liftB | How much tailoring lifts the match score (base->tailored), averaged per role
category, plus an overall row. Optionally filtered by role/level/type. |
| 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) naming quick_tailor / create_tailoring_session
against the recommended base.
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.
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.
enrich=False (default) skips the backend's fast-model enrichment pass — no
in-house LLM call — so gaps carry only their deterministic fields (no
enrichment.elicitation_question / suggested_wording / suggested_placement).
KB coverage detection does NOT depend on it: the deterministic self-nomination
and evidence-verification pass that used to sit downstream of the LLM call now
runs unconditionally, so KB autos still get stamped with enrich=False, and
even during a provider outage. enrich=True pays for one fast-model call
adding those display-only explanations and elicitation questions per gap —
it never changes scores or which gaps exist.
Response is the session plus a `next` next-step 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.
|
| quick_tailorA | Start Quick Tailor over MCP: the fast path that fills gaps from the
user's saved quick-tailor profile instead of walking them one by one. Calling again for the same job+base SUPERSEDES the open session (saved
resolutions become unreachable); list_tailoring_sessions shows existing
sessions.
Composes create_tailoring_session(enrich=False) with the profile fill
(deterministic; resolve_gaps' honesty-gated save path).
This tool makes NO in-house LLM call: sessions rely on the calling agent's
own model. The saved profile only plans
mechanical, evidence-respecting moves — it does NOT write resume prose.
The returned resolutions_json may already resolve every gap, including
system-planned entries (payload.provenance). Resume text comes from
caller-authored typed edit ops passed to
tailor_session(tailoring_session_id=..., ops=[...]), which skips the backend
LLM pass.
Honesty: the server does not gate caller-authored ops against fabricated
skills, experience, metrics, or dates. An unverified or absent skill can only
enter a skills category (add_skill_item), never as an experience or project
bullet asserting the candidate did the work.
The tailor_session that follows writes the job's ONE application: it
rebuilds the draft and removes its PDF (version history keeps the old
draft).
A session whose resolutions_json contains no planned action beyond
"skip" means the profile was not ALLOWED to add anything to THIS
session — it is not an error; the gaps can then be resolved with
resolve_gaps.
Raises (409, message says which): a failing fatal health gate on the
base blocks session creation; a stale or no-longer-open session blocks
the profile fill. Response is the filled session plus a `next` hint
suggesting tailor_session (profile standing instruction as user_prompt
when the session has no note of its own).
|
| 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: resolutions -> edit ops ->
tailored application, auto-scored. `tailoring_session_id` is the id returned by create_tailoring_session.
Two ways to produce the edit ops:
- Default (ops omitted): the backend builds the tailor prompt from the saved
resolutions and calls the LLM to generate the ops.
- Caller-supplied (ops given): the caller has already produced the typed edit
ops (same op shapes as tailor_application/edit_application). The backend
skips its own LLM pass and applies them directly — the caller's judgment
IS the tailor. `ops` carry only the changed fields (the base is in
get_base_resume).
Caller-supplied ops are applied as authored: the server validates structure
only, so nothing gates fabricated skills, experience, metrics, or dates
(keyword-survival checks run only on the backend-LLM path). 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 must be implied first person: no I/he/she/they or the owner's name; a hit returns 400 naming the lines.
Optional user_prompt adds free-form tailoring guidance. Writes the job's ONE
application from the base plus the ops: it rebuilds the draft and removes its
PDF (version history keeps the old draft), scores it, and returns
{session, compare, compare_error, next}. compare (before/after match score
deltas) may be null with compare_error set: the tailor still succeeded, so do
not retry. `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` is a next-step hint naming render_pdf for that application
(null when suppressed or unavailable).
|
| 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, EEO consent flag (no values), blocked/manual items, evidence manifest.
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 step evidence image (base64) to a proposal. The image is supplied
inline in the call; when the screenshot already exists as a file on this
machine, attach_evidence_file uploads it by path without passing image bytes
through the model context.
kind must be step | final_review | submission_receipt. |
| attach_evidence_fileA | Attach evidence from an image file already on this machine's disk
(e.g. a browser tool's saved screenshot — Playwright MCP saves to its
--output-dir and reports the path). file_path is an absolute path to an
existing image, typically as reported by the browser tool — with the
standard setup that is under the repo's .playwright-mcp/ tree (e.g.
/Users//Projects/maestro-career-studio/.playwright-mcp/.png);
a relative or ~ path is resolved against this server's working directory.
When this server runs inside the backend
container, a host path with a .playwright-mcp/ segment is read from the
container's mount of that tree (paths that leave the tree are refused). The
bytes are read and uploaded server-side so they never enter the model context.
PNG/JPEG only, 5 MB max. 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. |