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

ToolInputsWhat it does
pdf_inspectpdf_pathLists 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_fillpdf_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_mergepdf_paths (2 or more), output_path?Merges PDFs in the order given.
pdf_invoiceinvoice (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_usagenoneTier, 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

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:

Locally, absolute paths remain the better choice, since they keep PDF bytes out of the model context entirely.

What it does not do

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.