Case study · Personal

ATS-readable CV generator with an MCP server

Bilingual CV library rendered to a strict one-page PDF, checked for ATS readability, with an MCP server whose write tools only create proposals.

In brief

  • Personal tool to tailor a one-page CV and cover letter to each job offer in French or English, without losing content or letting an AI invent facts.
  • TypeScript monorepo: Hono API, React interface, Typst rendering with an automatic ATS-readability check, and an MCP server with 21 tools whose writes are only proposals.
  • Specified, designed, reviewed and accepted by me; code written with Claude Code. It runs online behind a passkey sign-in and feeds this portfolio.

Key facts

Role
Sole developer
Team
Solo project
Period
Status
In use for my own applications
AI assistance
major Specification, architecture, review, tests and acceptance by me; code written with Claude Code.
Stack
  • TypeScript
  • Node.js
  • Hono
  • React
  • Vite
  • Zod
  • Typst
  • MCP
  • WebAuthn
  • Vitest
  • nginx
  • systemd

Context

A personal project, started in October 2026 for my own job and internship applications. I wanted one source for my CV content in French and English, and a way to tailor a one-page CV to each offer.

Problem

Tailoring a CV by hand means copies that drift apart, translations that go stale and pages that overflow. Asking an AI assistant to do it is faster, but it can rephrase facts into claims that are not true. I also needed to know what an applicant-tracking system would actually read from the PDF.

My role, the team and AI assistance

Solo project. I wrote the specification (about 1,100 lines) and the 20 architecture decision records, chose the architecture, reviewed the code and ran the tests and acceptance. The code was written with Claude Code from that specification.

Constraints

  • No invention: an AI may suggest, never decide; every figure and skill must come from the library.
  • One page, strictly: an export that overflows is refused, with the overflow reported in lines.
  • Bilingual: an English CV cannot be exported while a displayed field is still untranslated.
  • Privacy: contact details and photo are never returned to the AI; real data never goes into the repository.

Solution and architecture

  • Data: a JSON library of 10 collections (experiences, education, projects, skills…) with bilingual fields validated by Zod; a missing translation is null, never an empty string. Presets and applications are tri-state selections over the library.
  • Rendering: Typst templates (ATS-friendly, visual, cover letter) render a deterministic PDF, plus a DOCX version.
  • ATS check: the text is extracted back from the PDF with two different extractors to check contacts, headings and dates; a 72-case battery covers layouts, languages and fonts.
  • Interfaces: a Hono API, a React/Vite interface, and an MCP server with 21 tools and 3 guided prompts. Its write tools only create proposals that I accept or reject in the interface, and server-side guardrails reject figures or skills absent from the source.
  • Imports: a LinkedIn data export and existing CVs become deduplicated proposals; cover letters are versioned, never overwritten.
  • Online instance: passkey (WebAuthn) sign-in, strict security headers and CSP, a hardened service, and a deployment script that rolls back automatically if the health check fails.

Stack

TypeScript, Node.js, npm workspaces, Hono, React 19, Vite, Zustand, Zod, Typst, docx, pdf.js, Model Context Protocol SDK, WebAuthn, Vitest, ESLint, nginx, systemd.

Quality and verification

  • About 410 Vitest cases: unit, API, MCP server in memory and over stdio, and passkey sign-in with a software authenticator.
  • The 72-case ATS-readability battery, and 8 written acceptance scenarios replayed before going online.
  • A pre-commit hook blocks real data and secrets; test fixtures use fictitious people.

Results and status

I use it for my own applications, and the experience, education and skills on this portfolio are exported from its library. The online instance is private.

What I would do differently

  • Add a continuous-integration run of the tests on every push, instead of relying on local runs before deployment.
  • Produce public screenshots from the fictitious personas from the start, so the project can be shown without exposing real data.
  • Write the public README separately from the operator notes, so the repository can be opened without editing history.
  • Source code: private repository, to be opened after clean-up.
  • Screenshots: to come, made with fictitious data.