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
}
experimental
{}

Tools

Functions exposed to the LLM to take actions

NameDescription
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 `&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_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.

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 88 tools

Disambiguation3/5

The extensive descriptions do a lot of work distinguishing overlapping tools (tailor_application vs edit_application, update_base_resume vs edit_base_resume), but genuine confusion remains. The proposal state-machine tools (record_decision, request_decision, record_triage, record_consent, resume_proposal) and pairs like attach_evidence vs attach_evidence_file, score_ats vs compare_ats require careful reading to select correctly.

Naming Consistency4/5

Almost everything is snake_case with a verb_noun shape (list_jobs, create_base_resume, kb_edit_point, render_pdf), and prefixes cleanly partition domains (kb_*, explore_*). A few names drop the noun (quick_tailor, sync_now, process-free but verb-only) and ingestion verbs vary (kb_ingest_resume / store_extracted_jd / kb_capture), but the overall convention is predictable.

Tool Count2/5

88 tools is far beyond the 25+ heavy threshold and forces an agent to navigate a very large surface. The career-studio domain is genuinely broad (KB, resumes, tailoring, scoring, rendering, templates, job capture, proposals, evidence, automation), so most tools earn a place, but the set could plausibly be consolidated into a smaller, more navigable surface.

Completeness4/5

Coverage is unusually thorough: full CRUD on resumes, KB entities/points, templates, applications, proposals, plus scoring, health checks, rendering, evidence, and automation lifecycle. Minor documented gaps remain that force fallbacks to the web UI (cannot clear application fields to null, disputes/word bank web-only, certificate PDF upload), but the core workflows are complete.

Maintenance

ActivityActive
ResponsivenessUnresponsive