Skip to main content
Glama

Job Search Assistant — MCP Server

Tests

A local Model Context Protocol server that lets Claude read and update your job-search spreadsheets through natural conversation — "what's still awaiting a response after 2 weeks?", "add this posting to my tracker", "mark the Amazon role as interviewing", "add a Salary Range column and set it to 180-210k for the Amazon role", "what remote leads did I find this week?", "promote the Acme Corp lead to my tracker".

It manages a funnel — discover → apply → track — plus an interview-prep note:

File

Role

Env var

Job_Tracking.xlsx

Applications you've submitted (the tracker)

JOB_TRACKER_FILE

Job_Search_Discovery.xlsx

Leads you've found but not yet applied to (discovery)

JOB_DISCOVERY_FILE

Interview_Prep_QA.md

Rolling interview Q&A reference (prep)

JOB_INTERVIEW_PREP_FILE

Resumes\

Folder for generated resumes / cover letters, saved and printed from

JOB_RESUMES_DIR

resume_master.json

Stable resume facts that generate_resume tailors from

JOB_RESUME_MASTER_FILE

promote_to_tracker turns a discovery lead into a tracked application; save_document and print_document write generated docs to the Resumes\ folder and print them. An optional Gmail integration (five tools) searches your inbox for recruiter mail, scans for updates on tracked roles, and drafts/sends replies — see Gmail integration.

  • Runtime: Node.js + TypeScript — no Python or other external interpreter required

  • Transport: stdio (local process — no networking, no auth)

  • Data source: your .xlsx files, read/written with SheetJS (xlsx); styling (bold header, freeze/auto-filter, hyperlinks) via exceljs

  • Schema: dynamic — driven by each sheet's header row (custom columns supported)

  • Resumes: .docx built in-process with the docx package; .pdf/.docx text extraction via pdf-parse + mammoth

  • Board sweep: run_job_sweep probes ATS job boards directly over HTTP (native fetch), in-process — no external script

  • Printing: silent PDF printing — pdf-to-printer (bundles SumatraPDF) on Windows, CUPS's lp on macOS/Linux; .docx is converted to PDF first using LibreOffice (any OS) or Microsoft Word (Windows only) if installed (optional — see Safety & behavior notes)

  • Client: Claude Desktop or Claude Code (see config below)

Prerequisites

Setting this up on a brand-new Windows 11 machine, with nothing installed yet? WINDOWS_SETUP.md is a complete beginner-friendly walkthrough — Node, the project files, Claude Desktop, and troubleshooting, in order. The steps below are the terse reference version.

  • Node.js 18 or later — required. Download the LTS installer for your OS from nodejs.org and run it; verify with node --version.

  • LibreOffice — optional. Only needed if you want generate_resume to also produce a .pdf (it always produces the .docx regardless). Works on Windows, macOS, and Linux. On Windows only, Microsoft Word (if already installed) works as an alternative — nothing extra to download for that path.

  • Nothing else. No Python, no other interpreter, no compiler — every other dependency is pure JavaScript and installed by npm install below.

Related MCP server: career-agent

Build

cd mcp-job-tracker
npm install
npm run build

This produces dist/index.js. npm install pulls everything this server needs — pdf-to-printer (bundles SumatraPDF for silent Windows printing, no separate install; macOS/Linux print through CUPS's lp instead, no npm package needed), exceljs, docx, pdf-parse, mammoth — all pure JS, nothing to compile and no Python. The one optional external dependency is LibreOffice (any OS) or Microsoft Word (Windows only), used only to convert a generated resume's .docx to .pdf; if none is installed, generate_resume still produces the .docx and says so. The optional Gmail tools need a one-time npm run gmail:auth (see Gmail integration).

Configure Claude Desktop

Open Claude Desktop's config file:

Windows:  %APPDATA%\Claude\claude_desktop_config.json
macOS:    ~/Library/Application Support/Claude/claude_desktop_config.json

Add a job-tracker server entry (create the file / mcpServers object if needed).

Windows:

{
  "mcpServers": {
    "job-tracker": {
      "command": "node",
      "args": ["C:\\Users\\developer\\mcp-job-tracker\\dist\\index.js"],
      "env": {
        "JOB_TRACKER_FILE": "C:\\Users\\developer\\Documents\\Job_Tracking.xlsx",
        "JOB_DISCOVERY_FILE": "C:\\Users\\developer\\Documents\\Job_Search_Discovery.xlsx",
        "JOB_INTERVIEW_PREP_FILE": "C:\\Users\\developer\\Documents\\Interview_Prep_QA.md",
        "JOB_RESUMES_DIR": "C:\\Users\\developer\\Documents\\Resumes"
      }
    }
  }
}

macOS (same shape, forward-slash paths):

{
  "mcpServers": {
    "job-tracker": {
      "command": "node",
      "args": ["/Users/developer/mcp-job-tracker/dist/index.js"],
      "env": {
        "JOB_TRACKER_FILE": "/Users/developer/Documents/Job_Tracking.xlsx",
        "JOB_DISCOVERY_FILE": "/Users/developer/Documents/Job_Search_Discovery.xlsx",
        "JOB_INTERVIEW_PREP_FILE": "/Users/developer/Documents/Interview_Prep_QA.md",
        "JOB_RESUMES_DIR": "/Users/developer/Documents/Resumes"
      }
    }
  }
}

node above assumes it's on PATH; if Claude Desktop can't find it, replace "command": "node" with the absolute path from which node (macOS) / where node (Windows). setup.mjs (below) writes this file for you automatically, on either OS, so hand-editing it is only needed if you'd rather not use it.

Restart Claude Desktop. You should see the job-tracker tools appear. If JOB_TRACKER_FILE is omitted, the server defaults to <home>\Documents\Job_Tracking.xlsx. If JOB_DISCOVERY_FILE, JOB_INTERVIEW_PREP_FILE, or JOB_RESUMES_DIR are omitted, they default to Job_Search_Discovery.xlsx, Interview_Prep_QA.md, and a Resumes\ folder sitting next to the tracker file. Claude Code uses the same entry under mcpServers in ~/.claude.json.

Moving to another machine

The server is portable (plain Node stdio — Windows or macOS). To set it up on a new machine:

  1. Copy this project folder over (you can skip node_modules and dist).

  2. Copy your Job_Tracking.xlsx to the new machine — or, for a genuinely fresh start with no existing spreadsheet, skip this step and call the init_job_tracker_files tool once the server is running; it creates the folder and both blank, formatted workbooks for you.

  3. Make sure Node.js 18+ is installed (node --version) — see Prerequisites above; nothing else is required for the server itself.

  4. Run the installer, pointing it at your spreadsheet:

    node setup.mjs "C:\\path\\to\\Job_Tracking.xlsx"     # Windows
    node setup.mjs "/Users/you/Documents/Job_Tracking.xlsx"  # macOS

    It installs deps, builds, backs up the existing Claude Desktop config, and wires in the job-tracker entry with paths correct for that machine. Omit the path to default to <home>/Documents/Job_Tracking.xlsx. Flags: --no-build (reuse a copied dist/), --bare-node (use node from PATH instead of an absolute node path).

  5. Fully quit and restart Claude Desktop.

Tools

Setup

Tool

What it does

init_job_tracker_files

Bootstrap a fresh machine/profile: creates the Job Tracking root folder if missing, and creates Job_Tracking.xlsx and/or Job_Search_Discovery.xlsx from a blank, formatted template (header row only) for whichever is missing. Never touches a file that already exists.

check_setup

Read-only readiness report: which files/folders exist, whether search criteria and the resume master are set up, whether Gmail is authorized, and whether a PDF converter (LibreOffice/Word) is available — with a nextSteps list for anything missing. Run this first on an unfamiliar machine instead of discovering gaps one tool at a time.

Tool

What it does

list_jobs

List all rows. Optional filters: status (case-insensitive exact), company (substring), and a applied_from / applied_to date range (YYYY-MM-DD).

get_job

Full details for one application, matched by company (+ position if the company has several rows). Includes daysSinceApplied and resumeFiles (matching files in the Resumes folder, so you can see which resume you used).

add_job

Append a new row. company + position required; everything else optional. status defaults to Awaiting Response, date_applied defaults to today. Set custom columns via extra_fields.

update_job_status

Update status (and optionally notes) for a row matched by company + position. Refuses to touch a non-existent or ambiguous match.

update_job

Edit any field(s) of a row matched by company + position — location, resume version, dates, notes, and custom columns (via extra_fields). Rename via new_company / new_position; pass an empty string to clear a field. Only supplied fields change; reports exactly what changed.

delete_job

Permanently remove a row matched by company + position, shifting rows below it up (like Excel's Delete Row). Refuses missing/ambiguous matches, backs up first, and returns the full deleted row so it can be re-added.

get_stale_jobs

Rows still Awaiting Response or Applied whose Date Applied is more than days ago (default 14), computed against today.

get_due_followups

Rows whose Next Follow-Up date is due — on/before today, or within the next days days. Skips closed rows (Declined/Withdrawn/Closed); sorted most-overdue first (daysUntil is negative when overdue).

search_jobs

Case-insensitive substring search across Company, Position, and Notes.

draft_followup_message

(stretch) Drafts a short, professional follow-up email for a role, referencing time since applying and the resume version used. Draft text only — it does not send anything.

add_column

Add a new custom column at the end of the sheet. Optional default_value fills every existing row. Rejects duplicate names.

modify_column

Rename a custom column (new_name) and/or set one value across every row (fill_value; empty string clears it). Refuses on the base columns.

delete_column

Remove a custom column, shifting the columns to its right left (like Excel's Delete Column). Refuses on the base columns.

Discovery-sheet tools (Job_Search_Discovery.xlsx)

Tool

What it does

discovery_list

List leads. Optional filters: company (substring), status (exact), query (substring across all columns).

discovery_add

Append a lead. company + position required; all other columns via fields. Date Found defaults to today, Status to Open.

discovery_update

Edit a lead matched by company + position. Rename via new_company / new_position; set columns via fields (empty string clears).

discovery_delete

Remove a lead matched by company + position, returning the deleted row so it can be re-added.

promote_to_tracker

Copy a lead into the tracker as an application — maps Company/Position/Job Link/Location, sets Status Applied and Date Applied today (both overridable), and folds Salary + Match Assessment into Notes. Refuses to create a duplicate tracker row. Optional remove_from_discovery moves it instead of copying.

discovery_sync

Housekeeping + append in one call: drops discovery rows whose Company+Position now matches a tracker application, flags anything 60+ days old Stale (60+ days), then appends new_rows (refusing duplicates by Company+Position, Job Link, or an existing tracker application). Pass an empty array to run housekeeping alone. Prefer this over repeated discovery_add calls when processing a sweep's results.

Search criteria & board sweep

Tool

What it does

get_search_criteria

Return the saved search criteria (search_criteria.json): target job titles, work style (remote/hybrid/onsite), city/country, minimum annual salary, and a free-text notes field. On a fresh install nothing is saved yet — the result's isComplete is false and promptsNeeded lists exactly what to ask the user before calling update_search_criteria.

update_search_criteria

Create or update the saved criteria. Every field is optional — only what you pass changes. Backs up the previous file first.

run_job_sweep

Probe ~95 Greenhouse/Ashby/Lever/SmartRecruiters/Workday boards concurrently (in-process, plain HTTP — no external script), filter to engineering-leadership titles whose location plausibly matches the saved criteria, fetch salary/posted-date detail, HTTP-check links, and dedupe against both spreadsheets. Returns only new candidates. Refuses to run until search criteria are complete. Optional quick: true skips boards the last run recorded as unreachable.

get_broad_search_queries

Returns ready-to-run WebSearch queries (built from the same saved criteria) aimed at companies outside run_job_sweep's fixed board list — that list is a hard ceiling on what the sweep can ever find, so this is a mandatory complement, not a fallback. The calling model runs the queries itself (this server has no web-search access) and verifies hits before treating them as candidates.

Interview-prep tools (Interview_Prep_QA.md)

Tool

What it does

read_interview_prep

Return the prep file as text, or just one company's section with the optional company filter. Friendly message (not an error) if the file or section doesn't exist.

append_interview_prep

Append markdown under a ## {company} section — creating the file (with a top-level heading) and/or the section as needed, or adding to an existing section without duplicating its heading. Backs up first.

Document tools (Resumes\)

Tool

What it does

generate_resume

Generate a tailored resume PDF (or docx) host-side and save it to Resumes\ — no base64, works from Claude Desktop. Stable facts (employers, dates, education, skills) come from resume_master.json; the model supplies only the per-posting summary + key_qualifications bullets for a company + position. The .docx is built in-process (the docx package); PDF output converts that via LibreOffice or Word if either is installed — if neither is found and format wasn't explicitly set, it falls back to .docx and says so (note in the result) rather than failing. See the workflow below.

create_resume_master

One-time setup: creates resume_master.json from a user-uploaded resume's real content (name, contact, experience, education, skills, etc.) — never fabricated. Refuses to run if the file already exists, so it only ever fires once; use update_resume_master_field after that.

save_document

Save a .docx/.pdf into the Resumes\ folder (created if missing). Takes source_path (preferred — an absolute path to a file already on disk, copied instantly) or content_base64 (fallback, small files only — capped at 20 000 chars ≈ 15 KB). Validates the leading bytes match the extension. Refuses to overwrite unless overwrite: true, backing the old file up first. Returns the saved path and size.

print_document

Silently print a saved file (name in Resumes\, or a full path) to the default or a named printer. PDFs print directly; .docx is converted to PDF first (cached next to the docx). Lists available printers if a bad name is given. Confirms the job was sent, not physically finished.

read_document

Extract the plain text of a saved .pdf/.docx in Resumes\ (same scoping as delete_document) so it can be read/analyzed in conversation. Read-only; returns text + metadata (type, pages, words, chars). Very long text is capped with an explicit truncated note — never silently.

read_text_file

Read any plain-text file (.md, .txt, .json, .csv, …) anywhere under the Job Tracking root — handoff notes, scratch files, etc. — by filename or relative path. Read-only and strictly scoped: ../absolute paths that escape the root are refused; a missing file lists the directory's contents. Non-text extensions are refused (PDFs/docx → read_document). Truncates past ~1 MB with a note; refuses past 5 MB.

list_documents

List saved resumes / cover letters in the Resumes\ folder (name, size, modified date). Optional filter substring on the name; newest first.

delete_document

Delete a document from the Resumes\ folder, backing it up (rotating .backups) first. Refuses a missing file or a path outside the folder.

list_resume_master_structure

Show the shape of resume_master.json (keys, array lengths, scalar types — not full values) to find a path. Optional path scopes it.

get_resume_master_field

Read one field from resume_master.json by dot/bracket path, e.g. skills, projects[0].description, experience[2].title.

update_resume_master_field

Update one existing field in resume_master.json by path (backs up first, reports old → new). Won't create new keys or change a field's kind (array/object/scalar). Scoped to that one file.

Gmail tools (optional — require one-time auth)

Tool

What it does

search_gmail_for_job

Search your Gmail for job-related mail by company (+ optional position, free-text query, and limit, default 10 / max 50). Read-only.

read_gmail_message

Read one message's full body by its id (e.g. from a search_gmail_for_job result). Read-only.

scan_job_updates

Scan recent Gmail for updates relevant to your tracked applications — optional company filter, days window (default 30), per-company limit (default 3). Read-only.

draft_gmail_reply

Save a Gmail draft — reply within a thread_id, or a new message to to; body required, optional subject/cc. Never sends.

send_gmail_email

Sends immediately and cannot be recalled. Refuses unless confirm: true, and only after the user has approved the exact to/subject/body.

Until Gmail is authorized (see Gmail integration below), all five return a clear "authorize first" error.

Spreadsheet schema

Both workbooks use a dynamic schema — the columns are driven by each sheet's actual header row, and the read/write engine is schema-agnostic (a sheet is just a spec: file path, required columns, which columns are dates, and the match keys).

Tracker (Job_Tracking.xlsx)

The ten base columns below must always be present (the tools match, filter, and compute dates on them) and are protected from rename/delete:

Company | Position | Job Link | Location | Resume Version | Contact/Referral | Date Applied | Status | Next Follow-Up | Notes

Any custom columns you add beyond these are first-class:

  • They appear in list_jobs / get_job output automatically.

  • You set them per job with the extra_fields argument of add_job and update_job, e.g. extra_fields: { "Salary Range": "180-210k" } (an empty string clears the value).

  • You add / rename / remove them with add_column, modify_column, and delete_column.

Custom columns are stored as text. The two base date columns (Date Applied, Next Follow-Up) are the ones stored as Excel date serials.

Managing columns

// add a column, pre-filled on every existing row
add_column      { "name": "Salary Range", "default_value": "TBD" }
// rename it and/or overwrite every row's value
modify_column   { "name": "Salary Range", "new_name": "Comp", "fill_value": "Confidential" }
// remove it (shifts later columns left, like Excel)
delete_column   { "name": "Comp" }

Conventional Status values: Applied, Awaiting Response, Informal - Referral Sent, Interviewing, Offer, Declined, Withdrawn, Closed - No Longer Available. (The field is free text — these are just the values suggested to Claude.)

Discovery (Job_Search_Discovery.xlsx)

The base columns, in order:

Date Found | Company | Position | Location (Remote/Hybrid) | Salary | Salary Confidence (Confirmed/Estimated) | Job Link | Source (Company Careers Page/LinkedIn/Aggregator) | Posted Date / Days Since Posted | Known Gap Flag | Match Assessment | Status

All discovery values are stored as text (including Date Found, kept as an ISO YYYY-MM-DD string — no date-serial conversion). Rows are matched by Company + Position. Set columns through the fields argument of discovery_add / discovery_update; Company and Position have their own parameters. Status on new leads defaults to Open.

The column-management tools (add_column etc.) operate on the tracker; the discovery sheet is managed through its own row tools.

Formatting: SheetJS (used for the data read/write itself) doesn't re-serialize rich cell styling, so every MCP discovery write is followed by a best-effort exceljs reformat pass (xlsxFormat.ts) that (re)applies bold header / frozen row / auto-filter / wrapped text / clickable Job Link cells. Pure JS, nothing external required; if it ever fails for some reason the data write still succeeds (the tool returns formatted: false).

Interview prep (Interview_Prep_QA.md)

A plain-markdown reference — not a spreadsheet — organised as:

# Interview Prep — Q&A Reference

## Company — Role
**Q: ...?**

A: ...

*Notes: ...*

---

## Next Company — Role
...

append_interview_prep matches an existing ## Company section on the company portion of the heading (the part before — Role), so appending "Acme Corp" lands under ## Acme Corp — Engineering Manager. New sections are separated with ---. Keep to the **Q: …** / A: … / optional *Notes: …* pattern when adding content.

If a bare company name matches more than one section (e.g. ## Acme Corp — Billing and ## Acme Corp — Clinical), both read/append refuse and list the matches — pass the full heading to disambiguate. An exact full-title match always wins over a bare prefix, so a specific heading is never ambiguous.

Generating a tailored resume (generate_resume)

The recommended way to produce a resume — especially from Claude Desktop, which can't write to arbitrary host paths. The model never emits the binary; it emits tailored text, and the server renders the PDF host-side. The server's MCP instructions tell connected clients to use this tool for resumes rather than hand-rolling their own resume-generation script or moving bytes through base64, so it's the single resume path (resumeDocx.ts is the one generator).

generate_resume {
  "company": "Acme Corp",
  "position": "Engineering Manager",
  "summary": "…tailored SUMMARY paragraph for this posting…",
  "key_qualifications": [
    "…bullet tying real experience to this role…",
    "…another alignment bullet…"
  ]
  // optional: skills[], include_projects, format ("pdf"|"docx"), filename, overwrite
}
  • Facts stay fixed. Employers, titles, dates, education, certs, and the base skills live in resume_master.json; the model can't invent or drift them — it only supplies the summary and key_qualifications for this company + position. Edit your real facts in resume_master.json (a sibling of the tracker; override with JOB_RESUME_MASTER_FILE).

  • Rendering merges master + tailoring → .docx via resumeDocx.ts (the docx package, in-process; the measured spec: Liberation Serif, 20 pt name, ruled 11 pt headings, 0.63" margins, US Letter) → PDF via LibreOffice or Word, whichever is installed. ~15 s per resume.

  • Output lands in Resumes\ (e.g. Dale-Magrath-Resume-Acme-Corp-Engineering-Manager.pdf), ready for print_document. No base64, no download-then-move.

  • Wired to tracking. The result carries a nextStep with the exact follow-up call, chosen from the sheets: brand-new company → add_job (as Applied, Resume Version set to the file), an existing discovery lead → promote_to_tracker, already tracked → update_job to set the Resume Version. So tailor → save → apply → track is one Desktop flow — the model just offers the suggested step.

Why this beats base64: the tailored text is ~1 k tokens; the 55 KB PDF binary would be ~22 k. The model does the writing; the host does the rendering.

Rendering notes (why it's built this way)

Hard-won details worth keeping — changing them tends to reintroduce old bugs:

  • Role/date lines are a 2-column borderless table, not a tab stop. A tab with a right tab-stop spanning a bold title and italic dates in one paragraph triggered a LibreOffice PDF-export bug (ToUnicode CMap on subset fonts) that made the title extract as garbled characters in pdftotext, pypdf, and pdfminer.six — even though it looked fine on screen (and could trip ATS parsers). The table renders identically and extracts cleanly in all three. Keep it a table.

  • LibreOffice does the docx→PDF, not Word. LibreOffice subsets fonts → ~55 KB with Liberation Serif, matching the existing resumes. Word barely subsets (embedded ~1.1 MB of Calibri → a 258 KB PDF), so convertDocxToPdf prefers LibreOffice and falls back to Word only if it's absent — and only on Windows; macOS/Linux have no COM automation to fall back to, so LibreOffice is the sole path there (checked at /Applications/LibreOffice.app/... on macOS, common Linux install paths, then bare soffice on PATH).

  • 2-page fit is by layout, never by truncation. A full experience history + 6-bullet Key Qualifications + Projects + Education + Skills lands on 2 pages with Honors & Awards off (opt in with include_honors). If it ever runs long, the lever is tightening spacing (heading 12/5 pt, body 8 pt) — content is never silently trimmed to hit a page count.

  • base64 is a last resort. The only resume ever saved through the base64 path came out corrupt (valid %PDF/%%EOF but a broken startxref). The leading-bytes check catches gross truncation, not subtle corruption — so prefer generate_resume, or save_document with source_path, for anything binary.

Saving an already-generated document (Resumes\)

If a file already exists on disk (e.g. produced by the docx/pdf skill), save_document puts it into Resumes\, and print_document sends it to a printer:

// Preferred: hand over a path to a file already written to disk.
save_document   { "filename": "Dale-Resume-Acme-Corp.docx", "source_path": "C:\\Temp\\out.docx" }
print_document  { "filename": "Dale-Resume-Acme-Corp.docx" }            // default printer
print_document  { "filename": "sample.pdf", "printer_name": "Office EPSON ET-3850 Series" }

Always prefer source_path for real documents. content_base64 makes the model emit the whole file as base64 token-by-token — roughly 3 characters per token for binary formats, so a 53 KB resume is ~22 000 output tokens (several minutes), and a 134 KB PDF exceeds the response limit and can never finish. source_path costs ~30 tokens at any file size and can't corrupt the bytes. Inline base64 is therefore capped at 20 000 characters and fails immediately with a pointer to source_path.

  • PDFs print directly — via SumatraPDF (bundled with pdf-to-printer) on Windows, or CUPS's lp on macOS/Linux (present by default on macOS, and on any Linux with CUPS installed). No dialog, single copy, default settings either way.

  • .docx is converted to a PDF next to it first — using LibreOffice (soffice --headless --convert-to pdf) if installed, otherwise (Windows only) Microsoft Word via COM. The PDF is cached and reused while it's newer than the docx. If no converter is available, printing a .docx returns a clear error suggesting you save it as a PDF instead.

  • print_document confirms the job was sent to the printer — not that it physically finished. Base64 transport is meant for small files (resumes, well under 1 MB), not large documents.

Gmail integration (optional)

The five Gmail tools let Claude search your inbox for recruiter mail, scan for updates on tracked applications, and draft/send replies — all against your own account. They're off until you authorize them; unauthorized calls return a clear error telling you what to run.

One-time setup (full walkthrough in GMAIL_SETUP.md):

  1. Create a Google Cloud project, enable the Gmail API, and create a Desktop app OAuth client; download its JSON as credentials.json in the project root (next to package.json). This step is manual — it can't be scripted.

  2. Run the one-time consent flow:

    npm run gmail:auth

    It opens Google's consent screen and saves the token to gmail_token.json. The server refreshes it automatically afterward.

  3. Restart Claude Desktop so the tools load.

Config / files (both .gitignored):

File

Purpose

Env override

credentials.json

OAuth client (manual download)

JOB_GMAIL_CREDENTIALS_FILE

gmail_token.json

Stored access/refresh token from consent

JOB_GMAIL_TOKEN_FILE

Requested scopes are read-only + compose + send (gmail.readonly, gmail.compose, gmail.send) — intentionally no mailbox modify/delete access. npm install pulls the one dependency (google-auth-library).

Safety: search_gmail_for_job / read_gmail_message / scan_job_updates are read-only; draft_gmail_reply only ever creates a draft; send_gmail_email sends immediately and can't be undone, so it refuses without confirm: true and should only be called after you've reviewed the exact to/subject/body.

Safety & behavior notes

  • Backups (rotating): every write first copies the file into a sibling .backups\ folder with a timestamped name (<file>.<YYYYMMDD-HHmmss-SSS>.bak), keeping the last 10 per file. This replaces the old single-slot .bak, so repeated writes (manual edits or the daily task) can't clobber your last-good copy. Applies to the sheets, the prep file, and delete_document; promote_to_tracker backs up each file it writes. (Nothing is backed up when a file is created fresh.)

  • Fresh reads: the workbook is re-read on every call, so edits you make directly in Excel are always reflected.

  • Validation: row-editing tools verify the row exists (and is unambiguous) before changing anything; bad dates are rejected up front. promote_to_tracker refuses to create a duplicate tracker row.

  • Dates (tracker only) are stored as Excel date serials formatted d-mmm-yy. New rows are written the same way, so 2026-06-01 shows as 1-Jun-26 — matching the existing rows. Reads/writes use timezone-free calendar-date math. Discovery dates are plain ISO text.

  • Formatting preservation: new rows copy the number format (and, where the writer supports it, the cell style) of the last existing row, and column widths are preserved — the way a human copy-pasting a row would leave things.

    Note: the open-source SheetJS build faithfully preserves number formats and column widths (what this workbook uses). Rich per-cell styling (custom fonts/fills/borders) is not re-serialized by the community build; if you later add heavy cell styling, preserving it would require SheetJS Pro.

  • File locked: if a file is open in another program (Excel for the sheets, Word/an editor for the prep .md), writes fail with a clear "please close it" message rather than a cryptic error.

Testing

npm test

npm test (via run-tests.mjs) runs two suites:

  • test-client.mjs — an integration suite. Builds the server, spins it up over stdio against self-contained fixtures in a temp folder, and asserts on every tool (see the file for the current count — run npm test rather than trusting a hardcoded number here) across the tracker, discovery, promote, interview-prep ambiguity, document, search-criteria/sweep/ discovery-sync, setup, and backup paths. Your real files are never touched — the suite creates its own throwaway tracker/discovery/prep/Resumes in the OS temp dir and deletes them afterward.

  • test-unit.mjs — plain unit tests, importing dist/*.js directly (no server process) for the pure logic that lives right next to the network/ OAuth calls above: title/location classification, salary-text mining, per-ATS board-response parsing, Gmail header/MIME/raw-email helpers, and the board-health cache round-trip. This is exactly the logic that would otherwise go completely untested, since it sits inside the same files as the excluded network calls.

Either script exits non-zero if any check fails, and so does npm test.

It does not send a physical print job — print_document is covered only via its error paths (missing file, unsupported type, unknown printer). Likewise, run_job_sweep/runSweep() itself is only covered via its refuses-without- criteria path — it makes live outbound calls to ~95 real ATS endpoints, which isn't something either suite exercises automatically.

npm run coverage

Runs both suites together under c8 and prints a per-file statement/branch/function breakdown (plus an HTML report under coverage/, gitignored). sweepEngine.ts/gmailAuth.ts/gmailTools.ts wrap their genuinely network/OAuth-bound functions in /* c8 ignore */ comments — deliberately narrow: only the functions whose entire body is a live call are excluded, so pure logic living right next to them (and tested by test-unit.mjs) still counts. What's left uncovered after that is real, visible, and mostly runSweep()'s own orchestration plus the Gmail tool handlers' post-auth bodies — both need either mocking or real credentials/network to close further.

Available Tools

35 tools
add_columnAdd columnA

Add a new custom column to the spreadsheet, appended after the last column. Optionally fill every existing job row with a default value. Refuses if a column with that name already exists. Backs the file up before writing. (Note: the built-in job tools only read/write the standard columns; custom columns are managed with these column tools.)

Inputs: name (required), default_value.

add_jobAdd jobA

Append a new job application row. Company and Position are required; all other fields are optional. Status defaults to "Awaiting Response" and Date Applied defaults to today if omitted. Use extra_fields to set any custom columns you've added. Backs the file up to .bak before writing.

Inputs: company (required), position (required), notes, status, +7 more.

append_interview_prepAppend interview prepA

Append markdown to the interview-prep file under a company's section. Creates the file (with a top-level heading) and/or the company section if needed; otherwise adds to the existing section without duplicating its heading. Keep the file's style: 'Q: ...' / 'A: ...' pairs and optional 'Notes: ...' lines. Backs the file up before writing.

Inputs: company (required), content (required).

delete_columnDelete columnA

Permanently remove a custom column and shift the columns to its right left by one (like Excel's Delete Column). Refuses on the built-in columns and on a missing column. Backs the file up to .bak first.

Inputs: name (required).

delete_documentDelete documentA

Delete a saved document from the Resumes folder. Backs it up (to the rotating .backups folder) first so it can be restored, then removes it. Refuses a missing file or any path outside the Resumes folder.

Inputs: filename (required).

delete_jobDelete jobA

Permanently remove a job application row, matched by company + position, shifting the rows below it up (like Excel's Delete Row). Refuses to act on a missing or ambiguous match, backs the file up to .bak first, and returns the full deleted row so it can be re-added if needed.

Inputs: company (required), position (required).

discovery_addAdd discovery leadA

Append a new lead to the discovery sheet. Company and Position are required; set any other columns via fields (e.g. Salary, Job Link, "Match Assessment"). Date Found defaults to today and Status to "Open" if not supplied. Backs the file up first.

Inputs: company (required), position (required), fields.

discovery_deleteDelete discovery leadA

Permanently remove a discovery lead, matched by company + position, shifting rows below it up. Refuses missing/ambiguous matches, backs up first, and returns the deleted row so it can be re-added.

Inputs: company (required), position (required).

discovery_listList discovery leadsA

List job leads from the discovery sheet (Job_Search_Discovery.xlsx). Optional filters: company (case-insensitive substring), status (case-insensitive exact), and query (substring across every column).

Inputs: query, status, company.

discovery_updateUpdate discovery leadA

Edit a discovery lead, matched by company + position. Rename via new_company / new_position; set any columns via fields (empty string clears a value). Refuses missing/ambiguous matches; backs up first.

Inputs: company (required), position (required), fields, new_company, +1 more.

draft_followup_messageDraft follow-up messageA

Draft a short, professional follow-up email for a given application, referencing how long it has been since applying, the role, and the resume version used. Returns draft text only — it does not send anything.

Inputs: company (required), position.

draft_gmail_replyDraft a Gmail reply (not sent)A

Create a real Gmail draft — visible in the Drafts folder — but does NOT send it; a human still has to open it and hit Send. Either replies within an existing thread (pass thread_id, e.g. from search_gmail_for_job or scan_job_updates) or starts a new message (pass to + subject). Requires Gmail to be authorized (see GMAIL_SETUP.md).

Inputs: body (required), cc, to, subject, +1 more.

generate_resumeGenerate tailored resumeA

Generate a tailored resume PDF (or docx) on the host and save it into the Resumes folder — no base64, works from Claude Desktop. The STABLE facts (employers, titles, dates, education, skills) come from resume_master.json; YOU supply only the per-posting tailoring: a rewritten summary and the key_qualifications bullets aligning the candidate to this specific company + position. The server renders the master + your tailoring into a formatted PDF (python-docx + LibreOffice) matching the existing resumes. Do NOT invent employers, dates, or degrees — those are fixed in the master. Returns the saved path and size, plus a nextStep with a suggested follow-up call (add_job / promote_to_tracker / update_job) — offer it to the user so tailoring, saving, and tracking flow together.

Inputs: company (required), summary (required), position (required), key_qualifications (required), +6 more.

get_due_followupsGet due follow-upsA

List applications whose Next Follow-Up date is due — on or before today (or within the next days days). Skips clearly-closed rows (Declined / Withdrawn / Closed). Sorted most-overdue first; daysUntil is negative when overdue.

Inputs: days.

get_jobGet jobA

Return full details for a single application, matched by company (and position, if the company has more than one row).

Inputs: company (required), position.

get_resume_master_fieldGet resume master fieldA

Read one field from resume_master.json (the stable resume facts) by dot/bracket path, e.g. "skills", "projects[0].description", "experience[2].title". Scoped to that one file. Use list_resume_master_structure first if you're unsure of the path.

Inputs: path (required).

get_stale_jobsGet stale jobsA

List applications still "Awaiting Response" or "Applied" whose Date Applied is more than N days ago (default 14), computed against today.

Inputs: days.

list_documentsList documentsA

List the saved resumes / cover letters in the Resumes folder (name, size, and modified date). Optional filter is a case-insensitive substring on the file name. Newest first.

Inputs: filter.

list_jobsList jobsA

List all tracked job applications, with optional filters. Filter by status (case-insensitive exact match), company (case-insensitive substring), and/or a Date Applied range (inclusive, YYYY-MM-DD).

Inputs: status, company, applied_to, applied_from.

list_resume_master_structureList resume master structureA

Show the SHAPE of resume_master.json (keys, array lengths, scalar types — not the full field values) to help pick a path for get_resume_master_field / update_resume_master_field. Optional path scopes the listing to a sub-section.

Inputs: path.

modify_columnModify columnA

Modify a custom column: rename it (new_name) and/or set the same value into every existing job row (fill_value; pass an empty string to clear the column). At least one of new_name / fill_value is required. Refuses on the built-in columns and on a missing column. Backs the file up first.

Inputs: name (required), new_name, fill_value.

promote_to_trackerPromote lead to trackerA

Turn a discovery lead into a tracked application: copies Company, Position, Job Link, and Location into the tracker with Status "Applied" and Date Applied = today (both overridable). Salary and Match Assessment are carried into Notes unless you supply your own. Optionally removes the lead from the discovery sheet. Backs up both files it writes.

Inputs: company (required), position (required), notes, status, +3 more.

read_documentRead documentA

Extract and return the plain text of a saved resume / cover letter (.pdf or .docx) from the Resumes folder, so it can be read and analyzed in conversation (e.g. "read my Acme Corp resume and check it against this posting"). Read-only. Give a file name in the Resumes folder.

Inputs: filename (required).

read_gmail_messageRead a Gmail message's full bodyA

Read the full body of ONE specific Gmail message — search_gmail_for_job and scan_job_updates only return a short snippet, so use this when you need the actual content (e.g. an interview link or instructions). Pass message_id (the id from a search/scan result), or thread_id to read the most recent message in that thread. Read-only.

Inputs: thread_id, message_id.

read_interview_prepRead interview prepA

Read the interview-prep Q&A markdown file (Interview_Prep_QA.md) and return it as text. Pass an optional company to return just that company's section. Returns a friendly message (not an error) if the file doesn't exist yet or the company has no section.

Inputs: company.

read_text_fileRead text fileA

Read any plain-text file (.md, .txt, .json, .csv, …) inside the Job Tracking folder tree — handoff notes, scratch files, etc. Give a filename or relative path (e.g. "HANDOFF_2026-08-02.md" or "notes/plan.txt"), resolved under the Job Tracking root. Read-only, and strictly scoped to that tree — paths that escape it are refused. For PDFs/docx in the Resumes folder use read_document instead.

Inputs: filename (required).

save_documentSave documentA

Save a generated document (docx or PDF) into the Resumes folder (created if missing). PREFERRED: pass source_path — the full path to a file already written to disk (e.g. one the docx/pdf skill just generated). The server copies it; this is instant and cannot corrupt the file. FALLBACK: content_base64, for SMALL files only (~15 KB decoded max). Base64 must be generated token-by-token, so a typical 50 KB resume takes minutes and may exceed the response limit outright — use source_path for anything binary or non-trivial. Give exactly one of the two. Refuses to overwrite an existing file unless overwrite=true, in which case the old file is backed up first. Returns the saved path and size.

Inputs: filename (required), overwrite, source_path, content_base64.

scan_job_updatesScan Gmail for tracker updatesA

Cross-reference open applications in the tracker against Gmail: for each open job (or one company, if given), searches Gmail for recent related mail and flags simple keyword-based signals (interview, offer, rejection language). Read-only — it only SUGGESTS a status change via suggestedCall; it never updates the tracker itself. Review the emails and call update_job_status yourself if the suggestion looks right.

Inputs: company, days_back, max_per_company.

search_gmail_for_jobSearch Gmail for job-related emailA

Read-only search of the authorized Gmail account for messages related to a company (and optionally position/keywords) — recruiter replies, interview invites, rejections, etc. Does not modify anything. Requires Gmail to be authorized first (see GMAIL_SETUP.md / npm run gmail:auth).

Inputs: company (required), query, position, max_results.

search_jobsSearch jobsA

Case-insensitive substring search across the Company, Position, and Notes columns.

Inputs: query (required).

send_gmail_emailSend an email via GmailA

SENDS a real email immediately through the authorized Gmail account. This cannot be undone — there is no draft step. Only call this after showing the user the exact to/subject/body and getting explicit confirmation; the call must include confirm: true or it is refused. Either sends a reply within an existing thread (thread_id) or a new email (to + subject). Requires Gmail to be authorized (see GMAIL_SETUP.md).

Inputs: body (required), confirm (required), cc, to, +2 more.

update_jobUpdate job (any field)A

Edit any field(s) of an existing application, matched by company + position. Only the fields you supply are changed; pass an empty string to clear a field. Rename with new_company / new_position. Set custom columns via extra_fields. Refuses missing or ambiguous matches, validates dates, and backs the file up before writing.

Inputs: company (required), position (required), notes, status, +9 more.

update_job_statusUpdate job statusA

Update the Status (and optionally the Notes) of an existing application, matched by company + position. Validates the row exists and backs the file up before writing.

Inputs: status (required), company (required), position (required), notes.

update_resume_master_fieldUpdate resume master fieldA

Update one existing field in resume_master.json by dot/bracket path (e.g. a corrected date, an added skill, a new project). Only edits fields that ALREADY exist — it will not create new keys (that's a deliberate schema change to make by hand). Backs the file up first and reports old → new. Scoped to resume_master.json only.

Inputs: path (required), value (required).

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A3.8/5.0

    Scored across 35 tools

    Disambiguation3/5

    Most tools target distinct resources and actions, but update_job_status overlaps with update_job (which can also set status), and scan_job_updates vs search_gmail_for_job both provide Gmail search functionality. The descriptions help, but a few tools could cause misselection.

    Naming Consistency3/5

    The dominant pattern is underscore-separated verb_noun (add_job, delete_document, get_resume_master_field), but discovery_* tools reverse this to noun_verb (discovery_list, discovery_add), and Gmail tools mix patterns (search_gmail_for_job vs read_gmail_message). The inconsistency is noticeable but not chaotic.

    Tool Count2/5

    With 35 tools this is well above the typical well-scoped range (3-15) and even the heavy range (16-25). The large count reflects the many subdomains covered, but several tools could be consolidated (e.g., update_job_status into update_job, or read_document/read_text_file into one scoped reader).

    Completeness4/5

    The tool set covers the full job-search lifecycle: tracker CRUD, discovery pipeline, interview prep, resume generation and document management, plus Gmail integration with a safety-conscious send. Minor gaps exist (e.g., no way to edit or delete interview prep sections, no direct Gmail thread listing) but they don't create dead ends.

    Related MCP Connectors

    Related MCP Servers

    • F
      license
      Not graded
      quality
      C
      maintenance
      Enables Claude Desktop to manage a job search end-to-end: find and score job listings, tailor resumes, generate application messages, and track application history, while leaving final external actions to the user.
      -
    • A
      license
      A
      quality
      B
      maintenance
      Enables MCP clients such as Claude Desktop or compatible editors to run local, privacy-first job-search workflows including resume generation and STAR polishing, JD matching, GitHub project recommendations, mock interviews, and application tracking. It exposes these capabilities as callable tools through the Model Context Protocol.
      7
      1
      MIT
    • A
      license
      Not graded
      quality
      B
      maintenance
      Enables Claude to read and update the local job-search tracker, adding postings from screenshots, pasted text or links, drafting application emails, syncing Gmail to log sent applications and replies, and answering questions about what's due or which jobs match a criteria. Every change is validated and appears live on the dashboard with a timeline entry.
      MIT