PDF generation is one of those features that eats more time than it should. You pick a library, fight with layouts, figure out fonts, write the rendering logic, and wire it into your backend. It's boring plumbing, and it gets worse when you're working inside an AI coding agent like Claude Code or Cursor, because the agent can write your business logic but can't actually see or test the PDF it's building. You end up context-switching between your agent conversation, a code editor, and a PDF viewer.
MCP changes this. When your AI coding agent connects to a PDF API through the Model Context Protocol, it can create templates, render real documents, and write the integration code, all inside a single conversation. No tab switching. No copy-paste cycles.
This article walks through how that works with Transactional.dev's MCP server, using a concrete example: adding a PDF receipt to a Stripe checkout flow.
The problem: PDF generation in an AI-driven workflow
Here's how PDF generation typically goes when you're working with an AI coding agent:
- You ask the agent to add invoice generation to your app.
- The agent writes some code using a PDF library it knows about, or suggests one.
- You run the code. The PDF looks broken, or the layout isn't what you wanted.
- You describe the problem back to the agent. It can't see the PDF, so it guesses.
- You go back and forth three or four times, fixing margins and font sizes by describing pixels in words.
The core issue is that the agent is working blind. It generates code that produces a PDF, but it never sees the result. Every iteration requires you to be the eyes, translating visual output into text descriptions the agent can act on.
This is the same problem with any approach where the agent writes rendering code but can't test it. Whether you're using Puppeteer, wkhtmltopdf, or a library like jsPDF, the feedback loop is broken.
How MCP changes this
The Model Context Protocol lets an AI agent call external tools directly during a conversation. Instead of just generating code that you run later, the agent can execute API calls, inspect results, and iterate based on real output.
When you connect your agent to Transactional.dev's MCP server, it gets access to the full API:
create_documentto create a new PDF templateupdate_document_htmlto edit the template's HTML and Tailwind CSSgenerateto render a real PDF with sample data and get a URL backget_documentto read the current template state
This means the agent can design a template, render it, check the output, fix issues, and iterate, all without you leaving the conversation. The feedback loop that was broken is now closed inside the agent's own tool-calling capability.
A concrete example: "Add a PDF receipt to my Stripe checkout"
Let's walk through what happens when you give this prompt to Claude Code with the Transactional MCP server connected.
You say:
"Add a PDF receipt to my checkout flow. When a Stripe payment succeeds, generate a receipt PDF with the customer name, items purchased, amounts, and a total. Email it as an attachment."
The agent's process:
The agent doesn't just write code. It builds and tests the actual template, then writes the integration.
Step 1: Create the document
The agent calls create_document to create a new template in your Transactional.dev workspace. It gets back a documentId, which is the UUID it'll use for everything else.
Step 2: Build the HTML template
The agent calls update_document_html with an HTML template that uses Handlebars variables and Tailwind CSS for styling:
<div class="max-w-2xl mx-auto p-8 font-sans">
<div class="flex justify-between items-start mb-8">
<div>
<h1 class="text-2xl font-bold text-gray-900">Receipt</h1>
<p class="text-sm text-gray-500 mt-1">{{receiptNumber}}</p>
</div>
<div class="text-right text-sm text-gray-600">
<p>{{date}}</p>
</div>
</div>
<div class="mb-6">
<p class="font-medium text-gray-800">{{customerName}}</p>
<p class="text-sm text-gray-500">{{customerEmail}}</p>
</div>
<table class="w-full mb-6">
<thead>
<tr class="border-b border-gray-200 text-left text-sm text-gray-500">
<th class="pb-2">Item</th>
<th class="pb-2 text-right">Qty</th>
<th class="pb-2 text-right">Price</th>
</tr>
</thead>
<tbody>
{{#each items}}
<tr class="border-b border-gray-100">
<td class="py-2 text-sm">{{this.name}}</td>
<td class="py-2 text-sm text-right">{{this.quantity}}</td>
<td class="py-2 text-sm text-right">{{this.price}}</td>
</tr>
{{/each}}
</tbody>
</table>
<div class="flex justify-end">
<div class="text-right">
<p class="text-lg font-bold text-gray-900">Total: {{total}}</p>
</div>
</div>
</div>
Notice the Handlebars syntax: {{customerName}}, {{#each items}}, {{this.name}}. These are the variables that get filled in at generation time.
Step 3: Render a test PDF
The agent calls generate with sample data:
{
"documentId": "abc12345-...",
"variables": {
"receiptNumber": "REC-2025-0042",
"date": "May 30, 2025",
"customerName": "Jane Smith",
"customerEmail": "jane@example.com",
"items": [
{ "name": "Pro Plan (Annual)", "quantity": 1, "price": "$199.00" },
{ "name": "Extra Seats x3", "quantity": 3, "price": "$89.00" }
],
"total": "$466.00"
}
}
The API returns a URL to the rendered PDF. The agent can inspect the result and decide if the layout needs adjustments, like tweaking spacing, changing font sizes, or restructuring the table. If something's off, it calls update_document_html again, re-renders, and checks.
Step 4: Write the integration code
Once the template looks right, the agent writes the backend code directly into your project. For a Node.js app with Stripe webhooks, it might produce something like:
// src/webhooks/stripe.js
const handlePaymentSuccess = async (event) => {
const session = event.data.object;
const response = await fetch('https://api.transactional.dev/v1/generate', {
method: 'POST',
headers: {
'x-api-token': process.env.TRANSACTIONAL_API_TOKEN,
'Content-Type': 'application/json',
},
body: JSON.stringify({
documentId: process.env.RECEIPT_DOCUMENT_ID,
variables: {
receiptNumber: `REC-${session.id.slice(-8).toUpperCase()}`,
date: new Date().toLocaleDateString('en-US', {
year: 'numeric', month: 'long', day: 'numeric'
}),
customerName: session.customer_details.name,
customerEmail: session.customer_details.email,
items: session.line_items.data.map(item => ({
name: item.description,
quantity: item.quantity,
price: `$${(item.amount_total / 100).toFixed(2)}`,
})),
total: `$${(session.amount_total / 100).toFixed(2)}`,
},
}),
});
const pdf = await response.json();
// Attach pdf.url to your email service
await sendReceiptEmail(session.customer_details.email, pdf.url);
};
The agent writes this file, adds the environment variables to your .env.example, and updates your webhook handler to call it.
Why this works better than pasted snippets
When an agent generates PDF code without MCP, it's working from documentation it memorized during training. It might use outdated API signatures, hallucinate parameters, or produce templates it can't verify.
With MCP, the agent works against the real API:
- The template is real. It exists in your Transactional.dev workspace. You can open it in the dashboard and see it.
- The test PDF is real. The agent rendered an actual document with your sample data. It's not a guess.
- The API call is verified. The agent used the same endpoint and authentication it's writing into your code.
- Iterations happen on the spot. If the table layout is off, the agent fixes it and re-renders. You don't need to describe the problem.
The difference is between "here's some code that should work" and "here's code that I tested against the real API, and here's the PDF it produced."
Implementation notes
Setting up the MCP server
To connect your AI coding agent to Transactional.dev, add the MCP server configuration. For Claude Code, add this to your MCP settings:
{
"mcpServers": {
"transactional": {
"type": "streamable-http",
"url": "https://mcp.transactional.dev",
"headers": {
"Authorization": "Bearer YOUR_API_TOKEN"
}
}
}
}
Once connected, the agent can discover and call all available tools automatically.
Working with documentId
Every template in Transactional.dev has a UUID (documentId). When the agent creates a template via create_document, it gets this ID back and uses it for all subsequent operations: editing the HTML, rendering PDFs, and writing the integration code.
Store this ID as an environment variable in your project. The agent will typically do this for you as part of writing the integration code.
Variables and Handlebars
Templates use Handlebars for dynamic content. The basics:
{{variableName}}for simple values{{#each items}}...{{/each}}for loops{{#if condition}}...{{/if}}for conditionals
The agent knows Handlebars syntax and will use it correctly when building templates. The variables you pass in the generate call must match what the template expects.
The generate API
The actual API call is straightforward:
curl -X POST 'https://api.transactional.dev/v1/generate' \
-H 'x-api-token: YOUR_API_TOKEN' \
-H 'Content-Type: application/json' \
-d '{"documentId": "YOUR_DOCUMENT_UUID", "variables": {}}'
It returns a URL to the generated PDF. No Chromium binary to manage, no headless browser to configure. The rendering happens on Transactional.dev's infrastructure with a hosted CDN for assets and Google Fonts support built in.
Getting started
The workflow is simple: connect the MCP server, give your agent a task that involves PDF output, and let it handle the template creation, testing, and integration code.
You don't need to learn a new templating language or DSL. If your agent can write HTML and Tailwind (and it can), it can build PDF templates. The MCP connection just gives it the tools to actually create and test those templates instead of guessing at the output.
You can start with the free tier and generate your first PDF from an HTML template in a few minutes. Connect the MCP server to your coding agent, and the next time you need an invoice, receipt, report, or any generated document, just ask for it in the conversation. The agent will handle the rest.



