To let Claude generate PDFs, connect it to a hosted PDF service, because Claude outputs text and cannot emit a binary file by itself. The fastest path is MCP: add the PDF4.dev MCP server at https://pdf4.dev/api/mcp to Claude Desktop or Claude Code, and Claude can render PDFs directly in a chat. To build PDF generation into your own app, define a generate_pdf tool in an Anthropic API call and have your code POST to PDF4.dev when Claude requests it. PDF4.dev renders the HTML with headless Chromium (Playwright) server-side, so there is no browser to install.
This guide covers both paths with working code, and a decision table to pick between them.
Which way should you connect Claude to a PDF API?
There are two real ways to give Claude PDF generation, and they solve different problems. MCP wires Claude to a running PDF server once, so the tools show up in every conversation, ideal for interactive work in Claude Desktop or Claude Code. Tool use embeds PDF generation inside an app you build on the Anthropic API, where your own code runs the request. The table compares them across the criteria that decide the choice.
| Criterion | MCP server | Anthropic tool use |
|---|---|---|
| Where it runs | Claude Desktop / Claude Code | Your app (server-side Anthropic SDK) |
| Setup | One config entry, then restart | Tool definition + tool_result loop in code |
| Who executes the render | PDF4.dev MCP server | Your code POSTs to PDF4.dev |
| Best for | Interactive chats, ad hoc documents | Production apps, automated pipelines |
| Tools exposed | render_pdf, create_template, list_templates, and more | Whatever tools you define |
| API key location | Claude Desktop config | Your server environment |
| Code to write | None | ~40 lines (tool def + loop) |
BLUF for choosing: if you want Claude itself to make PDFs while you chat, use MCP. If you are shipping a feature where Claude generates documents for your users, use tool use. The two are not exclusive, you can prototype over MCP and ship over the API.
How do you add PDF4.dev as an MCP server in Claude?
Add the PDF4.dev MCP endpoint to your client config, then restart. MCP (the Model Context Protocol) is an open standard from Anthropic that lets a Claude client connect to external tool servers. PDF4.dev runs one at https://pdf4.dev/api/mcp over Streamable HTTP, exposing tools like render_pdf, create_template, and list_templates. Once connected, Claude can render a PDF inside the conversation without any code from you.
You need a PDF4.dev API key (format p4_live_...) from your dashboard settings. The key authenticates the render calls; your Anthropic subscription covers the model.
Claude Desktop
Edit claude_desktop_config.json (Settings, Developer, Edit Config) and add an mcpServers entry. Remote Streamable HTTP servers are bridged with the mcp-remote helper, which passes your Bearer token through:
{
"mcpServers": {
"pdf4": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://pdf4.dev/api/mcp",
"--header",
"Authorization: Bearer p4_live_your_key_here"
]
}
}
}Restart Claude Desktop. A tools icon appears in the message box; ask Claude to "create a PDF invoice for Acme Corp, total 1500 dollars" and it calls render_pdf. The render runs on PDF4.dev's Chromium, and Claude hands you back a download link.
Claude Code
Claude Code connects to remote MCP servers from the command line, no JSON editing needed:
claude mcp add --transport http pdf4 https://pdf4.dev/api/mcp \
--header "Authorization: Bearer p4_live_your_key_here"Run claude mcp list to confirm the connection. From then on, any Claude Code session can render PDFs, generate reports from a repo, or turn Markdown into a styled document, by calling the PDF4.dev tools.
Keep your PDF4.dev key out of shared configs. Use a render_only-scoped key for MCP setups so a leaked config cannot delete your templates, only render PDFs.
How do you define a PDF tool in the Anthropic API?
Add a tool to the tools array of your Messages API call, then run the tool-use loop. Anthropic tool use has a fixed three-part shape: you declare tools with a name, description, and JSON Schema input_schema; Claude responds with a tool_use block when it wants to call one; you execute it and send the output back as a tool_result block in the next request. For PDFs, your tool handler POSTs to https://pdf4.dev/api/v1/render and returns the URL.
The tool definition tells Claude what it can generate. Keep the schema tight so Claude fills it correctly:
{
"name": "generate_pdf",
"description": "Render an HTML document to a PDF and return a download URL. Use this whenever the user asks for a PDF, invoice, report, certificate, or printable document.",
"input_schema": {
"type": "object",
"properties": {
"html": {
"type": "string",
"description": "Complete HTML for the document, including inline CSS."
}
},
"required": ["html"]
}
}When Claude calls this tool, your code reads tool_use.input.html, sends it to PDF4.dev with delivery: "url", and returns the link. Recommending delivery: "url" matters: a base64 PDF can be hundreds of kilobytes and bloats the context window, while a URL is a few hundred characters Claude can pass straight to the user.
How do you run the full tool-use loop in TypeScript or Python?
Loop until Claude stops calling tools, sending each tool_result back. The pattern is: call the model with your tools, check stop_reason, and when it is tool_use, execute every tool_use block, append the assistant turn plus a user turn of tool_result blocks, then call the model again. The examples below use claude-opus-4-8; the loop is identical across tool-capable Claude models, so the model id is a one-line swap.
Both versions define generate_pdf, POST to PDF4.dev when Claude calls it, and return the render URL.
import Anthropic from "@anthropic-ai/sdk";
const anthropic = new Anthropic(); // ANTHROPIC_API_KEY from env
const PDF4_KEY = process.env.PDF4_API_KEY!; // p4_live_...
const tools: Anthropic.Tool[] = [
{
name: "generate_pdf",
description:
"Render HTML to a PDF and return a download URL. Use for any invoice, report, or printable document.",
input_schema: {
type: "object",
properties: {
html: { type: "string", description: "Complete HTML with inline CSS." },
},
required: ["html"],
},
},
];
// Your tool handler: call PDF4.dev, ask for a URL not a binary
async function generatePdf(html: string): Promise<string> {
const res = await fetch("https://pdf4.dev/api/v1/render", {
method: "POST",
headers: {
Authorization: `Bearer ${PDF4_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ html, data: {}, delivery: "url" }),
});
const json = await res.json();
return json.url; // short-lived download link, valid 24h
}
const messages: Anthropic.MessageParam[] = [
{ role: "user", content: "Make a PDF invoice for Acme Corp, total $1,500." },
];
while (true) {
const response = await anthropic.messages.create({
model: "claude-opus-4-8",
max_tokens: 16000,
tools,
messages,
});
if (response.stop_reason !== "tool_use") {
// Final answer: Claude shares the URL with the user
for (const block of response.content) {
if (block.type === "text") console.log(block.text);
}
break;
}
messages.push({ role: "assistant", content: response.content });
const toolResults: Anthropic.ToolResultBlockParam[] = [];
for (const block of response.content) {
if (block.type === "tool_use" && block.name === "generate_pdf") {
const input = block.input as { html: string };
const url = await generatePdf(input.html);
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: `PDF ready: ${url}`,
});
}
}
messages.push({ role: "user", content: toolResults });
}The honest caveats: each PDF render adds latency (a Chromium render is roughly 300ms with a warm browser pool, plus network), so a chat that generates ten documents feels slower than one. Watch for tool loops where Claude regenerates the same document, cap iterations if you automate this. And validate the html Claude produces if it is going into a regulated document; the model writes good HTML but you own what ships.
How does PDF4.dev render the HTML Claude sends?
PDF4.dev runs headless Chromium server-side and returns a finished PDF. When Claude's tool call reaches POST https://pdf4.dev/api/v1/render, the service compiles any Handlebars {{variables}} against your data, loads the HTML into a pooled Chromium instance with Playwright, and calls the print-to-PDF API. The result is the same rendering engine that draws Chrome, so CSS, web fonts, flexbox, and grid behave exactly as they do in a browser.
This is the "no infrastructure" part. You are not installing Chromium in a Lambda layer, fighting serverless cold starts, or keeping a browser pool warm. The minimal request body is small:
curl -X POST https://pdf4.dev/api/v1/render \
-H "Authorization: Bearer p4_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"html": "<h1>Hello from Claude</h1>",
"data": {},
"delivery": "url"
}'The response carries { url, expires_at, size_bytes, duration_ms }. The url points to a signed, short-lived download link (24 hours) that Claude returns to the user. For repeated documents, create a template once with create_template and have Claude call render_pdf with just a template_id and data, which keeps the HTML out of every prompt. If you want to try Chromium-grade rendering by hand first, the free HTML to PDFTry it free tool uses the same engine.
Which option should you choose?
Pick by where Claude runs and who owns the render. Three scenarios cover most cases.
You want Claude to make PDFs while you work. Use MCP in Claude Desktop or Claude Code. One config entry, no code, and render_pdf is available in every chat. Best for drafting invoices, turning notes into reports, or generating documents from a codebase interactively.
You are building a product feature where Claude generates documents for users. Use Anthropic tool use. Define generate_pdf, run the loop on your server, and keep both API keys server-side. You control validation, logging, and rate limits. This is the path for SaaS document generation, automated email attachments, or agent workflows.
You are not sure yet. Prototype over MCP to feel out how Claude writes the HTML and what documents your users ask for, then port the same render_pdf call into a tool definition when you ship. The PDF4.dev render contract is identical across both, so nothing is wasted.
Across all three, recommend delivery: "url" so Claude handles a link instead of a binary, and use a render_only key wherever the credential might leak. For a deeper look at agent setups, see generating PDFs with AI agents over MCP, the ChatGPT equivalent of this guide, and designing PDFs with Claude.
Free tools mentioned:
Start generating PDFs
Build PDF templates with a visual editor. Render them via API from any language in ~300ms.



