Claude is excellent at writing resume content and terrible at handing you a finished PDF, because generating a compiled document is not something a language model does on its own. That gap is exactly what the Model Context Protocol was built to close. With a small MCP server you define a tool, Claude calls it with structured resume data, your code renders a real PDF, and the file comes back to the user. This is a developer walkthrough of how to build that server: the tool schema, the rendering step, and how the file gets served back. It assumes you are comfortable with a bit of Python and want the architecture, not a copy-paste blob.
Why MCP is the right tool for this
MCP is an open standard that lets an AI client, Claude Desktop, the API, or another host, discover and call tools you expose from a server. Instead of asking Claude to describe a PDF it cannot make, you give it a tool called something like generate_resume, declare the shape of the data it takes, and let Claude fill that data from a conversation. The model handles language and judgment; your server handles deterministic document generation. That separation is the whole point, and it is why MCP matters for the job hunt more broadly.
The architecture in one picture
Four moving parts, in order:
- Tool definition. A schema describing the resume data your tool accepts (name, contact, experience, skills, and so on).
- Handler. The function that runs when Claude calls the tool. It validates the input and kicks off rendering.
- Renderer. The step that turns structured data into a PDF, via LaTeX or HTML to PDF.
- Return path. How the finished file gets back to the user, usually a hosted URL.
Step 1: define the tool and its schema
The schema is the contract between Claude and your code. Keep it explicit, because the clearer the shape, the more reliably Claude fills it. A resume tool typically accepts a structured object: a header block (name, email, phone, links), a list of experience entries (company, title, dates, bullet points), an education list, and a skills list. Mark the genuinely required fields as required, give every field a plain description, and prefer arrays of objects over free-form strings so the renderer gets clean data.
In the Python MCP SDK you register the tool with its name, a human-readable description Claude uses to decide when to call it, and a JSON Schema for the input. The description is not decoration: it is how the model knows this tool exists to produce a resume PDF, so write it like an instruction, for example "Generate a print-ready resume PDF from structured resume data and return a download URL."
Step 2: write the handler
The handler is where control returns to your code. Do three things in it, in order.
- Validate. Never trust the incoming payload blindly. Confirm the required fields exist and coerce types. A model-filled object is usually good but not guaranteed.
- Render. Pass the validated data to your renderer and get back a file path or bytes.
- Return a result the client can use. Hand back a URL or resource, not the raw internal path, unless you are running locally.
Keep the handler thin. Its job is orchestration, not formatting logic. Push the actual document work into the renderer so you can test and swap it independently.
Step 3: render the PDF
This is the decision that shapes everything downstream, and it comes down to LaTeX versus HTML to PDF.
LaTeX: print quality and clean extraction
LaTeX produces the sharpest typography and, done right, a PDF whose text extracts cleanly in order, which is exactly what an applicant tracking system needs. The flow is: fill a template with the resume data using a templating engine, write the resulting source to a temporary directory, run the LaTeX compiler, and read back the PDF. Two practical notes from running this in production: compile twice if your template uses references or precise spacing that settles on a second pass, and always clean up the temporary directory in a finally block so failed compiles do not leave junk behind. The cost is that you need a LaTeX toolchain installed wherever the server runs, which matters for deployment.
HTML to PDF: fast to build, easy to style
The alternative is to render an HTML template with CSS and convert it to PDF with a headless browser or a library that wraps one. You get to style with CSS you already know and skip the LaTeX install. The trade-off is less precise typographic control and a real risk of ATS-unfriendly output if you reach for multi-column layouts or background images. If you go this route, build a single-column, semantic template and test that the exported text copies out cleanly.
Whichever renderer you choose, run the extraction test: open the generated PDF, select all, copy, and paste into a plain text editor. If the text comes out clean and in reading order, an ATS can parse it. If it scrambles, no amount of visual polish will save the application. This is the same test covered in passing AI resume screening.
Step 4: get the file back to the user
MCP tools return content to the client, and for a binary like a PDF you have three sane options.
- Return a URL. Upload the PDF to storage (a bucket, or your own file endpoint) and return a link. This keeps the response small and lets the user click to download. It is the cleanest default, especially for a hosted server.
- Return a base64 resource. Encode the bytes and return them as an embedded resource. Fine for small files and local use, but it bloats the payload.
- Write to a known path. For a local stdio server, write the file to an output directory and return the path. Simple, but only works when client and server share a filesystem.
For anything beyond local experimentation, prefer the URL pattern. Generate a short-lived signed URL if the file is private, so the download link works for the user without exposing your storage.
Step 5: transport and connecting to Claude
MCP servers speak over a transport. For a local tool used from Claude Desktop, stdio is the standard: you register the server in the client config and it launches your process. For a hosted server that multiple clients reach over the network, use the HTTP or SSE transport and mount it behind your web framework. A common production shape is a FastAPI app that both exposes normal HTTP endpoints and mounts the MCP server at a path, so the same rendering logic serves an API and Claude at once. Once connected, Claude sees your generate_resume tool, and a user can simply ask it to build a resume and get a PDF link back.
Common mistakes to avoid
- Vague tool descriptions. If Claude does not reliably call your tool, the description is usually why. Make it explicit about what it does and when to use it.
- Trusting the payload. Validate. A missing required field should fail loudly, not produce a broken PDF.
- Ignoring ATS extraction. A beautiful PDF that scrambles on copy-paste defeats the entire purpose of a resume. Test extraction on every template.
- Returning giant base64 blobs for a hosted server. Use URLs; keep the protocol payload light.
- Leaving temp files behind. Clean up rendering artifacts in a finally block, or your disk fills up in production.
If you would rather not build it
Building your own MCP server is the right call when you want custom templates, full control of the data, or resume generation inside your own product. If you just want to use Claude to build and tailor resumes today, that pipeline already exists as a hosted service. ApplyJobFaster runs exactly this kind of setup, tailoring and compiling a clean resume PDF, and exposes it through MCP so you can connect it to Claude directly, which is walked through in connecting your resume to Claude with MCP. Build the server when you need the control; use the hosted one when you need the result.
