Guide
An MCP server that fills PDF forms
pdfops-mcp is a Model Context Protocol server that lets an
AI agent fill PDF forms. It gives the agent five tools: read a form's
field names (pdf_inspect), fill them (pdf_fill),
merge PDFs (pdf_merge), draw an invoice from data
(pdf_invoice) and check quota (pdfops_usage).
It runs next to the agent via npx -y pdfops-mcp, works
with no API key for 100 requests a month, and does the PDF work on the
hosted PDFops API, so there is no Chromium, pdftk or native library to
install.
Install
The package is pdfops-mcp on npm (MIT, Node.js 20+, source at github.com/pdfops/pdfops-mcp), listed in the official MCP registry as dev.pdfops/pdfops-mcp. Every client below launches the same command, npx -y pdfops-mcp, as a local stdio process. The API key line is optional everywhere.
Claude Code
claude mcp add pdfops -- npx -y pdfops-mcp
# with a key
claude mcp add pdfops -e PDFOPS_API_KEY=pdfops_live_... -- npx -y pdfops-mcp
Claude Desktop
Edit claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/, Windows: %APPDATA%\Claude\), then restart the app:
{
"mcpServers": {
"pdfops": {
"command": "npx",
"args": ["-y", "pdfops-mcp"],
"env": { "PDFOPS_API_KEY": "pdfops_live_..." }
}
}
}
Cursor
Same shape, in ~/.cursor/mcp.json for every project or .cursor/mcp.json for one:
{
"mcpServers": {
"pdfops": {
"command": "npx",
"args": ["-y", "pdfops-mcp"],
"env": { "PDFOPS_API_KEY": "pdfops_live_..." }
}
}
}
VS Code (Copilot agent mode)
VS Code uses a servers key and an explicit type, in .vscode/mcp.json:
{
"servers": {
"pdfops": {
"type": "stdio",
"command": "npx",
"args": ["-y", "pdfops-mcp"],
"env": { "PDFOPS_API_KEY": "pdfops_live_..." }
}
}
}
Any other client that takes a command and arguments (Windsurf, Cline, Zed, a custom agent built on an MCP SDK) takes the same two values: command npx, arguments -y pdfops-mcp.
The five tools
| Tool | Inputs | What it does |
|---|---|---|
pdf_inspect | pdf_path | Lists the form's AcroForm fields: name, type, options, current value, maxLength where declared, plus a paste-ready fillTemplate and a hasXFA flag. Read-only. |
pdf_fill | pdf_path, fields, output_path?, flatten? | Fills fields by name and writes the result. flatten bakes the values into the page and removes the form. |
pdf_merge | pdf_paths (2 or more), output_path? | Merges PDFs in the order given. |
pdf_invoice | invoice (from, to, items, dates, currency, tax), output_path? | Draws a complete invoice PDF from data. Same input, byte-identical output. See JSON to invoice PDF. |
pdfops_usage | none | Tier, limit, used, remaining and reset date for the configured key. |
Field values are strings. Checkboxes take "true" or "false", whatever on-state name the PDF uses internally. Dropdown and radio values must be one of the options pdf_inspect returned, and a text value longer than the field's maxLength is rejected instead of silently cut.
A fill, tool call by tool call
Below is real output from pdfops-mcp 0.3.2 against the sample invoice-template.pdf. The agent is asked to fill it for Acme Co. First it inspects:
pdf_inspect { "pdf_path": "/home/me/invoice-template.pdf" }
{
"count": 2,
"hasXFA": false,
"fields": [
{ "name": "customer_name", "type": "text", "value": "", "readOnly": false },
{ "name": "total", "type": "text", "value": "", "readOnly": false }
],
"fillTemplate": { "customer_name": "", "total": "" }
}
Then it fills with the names it just read back:
pdf_fill {
"pdf_path": "/home/me/invoice-template.pdf",
"fields": { "customer_name": "Acme Co", "total": "$1,250.00" },
"output_path": "/home/me/out.pdf"
}
Filled /home/me/invoice-template.pdf written to /home/me/out.pdf (4687 bytes)
Had it guessed a name instead of inspecting, the call fails loudly and names the missing field, so the agent can correct itself on the next turn:
pdf_fill { "pdf_path": "...", "fields": { "customer": "Acme Co" } }
PDFops API error 400 (unknown_field): unknown_field: no field named "customer" (isError: true)
That ordering, inspect then fill, is written into the tool descriptions themselves, because an agent reads a tool description as an instruction. The longer argument is in why an agent has to inspect before it fills.
Prompts that work
- "What fields does ~/forms/fw9.pdf have?" calls
pdf_inspectonly. - "Fill ~/forms/fw9.pdf for Ada Lovelace, 12 Analytical Way, London, and save it as ~/out/w9-ada.pdf, flattened." calls
pdf_inspect, thenpdf_fillwithflatten: true. - "Fill the onboarding form for each row of contractors.csv into ~/out/" inspects once, then calls
pdf_fillonce per row. - "Merge ~/out/w9-ada.pdf, ~/docs/nda.pdf and ~/docs/cover.pdf into one packet, cover first." calls
pdf_mergewith the order stated. - "Invoice Globex for 3 days of consulting at $650, 8.5% tax, due in 30 days." calls
pdf_invoice.
Many government blanks are useful test inputs; the form compatibility registry lists which real forms (IRS W-9, W-4, 1040, California FL-100 and others) fill cleanly and which need decrypting first.
Running it on a hosted MCP runtime
On Smithery, Glama's hosted mode or a cloud IDE gateway, the server runs on a machine where your paths do not exist. Pass sources it can reach and leave out output_path:
- Inputs (
pdf_path,pdf_paths): anhttps://URL the server can fetch, or adata:application/pdf;base64,…URI for small files. - Outputs: without
output_path,pdf_fill,pdf_mergeandpdf_invoicereturn the PDF inline as an embeddedapplication/pdfresource for the client to save.
Locally, absolute paths remain the better choice, since they keep PDF bytes out of the model context entirely.
What it does not do
- It is not local-only. Each call sends the PDF to the PDFops API. Sensitive documents that must stay on the machine need a local library, not a hosted API.
- It fills forms that have AcroForm fields. A flat, scanned or print-only PDF has nothing to fill; add fields once with the free make-fillable tool, then fill it repeatedly.
- It does not read a filled PDF's meaning, extract data from scans, or sign documents. It inspects, fills, merges and draws invoices.
Frequently asked
Which MCP clients does pdfops-mcp work with?
Any client that can launch a stdio MCP server: Claude Code, Claude Desktop, Cursor, VS Code, Windsurf, Cline and others. The command is always npx -y pdfops-mcp; only the config file that holds it differs per client.
Do I need an API key?
No. Without PDFOPS_API_KEY the server runs on the keyless trial, 100 requests per IP per month. A free key raises that to 250 a month with no card, from /pricing. Paid tiers are 4,000 and 25,000 requests a month.
Does the PDF pass through the model's context window?
Not when the server runs locally. Tools take file paths, the server reads and writes the files itself, and the agent only sees field names, a confirmation line and the output path. The PDF bytes go from your disk to the PDFops API and back to your disk.
Can it fill XFA forms?
No. It fills AcroForm fields. pdf_inspect reports hasXFA; a hybrid AcroForm plus XFA PDF fills through its AcroForm layer and loses the XFA layer, and a pure XFA form has no AcroForm fields to fill. Encrypted PDFs are rejected with the exact qpdf --decrypt command to run first.
Does my PDF leave my machine?
Yes. The fill runs on the PDFops API at pdfops.dev, which processes the request in memory and does not store documents (see the privacy policy). If a document must never leave the machine, use a local library such as pdf-lib or pypdf instead of any hosted MCP server.
Does it work on Smithery or other hosted MCP runtimes?
Yes, with URLs instead of paths. On a hosted runtime your local paths do not exist, so pass an https:// URL or a data:application/pdf;base64 URI as the source and omit output_path; the result comes back inline as an application/pdf resource.
← PDFops home · API docs · JSON to invoice PDF · Inspect before you fill