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_inspectpdf_fillpdf_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.