Work · Case file 09 · This website (private repository, live endpoints) · 2026

This site: a résumé engine, an MCP server and a build that checks itself

At a glance

Result
A live MCP server and résumé engine behind this site, callable from this page, and a build that refuses to ship a skill whose proof does not resolve.
Problem
A portfolio's claims are usually prose a reader cannot check, and each tailored résumé is retyped with nothing tying it to the evidence.
My role
Solo, with Claude Code as the coding agent: the résumé engine, the MCP server, the prerender step, the build gates and the agent skills.I direct the work and approved every block of résumé text; the code is written with Claude Code, and the commits say so. The repository is private.
Stack
TypeScript · React prerendering · Vercel Functions · pdf-lib and docx · MCP over Streamable HTTP

Proof

What try, see and check mean

The problem

A portfolio usually asks a reader to trust prose. Numbers drift from their sources, a skills list grows past what the work shows, and each tailored résumé is retyped by hand. An agent reading the site gets HTML to scrape and nothing to call.

The idea. Keep every fact in one set of TypeScript data files and serve it three ways from the same code: prerendered pages for people, a JSON export and an MCP server for agents, and résumés composed only by selecting approved blocks. Then make the build refuse to ship a claim whose proof does not resolve.

Fig. 01 · Two paths, one set of data

How this site is built and servedOne set of TypeScript data files feeds two paths. Build path, npm run build: check-words fails on banned terms and em dashes; check-links records which outbound links answer and never fails; tsc type-checks the site and the API; vite builds the client and server bundles; prerender writes HTML for every route; check-skills fails when a proof link does not resolve; the output is dist with pages, sitemap, feed and a 404 page. Request path on Vercel: people get static HTML that React hydrates; agents POST JSON-RPC to /api/mcp, arguments are validated against JSON Schema, owner tools need a bearer token, and the tool returns JSON; /api/resume composes approved blocks, fits them to the page and renders PDF, Word or Markdown; /api/portfolio and /llms.txt export the same data.src/data/*.ts, one set of factsrésumé blocks and phrasings, case files, skills with proof, profile, notesBuild pathnpm run build, on every deploycheck-wordsbanned terms,em dashescheck-linksrecords whichlinks answertscsite and APItypesvite buildclient andserver bundlesprerenderHTML forevery routecheck-skillsevery prooflink resolvesdist/pages, sitemap,feed, 404Request path, on VercelPeopleGET a pageany routestatic HTMLwords withoutJavaScripthydratethe sameReact treeAgentsPOST /api/mcpJSON-RPCvalidatearguments vsJSON Schemabearer tokenowner toolsonlyrun toolJSON resultRésumé files/api/resumeslug or speccompose, fitapprovedblocks onlyrenderPDF, Word,MarkdownExports/api/portfolioJSON export/llms.txtplain-textguidegate: stops the build or the callrecords onlyHow this site is built and servedOne set of TypeScript data files feeds two paths. Build path, npm run build: check-words fails on banned terms and em dashes; check-links records which outbound links answer and never fails; tsc type-checks the site and the API; vite builds the client and server bundles; prerender writes HTML for every route; check-skills fails when a proof link does not resolve; the output is dist with pages, sitemap, feed and a 404 page. Request path on Vercel: people get static HTML that React hydrates; agents POST JSON-RPC to /api/mcp, arguments are validated against JSON Schema, owner tools need a bearer token, and the tool returns JSON; /api/resume composes approved blocks, fits them to the page and renders PDF, Word or Markdown; /api/portfolio and /llms.txt export the same data.src/data/*.ts, one set of factsrésumé blocks, case files, skills, profileBuild pathnpm run build, on every deploycheck-wordsbanned terms,em dashescheck-linksrecords whichlinks answertscsite and APItypesvite buildclient andserver bundlesprerenderHTML forevery routecheck-skillsevery prooflink resolvesdist/pages, sitemap,feed, 404Request path, on VercelPeopleGET a pageany routestatic HTMLwords withoutJavaScripthydratethe sameReact treeAgentsPOST /api/mcpJSON-RPCvalidatearguments vsJSON Schemabearer tokenowner toolsonlyrun toolJSON resultRésumé files/api/resumeslug or speccompose, fitapprovedblocks onlyrenderPDF, Word,MarkdownExports/api/portfolioJSON export/llms.txtplain-textguidegate: stops the build or the callrecords only
The build path turns the data files into prerendered pages and stops at each gate; the request path serves the same data to people and agents through Vercel Functions. Drawn from package.json and vercel.json, not measured.

How it works

  1. Résumés from approved blocks

    src/data/resume.ts holds every summary, skills line and bullet with its approved phrasings. A spec names block ids, never text: validateSpec rejects unknown ids, composeResume orders the sections, and the layout steps the type size down until the page budget fits. server/render-pdf.ts draws the plan with pdf-lib's built-in Helvetica and fixed dates, so the same spec gives the same bytes; Word and Markdown come from the same resolved résumé. The builder on /resumes runs the same layout code.

  2. Tailoring that cannot write

    build_tailored_resume counts which terms from my attested vocabulary a posting asks for, scores each role edition by whether it demonstrates a term in a bullet or only lists it in skills, and starts from the winner. The posting is data: none of its words reach the document, a job URL is hashed and never fetched, and the report names the blocks that would supply a missing term.

  3. An MCP server in one stateless function

    api/mcp.ts hands each POST to server/mcp/server.ts, which answers initialize, ping, tools/list and tools/call over Streamable HTTP with no session. Arguments are validated against each tool's JSON Schema. Portfolio and résumé tools are open; job preferences and the application tracker refuse any caller without my bearer token, compared as hashes in constant time.

  4. Prerendered, then hydrated

    scripts/prerender.ts renders every route with React's prerender API from the same App the browser runs, with its own title, description, Open Graph tags and JSON-LD, plus sitemap.xml, feed.xml and a 404 page. The browser hydrates that HTML; /llms.txt and /api/portfolio project the same data as text and JSON.

  5. A build that checks itself

    npm run build runs check-words (banned terms, em dashes, loose résumé files), check-links (records which outbound links answer), both TypeScript projects, the client and server builds, the prerender, and check-skills, which fails when a skill has no proof, a cited anchor is missing from the prerendered page, or a résumé skills line names a skill the site does not prove. The repository's agent skills add commands run before shipping: npm run verify (screenshots, axe, hydration and layout shift in both themes), audit:evidence, lint:casefiles and smoke.

What was hard

Deciding how much an agent may write. A tool that drafts bullets would match more postings, and would also put sentences on my résumé that I never wrote. I chose selection only: callers pass block and phrasing ids, the spec has no field for text, and the posting is only terms matched against a fixed vocabulary. The trade-off is reach: when a posting asks for something none of my blocks demonstrates, the report marks the term missing instead of writing a sentence that claims it.

Limits. The unit tests were added in November 2026 and cover logic, not how the pages behave in a browser beyond the automated page checks. The repository is private, so this page shows excerpts read from the source at build time and links to live endpoints, not code. check-skills proves that each proof link resolves, not that the work behind it is good. Tailoring is term counting against my own vocabulary, not semantic matching, and has not been evaluated against a recruiter's judgement.

Evidence

MCP tools
18 tools in server/mcp/tools.ts on 11 Nov 2026: 13 open, 5 need the owner's bearer token. The console reads the live list.server/mcp/tools.ts; tools/list on /api/mcp
Protocol
Streamable HTTP, stateless, JSON responses; protocol versions 2025-06-18, 2025-03-26 and 2024-11-05; 1 to 20 JSON-RPC messages per POST; GET answers 405.server/mcp/server.ts
Résumés
7 editions, each as PDF, Word and Markdown, plus any spec built from the blocks; a posting may be up to 40,000 characters.src/data/resume.ts; server/tailor.ts; /resumes
Reproducible files
resume_version is rv_ plus 16 hex characters of a SHA-256 over the edition and the canonical spec; fixed PDF dates make one spec one file.server/resumes.ts; server/render-pdf.ts
Build gates
check-words, check-links (records, never fails), tsc on 2 projects, the client and server builds, prerender, check-skills.package.json, scripts.build
Agent skills
5 skills in .claude/skills: case-file-authoring, demo-capture, evidence-audit, release-check, site-verify; run as npm run verify, audit:evidence, lint:casefiles and smoke..claude/skills; package.json
Tests
Unit tests (vitest) cover the résumé engine, the MCP server, the JSON export, the skills data, the pond simulation and the redirect table; a GitHub Actions workflow runs them with the build, the audits and the page checks on each pull request.tests/; .github/workflows/ci.yml

Stack

  • TypeScript
  • React 19 with react-dom/static prerender
  • Vite and esbuild
  • Tailwind CSS
  • Vercel Functions (Node)
  • pdf-lib and docx
  • MCP over Streamable HTTP
  • onnxruntime-web
  • Playwright and axe-core (agent skills only)