Note: This was first published on the Resume-MCP blog. Reposting here for the dev community.
Applying to jobs is mostly copy-paste drudgery: re-tailor the resume, re-write the cover note, hunt down the recruiter's email, attach, send, repeat. I wanted to compress that whole loop into one paste. So I built Resume-MCP — paste a job description, and ~60 seconds later a JD-tailored, ATS-friendly PDF resume and a personalised cover email go out from your own Gmail.
This post is the engineering story, not the pitch: the architecture, the choices that mattered, and the parts that bit me.
The pipeline in one diagram
Job description (paste / PDF / DOCX / image)
│
▼
document_parser ──► extract clean JD text
│
▼
resume_customizer (Gemini, 3 parallel chunks)
│ header+skills │ experience │ projects
▼
render_latex (Jinja2 → .tex)
│
▼
pdflatex ×2 ──► ATS-friendly PDF
│
▼
Gmail API ──► email + attachment, sent as YOU
The whole thing is a FastAPI app. There's a web app, a Telegram bot, and — the part devs care about — an MCP server so you can drive it from any MCP client.
Why LaTeX instead of an HTML-to-PDF renderer
Most resume builders render HTML and print to PDF. That looks fine on screen and gets shredded by applicant tracking systems, because the text extraction order is whatever the DOM happens to be.
LaTeX gives me:
- Deterministic layout — the same input produces a byte-identical PDF. No "works on my Chrome" surprises.
-
Clean text extraction —
pdftotextpulls the content back in reading order, which is exactly what an ATS parser does. - Typography that looks hand-set without me hand-setting anything.
The catch: LaTeX needs to compile twice for cross-references (page numbers, any \ref) to resolve. So the compile step always runs pdflatex twice into a temp dir, then cleans up in a finally block:
for _ in range(2):
subprocess.run(["pdflatex", "-interaction=nonstopmode", tex_path],
cwd=tmp, check=True, capture_output=True)
Tailoring: three parallel Gemini calls, not one big prompt
The naive version is "here's my resume + the JD, rewrite it." That's slow and the model loses the plot on long inputs. Instead I split the resume into three independent chunks and fan them out concurrently:
- header + skills — reorder skills to surface JD-matched keywords first
- experience — rewrite bullets to mirror the JD's language without inventing anything
- projects — same treatment, and this chunk is non-fatal: if Gemini fails here, I log a warning and continue with empty projects rather than failing the whole request
Fanning out cut latency to roughly the slowest single chunk instead of the sum. The non-fatal projects chunk matters more than it sounds — it's the difference between "your resume is ready" and "something broke, try again."
The MCP angle
Model Context Protocol lets an AI client call your tools directly. I mounted an MCP server at /mcp that wraps the same HTTP endpoints, so from an MCP-aware client you can say "tailor my resume to this JD and apply" and it runs the full pipeline — no UI.
The lesson here: because the MCP tools are thin wrappers over the existing FastAPI routes, there's one code path to maintain. The web app, the Telegram bot, and the MCP server all hit the same endpoints. New feature ships everywhere at once.
Sending from your Gmail (the part everyone asks about)
The email isn't sent from some noreply@myapp address — it's sent from the user's own account via the Gmail API over OAuth. That's what makes it land in a recruiter's inbox like a real human applicant instead of a marketing blast.
The trade-off is real OAuth plumbing: gmail.send scope, refresh-token handling, and the un-fun edge cases (a missing refresh_token on second consent, a user who revoked the scope after granting it). If you're building anything that acts on a user's behalf, budget more time for the auth state machine than for the feature itself.
What I'd tell my past self
- Pick a deterministic output format early. LaTeX felt heavy on day one and saved me weeks of "why does the PDF look different now" later.
- Make the AI calls independent and let failures degrade gracefully. A best-effort chunk beats an all-or-nothing prompt.
- Build the core as plain HTTP endpoints first. The bot and the MCP server became trivial because the logic already lived behind a clean API.
If you want to try it: resume-mcp.site. Paste a JD, see the tailored PDF, and (if you connect Gmail) send the application without leaving the page.
Happy to go deeper on any layer in the comments — the LaTeX template, the Gemini chunking, or the MCP wiring.