How MCP Servers Let AI Agents Generate PDFs
MCP does not generate PDFs itself. Learn how agents discover a PDF tool, how the server creates the file, and how to build a safe implementation.
Direct answer: MCP does not render or generate PDFs by itself. It standardizes how an AI model discovers a server tool, validates its input, invokes it, and receives a result. The MCP server owns the PDF capability: it can create the document with a local library or call a hosted PDF service, then return bytes, a path, or a downloadable reference.
This distinction matters when you design permissions, storage, error handling, and user approval. The model chooses and calls a tool; your server decides what content is accepted, where files are written, and how the PDF is produced.
What happens during PDF generation
- The MCP client connects to a server.
- The client requests the server’s available tools with
tools/list. - The server advertises a tool such as
create_pdf, including its description and input schema. That name is an example design; the official PDF example reviewed for this guide uses viewer and annotation tools instead. - The model selects the tool and the client sends a
tools/callrequest. - The server validates the arguments, creates or delegates creation of the PDF, and returns a result or a reference to the file.
- The client displays the result and, for sensitive operations such as writing or replacing a file, gives the user a chance to approve or deny the call.
Tool discovery and invocation are defined by the MCP tools specification. MCP’s security guidance assigns least-privilege and input-validation work to the server and visibility and consent work to the client.
A minimal architecture
AI model
│ discovers tools/list and chooses create_pdf
▼
MCP client ── tools/call ──► MCP PDF server
│
├─ validates content and options
├─ renders with a local PDF library
│ or calls a hosted document service
├─ stores output in an allowed location
└─ returns bytes or a reference
The protocol does not prescribe the PDF library, storage backend, page layout engine, fonts, or output transport. Those are implementation decisions.
Build a local PDF tool in Python
The following example uses the Python MCP SDK’s FastMCP server and ReportLab for a small text-to-PDF tool. Pin compatible client, server, transport, and SDK versions in your project; the 2026-07-28 protocol release included breaking changes, so consult the SDK migration guidance before deploying.
1. Install dependencies
python -m venv .venv
. .venv/bin/activate
pip install "mcp" "reportlab"
2. Save the server as pdf_server.py
from pathlib import Path
from typing import Annotated
from mcp.server.fastmcp import FastMCP
from reportlab.lib.pagesizes import A4
from reportlab.pdfgen import canvas
mcp = FastMCP("local-pdf")
OUTPUT_ROOT = Path("./generated-pdfs").resolve()
OUTPUT_ROOT.mkdir(parents=True, exist_ok=True)
def safe_output_path(filename: str) -> Path:
name = Path(filename).name
if not name.lower().endswith(".pdf"):
name += ".pdf"
path = (OUTPUT_ROOT / name).resolve()
if path.parent != OUTPUT_ROOT:
raise ValueError("Output path is outside the permitted directory")
return path
@mcp.tool()
def create_pdf(
title: Annotated[str, "Document title"],
content: Annotated[str, "Plain text to place in the PDF"],
filename: Annotated[str, "Filename inside the permitted output directory"],
overwrite: Annotated[bool, "Replace an existing file only when explicitly true"] = False,
) -> str:
"""Create a simple PDF from text and return its absolute path."""
if not title.strip():
raise ValueError("title must not be empty")
if not content.strip():
raise ValueError("content must not be empty")
output = safe_output_path(filename)
if output.exists() and not overwrite:
raise FileExistsError("File exists; set overwrite=true to replace it")
pdf = canvas.Canvas(str(output), pagesize=A4)
width, height = A4
pdf.setTitle(title[:200])
pdf.setFont("Helvetica-Bold", 16)
pdf.drawString(54, height - 60, title[:110])
pdf.setFont("Helvetica", 10)
y = height - 90
for paragraph in content.splitlines():
if y < 54:
pdf.showPage()
pdf.setFont("Helvetica", 10)
y = height - 54
pdf.drawString(54, y, paragraph[:115])
y -= 14
pdf.save()
return str(output)
if __name__ == "__main__":
mcp.run()
3. Connect an MCP client
Configure your client to launch the server over its supported local transport. A generic configuration has the same shape as:
{
"mcpServers": {
"local-pdf": {
"command": "/absolute/path/.venv/bin/python",
"args": ["/absolute/path/pdf_server.py"]
}
}
}
Client configuration keys differ between applications. Verify the client and SDK versions together, and use the client’s documented server configuration format.
4. Ask the agent to create a document
After discovery, a prompt such as “Create brief.pdf with this title and text” can cause the model to call create_pdf. The client should show the arguments and request confirmation before writing or overwriting a file.
Calling the protocol directly
MCP messages are normally exchanged over a negotiated transport such as stdio or HTTP. The conceptual tool call has this structure:
{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "create_pdf",
"arguments": {
"title": "Quarterly brief",
"content": "Revenue increased...",
"filename": "quarterly-brief.pdf",
"overwrite": false
}
}
}
Do not paste this JSON into an arbitrary HTTP endpoint and assume it is an MCP server. The transport, initialization handshake, capabilities, and authentication are negotiated by the client and server.
Local renderer or hosted PDF service?
| Decision | Local renderer in the MCP server | Hosted PDF service |
|---|---|---|
| Data boundary | Content can remain in your environment. | Content and metadata leave your environment according to the service’s contract. |
| Layout control | Direct control over fonts, layout, accessibility, forms, and output fidelity. | Depends on the provider’s API and templates. |
| Operations | You maintain libraries, fonts, sandboxing, storage, and upgrades. | The provider runs the rendering service; you manage credentials, failures, and retention. |
| Output handling | Return bytes or a server-side reference. | Return the provider’s response or a controlled download reference. |
| Approval | Protect local writes and replacements with explicit confirmation. | Protect external uploads, sharing, and retention decisions. |
These are architecture trade-offs, not a ranking of particular vendors. The reviewed sources do not establish a production-ready text-to-PDF service or compare PDF libraries.
Security checklist
- Scope filesystem access. Allow writes only beneath a dedicated output directory. Never accept an arbitrary absolute path from the model.
- Validate every argument. Limit content size, title length, page settings, filenames, URLs, and template identifiers on the server.
- Protect replacement. Require an explicit
overwritevalue and show the existing path to the user before replacement. - Make calls visible. The client should display the tool name and arguments and retain a way to deny invocation.
- Sanitize output references. Do not return secrets, internal paths, credentials, or unsanitized URLs.
- Rate-limit expensive work. PDF rendering can consume CPU, memory, fonts, and storage.
- Authenticate state handles. If a job spans several calls, use an opaque identifier with an expiry and check that the caller is authorized to use it.
- Review remote access. In remote HTTP mode, client filesystem roots do not automatically refer to the same filesystem as the server.
MCP tool descriptions are not a security boundary. The security guidance places access control and validation on the implementation and configuration.
The official PDF example is not a general generator
The official ext-apps PDF example demonstrates an interactive viewer and annotation workflow. Its documented tools include list_pdfs, display_pdf, interact, read_pdf_bytes, and save_pdf. It can navigate, search, extract pages, annotate, fill forms, and save annotated files. It should not be described as a server that creates arbitrary new documents from text.
That example also separates local and remote access. Local files can be supplied as command-line arguments or through enabled client roots. Remote HTTP mode ignores client roots by default because those paths would belong to the server’s filesystem; opting in requires an explicit flag. Inspect the configured roots and permissions before giving an agent file access.
State, jobs, and asynchronous generation
The 2026-07-28 protocol core is stateless. If your application needs a multi-step job, return an explicit identifier and require the model to send it back on later calls. Treat that identifier as untrusted: use opaque values, enforce authorization, set an expiration, and define what happens after a restart.
For long renders, a practical design is:
create_pdf_jobvalidates input and returns a job identifier.- A worker renders the document with a time and memory limit.
get_pdf_jobreturns pending, failed, or completed status.- The completed response contains a short-lived download reference rather than an unrestricted filesystem path.
Troubleshooting
| Error or symptom | Likely cause | Fix |
|---|---|---|
The agent cannot see create_pdf. |
Server failed during initialization, tool registration is conditional, or client and server versions are incompatible. | Run the server directly, inspect initialization logs, confirm the tool appears in tools/list, and check SDK migration notes. |
| “File exists” on every call. | The server protects existing files. | Use a new filename or require an explicit, user-approved overwrite flag. |
| Permission denied. | The process cannot write to the configured directory. | Create a dedicated writable directory and run with the minimum required account permissions. |
| Fonts or symbols are missing. | The renderer cannot find the requested font or does not embed it. | Install and register approved fonts, then verify embedding and licensing before deployment. |
| Pages are blank or truncated. | Content exceeds the layout logic, a page break is missing, or rendering timed out. | Implement wrapping and pagination, cap input size, and return a clear failure status. |
| Remote files are unavailable. | Client roots refer to the client machine, not the remote server. | Upload an approved input explicitly or configure the server’s permitted storage path. |
| The model repeatedly retries. | The tool returns an ambiguous error or no durable job status. | Return structured error categories, correlation IDs, and idempotent job behavior. |
Performance, reliability, and cost
- Measure render time by document size, page count, images, fonts, and JavaScript or template work.
- Set request, CPU, memory, and output-size limits before exposing the tool to an agent.
- Cache immutable inputs using a content hash, but never reuse a result across users without authorization checks.
- Use a queue for large jobs and make retries idempotent so a retry does not create duplicate files.
- Keep temporary files outside the public download directory and delete them on a defined schedule.
- Log tool name, validated parameters, duration, result status, and a request ID; redact document contents and secrets.
- Budget for the renderer, storage, network transfer, hosted-service calls, and model invocations separately.
Or skip the browser setup
If the PDF is a rendered webpage, ScreenshotNeo provides a one-call capture API and an MCP server. It accepts consent banners before capture, removes more than 60 known consent platforms plus newsletter popups and chat widgets, and lets you control each step. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers identify the page verdict and whether the request was billed. Its MCP tools include take_screenshot, get_page_info, and capture_pdf.
See the ScreenshotNeo API documentation for PDF options such as paper size, margins, landscape mode, and page ranges.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' }); const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Use the documented PDF option and a .pdf output filename when requesting a PDF. ScreenshotNeo includes full-page capture, element capture, custom CSS and JavaScript, waits, request blocking, headers, cookies, geolocation, caching, signed links, asynchronous jobs, bulk capture, and a usage API. Every feature is included on every plan. The Free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots.
Create a free ScreenshotNeo account and start with 1,000 screenshots each month without a card.
FAQ
Does MCP generate the PDF?
No. MCP standardizes discovery and invocation. The server or a service it calls performs PDF generation.
Can any MCP server create a PDF?
Only if it exposes a PDF-capable tool and has an implementation behind that tool. Tool names and capabilities are server-defined.
Should a PDF tool return bytes or a path?
Either can work. Bytes simplify small local results; a short-lived authorized reference is safer for large files and remote clients.
Is the official MCP PDF example a text-to-PDF generator?
No. It focuses on viewing, extracting, annotating, filling, and saving existing PDFs.
What should I verify before deployment?
Confirm protocol and SDK compatibility, filesystem boundaries, input limits, approval behavior, authentication, rate limits, retention, and failure recovery.


