·5 min read

How to let an AI agent generate PDFs with MCP

Run npx @docweave/mcp and point your agent's MCP config at it — that's the whole setup. Your agent gets a generate_pdf tool that takes HTML, a URL, or a template plus JSON and writes a PDF to a path you choose, with retries and idempotency handled for you instead of hand-rolled HTTP calls.

What MCP actually is

The Model Context Protocol (MCP) is a standard way for an AI agent to discover and call tools without you writing custom glue code for each one. Instead of teaching an agent your API's request shape, auth headers, and error handling, you run an MCP server that exposes a typed tool, and the agent's runtime handles argument validation, invocation, and returning the result into the conversation. If the term is new, our MCP server glossary entry has the longer definition — this post is about using one, not building one.

Docweave ships an open-source MCP server that wraps our PDF rendering pipeline as a single tool: generate_pdf. It's the same rendering engine behind our REST API docs and the approach we recommend on our PDF generation for AI agents page — the MCP server just changes how you call it, not what it does under the hood.

Adding the Docweave MCP server to an agent

If your agent runtime speaks MCP (Claude Desktop, Claude Code, or any client built on @modelcontextprotocol/sdk), you add the server with an npx command — no install step, no repo to clone:

{
  "mcpServers": {
    "docweave": {
      "command": "npx",
      "args": ["-y", "@docweave/mcp"],
      "env": {
        "DOCWEAVE_API_KEY": "dw_live_your_key"
      }
    }
  }
}

Restart the client and the agent sees a new tool, generate_pdf, with a schema describing its arguments: a source (an HTML string, a URL, or a stored template ID plus JSON data), an outputPath for where to write the file, and an optional idempotencyKey. Nothing needs to be pasted into the system prompt — the schema is self-describing, which is the actual point of MCP over a bag of curl snippets.

Calling generate_pdf

Once connected, the agent can call the tool directly as part of a plan — no separate "write the curl command" step:

generate_pdf({
  source: {
    type: "html",
    html: "<h1>Invoice #1042</h1><p>Total due: $480.00</p>"
  },
  outputPath: "/tmp/invoice-1042.pdf",
  idempotencyKey: "invoice-1042-v1"
})

The server renders with headless Chromium (see headless Chromium PDF rendering if you want the mechanics), writes the file to outputPath, and returns a small result object — file path, byte size, page count, and ok: true/false — back to the agent. The agent reads that result and moves to its next step: attach the file, email it, or hand it to another tool. That's the whole loop for turning, say, an invoice or a generated report into a real file an agent can act on.

Templates work the same way: pass source: { type: "template", templateId: "tpl_abc123", data: { ... } } instead of raw HTML if you've got a stored template with variables — useful when the agent is filling in a fixed layout (a receipt, a certificate) rather than generating markup from scratch.

Why this beats HTTP plumbing for an agent

You could give an agent the same capability by describing our REST endpoint in a system prompt and letting it construct HTTP calls. It works, but every layer you add is a place the agent can get it wrong or where you burn tokens re-explaining it:

  • Auth: the agent has to remember to attach Authorization: Bearer dw_live_... on every call, correctly, every time. A tool call just has the key baked into the server's environment once.
  • Binary handling: a raw PDF response is bytes, not text. An agent driving HTTP directly has to know to request Accept: application/pdf, buffer the response as binary, and write it to disk without the LLM ever trying to read the bytes as a string. The MCP tool does that conversion internally and just tells the agent a file exists at a path.
  • Retries and double-billing: if a step in the agent's plan fails after the PDF rendered but before the result was recorded, does the agent retry and generate the document (and get billed) twice? Passing the same idempotencyKey on the retry makes it a no-op that returns the original result instead of re-rendering.
  • Error shape: HTTP failure modes (502, timeout, malformed JSON) all look different and an agent has to special-case each. A tool call either succeeds or comes back with a clear ok: false and an error field — one shape to reason about.

None of this means the REST API is worse — for a normal backend service calling from Node, Python, or another server context, the REST integration is exactly right, and there are language-specific guides for Node.js, Python, and others. MCP earns its keep specifically in the agent context, where the caller is an LLM making tool-call decisions rather than a program with a fixed control flow.

What the REST call looks like, for comparison

If you want to see what the MCP server is doing on your behalf, here's the same invoice as a direct REST call. Note the Accept: application/pdf header and that the response body is saved as raw bytes, not parsed as JSON:

curl https://docweave.dev/api/v1/pdf \
  -X POST \
  -H "Authorization: Bearer dw_live_your_key" \
  -H "Content-Type: application/json" \
  -H "Accept: application/pdf" \
  -H "Idempotency-Key: invoice-1042-v1" \
  -d '{"source": {"type": "html", "html": "<h1>Invoice #1042</h1><p>Total due: $480.00</p>"}}' \
  --output invoice-1042.pdf
const res = await fetch("https://docweave.dev/api/v1/pdf", {
  method: "POST",
  headers: {
    Authorization: "Bearer dw_live_your_key",
    "Content-Type": "application/json",
    Accept: "application/pdf",
    "Idempotency-Key": "invoice-1042-v1"
  },
  body: JSON.stringify({
    source: { type: "html", html: "<h1>Invoice #1042</h1><p>Total due: $480.00</p>" }
  })
});

const buffer = Buffer.from(await res.arrayBuffer());
require("fs").writeFileSync("invoice-1042.pdf", buffer);

That's four things (auth header, accept header, idempotency header, binary buffering) an agent would otherwise need to get right on every single call. The MCP tool call collapses all of it into one argument object with a source and a path.

When to use which

Use the MCP server when an LLM agent is the thing deciding to generate a document — a support bot producing a receipt, a workflow agent assembling a contract or quote, or any setup where the model itself is choosing arguments at runtime. Use the plain REST API (or a no-code trigger like Zapier, Make, or n8n) when the caller is deterministic code or an automation platform with a fixed trigger, since there's no LLM in the loop to benefit from tool-call ergonomics. Both paths hit the same renderer, so output quality and pricing (see pricing) are identical either way — this is purely an integration-surface decision.

FAQ

Do I need to write my own MCP server to expose PDF generation to an agent?

No. Docweave publishes an open-source MCP server (@docweave/mcp) that you run with npx and point at your API key. It exposes a single generate_pdf tool covering HTML, URL, and template sources, so there's nothing to build unless you want custom tool names or additional wrapping logic.

Does the MCP server support the same sources as the REST API?

Yes. generate_pdf accepts the same three source types as POST /api/v1/pdf: raw HTML, a URL to render, or a stored template ID with JSON data. It's a thin wrapper over the same rendering pipeline, not a separate feature set.

How does idempotency work through MCP versus REST?

Both paths accept an idempotency key: the MCP tool call takes it as an idempotencyKey argument, the REST call takes it as an Idempotency-Key header. Either way, repeating the same key returns the original result instead of re-rendering and re-billing.

Is MCP faster or cheaper than calling the REST API directly?

No difference in render time or price. The document is priced and billed per render either way; MCP is purely an integration convenience for agent runtimes, not a different backend or pricing tier.

Related

Generate your first PDF in minutes.

Get an API key