Guide
Giving an AI agent PDF tools over MCP, and why it has to inspect before it fills
MCP (Model Context Protocol) lets an agent call a tool directly instead
of you writing the client code that calls an API on the agent's
behalf. For PDF forms that convenience runs into the same problem
pypdf developers hit by hand: a fill call needs the form's exact
field names, and an agent guessing at them from a filename or a
user's request gets it wrong in the same silent way a person does.
pdfops-mcp handles this by shipping a tool whose entire
job is answering "what fields does this PDF actually have," and
documenting, in the tool description itself, that it has to be
called before pdf_fill. An agent reads tool
descriptions as instructions, not just documentation, so that
ordering is enforced in the one place the agent actually reads.
What pdf_inspect gives the agent
pdf_inspect takes a PDF source and returns its AcroForm
fields: name, type, options for a dropdown or radio group, current
value, and a per-field maxLength where the PDF declares
one. It also returns a fillTemplate object shaped
exactly like what pdf_fill expects, so the agent has a
paste-ready starting point instead of assembling one from the field
list by hand. Run against a W-9 template, the response for one field
looks like this:
{
"name": "name",
"type": "text",
"maxLength": 60,
"value": ""
},
{
"name": "tax_classification",
"type": "radio",
"options": ["individual", "c_corp", "s_corp", "partnership", "llc", "other"],
"value": ""
}
Neither of those names is guessable from "the W-9 form." A model
filling this blind would reach for something like full_name
or entity_type, both of which are wrong for this
specific template and would fail the same way a hand-written script
fails on a field name that doesn't exist. The tool description on
pdf_fill spells this out directly: field names must
exist in the PDF, values over a field's maxLength are
rejected, and checkboxes take a plain "true" or
"false" string rather than whatever on-state name the
PDF happens to use internally.
Wiring it in
For Claude Code, adding the server is one command:
claude mcp add pdfops -- npx -y pdfops-mcp
Claude Desktop and Cursor take the same package through a config file instead:
{
"mcpServers": {
"pdfops": {
"command": "npx",
"args": ["-y", "pdfops-mcp"],
"env": { "PDFOPS_API_KEY": "pdfops_live_…" }
}
}
}
The API key is optional. Without one the server runs on the keyless
trial (100 requests per IP per month); a free key from
/docs/signup raises that to 250 a month
with no card. Either way the agent now has five tools:
pdf_inspect, pdf_fill,
pdf_merge, pdf_invoice, and
pdfops_usage for checking remaining quota mid-session.
A two-step request, three tool calls
Ask the agent to "fill the W-9 at ~/docs/w9.pdf for the contractor
and merge it with the signed cover sheet," and the tool sequence is
pdf_inspect to read back the real field names,
pdf_fill with those names to write the filled copy,
then pdf_merge to combine it with the cover sheet in
order. All three take plain file paths when the server runs next to
the agent. npx pdfops-mcp under Claude Code or Claude
Desktop starts as a local process over stdio, so it reads and writes
the same filesystem the agent does, and the PDF bytes themselves
never pass through the model's context window at all. The agent
only ever sees field names, a confirmation string, and a path.
The part that breaks on a hosted server
That local-path convenience assumes the MCP server and the agent
share a filesystem. Run the same server on a hosted gateway, such as
Smithery, Glama's hosted mode, or a cloud IDE's remote MCP client,
and ~/docs/w9.pdf stops meaning anything, because it
names a path on the machine the agent is running on, not the
machine the server is running on. A file path handed to a remote
server just fails to open. pdfops-mcp's
pdf_path and pdf_paths arguments accept an
https:// URL or a data:application/pdf;base64,…
URI as well as a local path, and every tool that produces a PDF
returns it inline as an embedded resource whenever
output_path is omitted, rather than assuming there's
somewhere on that machine worth writing it. Nothing in the config
changes between local and hosted mode: the server just checks what
kind of string it got handed and resolves it accordingly.
Try it
The same pdf_inspect → pdf_fill →
pdf_merge sequence is also three plain HTTP calls if
you'd rather wire an agent up yourself:
/api/inspect,
/api/fill-form, and
/api/merge, all under one
X-API-Key header from a free key at
/docs/signup. But for anything already
running inside Claude Code, Claude Desktop, or Cursor,
npx -y pdfops-mcp skips writing that client entirely:
the agent just gets the tools.