pdfops-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +53 -0
  2. package/dist/index.js +133 -0
  3. package/package.json +37 -0
package/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # pdfops-mcp
2
+
3
+ MCP server that gives AI agents deterministic PDF tools, backed by the [PDFops API](https://pdfops.dev): **inspect** AcroForm fields, **fill** forms, **merge** PDFs, and **generate invoices** — no Chromium, no native deps, nothing to host.
4
+
5
+ Tools operate on local file paths, so PDF bytes never transit the model context: your agent says *"fill /tmp/form.pdf and save to /tmp/out.pdf"* and gets a one-line confirmation back.
6
+
7
+ ## Install
8
+
9
+ **Claude Code**
10
+
11
+ ```bash
12
+ claude mcp add pdfops -- npx -y pdfops-mcp
13
+ ```
14
+
15
+ **Claude Desktop** (`claude_desktop_config.json`) / **Cursor** (`.cursor/mcp.json`)
16
+
17
+ ```json
18
+ {
19
+ "mcpServers": {
20
+ "pdfops": {
21
+ "command": "npx",
22
+ "args": ["-y", "pdfops-mcp"],
23
+ "env": { "PDFOPS_API_KEY": "pdfops_live_…" }
24
+ }
25
+ }
26
+ }
27
+ ```
28
+
29
+ `PDFOPS_API_KEY` is optional — without it you get the keyless trial (100 requests/IP/month). A free key (250/month, no card) takes one field at [pdfops.dev/pricing](https://pdfops.dev/pricing).
30
+
31
+ ## Tools
32
+
33
+ | Tool | What it does |
34
+ |---|---|
35
+ | `pdf_inspect` | List a PDF's form fields (names, types, options, values) + a paste-ready fill template. Call first on unfamiliar PDFs. |
36
+ | `pdf_fill` | Fill AcroForm fields → write the filled PDF. |
37
+ | `pdf_merge` | Merge ≥2 PDFs in order → write the result. |
38
+ | `pdf_invoice` | Structured data → complete invoice PDF. Deterministic: same input, byte-identical output. |
39
+ | `pdfops_usage` | Quota check for the configured key. |
40
+
41
+ ## Example agent flow
42
+
43
+ > "Fill the W-9 template at ~/docs/w9.pdf for Ada Lovelace and merge it with ~/docs/cover.pdf"
44
+
45
+ 1. `pdf_inspect` → discovers field names + fill template
46
+ 2. `pdf_fill` → writes the filled W-9
47
+ 3. `pdf_merge` → writes the combined packet
48
+
49
+ ## Links
50
+
51
+ API docs: [pdfops.dev/docs](https://pdfops.dev/docs) · OpenAPI: [pdfops.dev/openapi.json](https://pdfops.dev/openapi.json) · Typed client: [`pdfops-sdk`](https://www.npmjs.com/package/pdfops-sdk) · Questions: hello@pdfops.dev
52
+
53
+ MIT © PDFops
package/dist/index.js ADDED
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { readFile, writeFile } from "node:fs/promises";
5
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
+ import { z } from "zod";
8
+ import { PdfOps, PdfOpsError } from "pdfops-sdk";
9
+ var client = new PdfOps({
10
+ apiKey: process.env.PDFOPS_API_KEY,
11
+ baseUrl: process.env.PDFOPS_BASE_URL,
12
+ clientTag: "mcp"
13
+ });
14
+ var server = new McpServer({
15
+ name: "pdfops",
16
+ version: "0.1.0"
17
+ });
18
+ var errText = (e) => e instanceof PdfOpsError ? `PDFops API error ${e.status} (${e.code}): ${e.message}` + (e.code === "rate_limited" ? " \u2014 get a free API key (250/mo) at https://pdfops.dev/pricing and set PDFOPS_API_KEY" : "") : String(e);
19
+ server.tool(
20
+ "pdf_inspect",
21
+ "List a PDF's AcroForm form fields \u2014 names, types, options, current values \u2014 plus a paste-ready fillTemplate object for pdf_fill. A PDF with no form returns count 0. Call this FIRST when filling an unfamiliar PDF: you cannot fill fields whose names you do not know.",
22
+ { pdf_path: z.string().describe("Absolute path to the PDF to inspect") },
23
+ async ({ pdf_path }) => {
24
+ try {
25
+ const result = await client.inspect(await readFile(pdf_path));
26
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
27
+ } catch (e) {
28
+ return { content: [{ type: "text", text: errText(e) }], isError: true };
29
+ }
30
+ }
31
+ );
32
+ server.tool(
33
+ "pdf_fill",
34
+ `Fill AcroForm form fields in a PDF and save the result. Field names must exist in the PDF (use pdf_inspect first). All values are strings; checkboxes take "true"/"false"; dropdown/radio/optionlist values must be one of the field's options.`,
35
+ {
36
+ pdf_path: z.string().describe("Absolute path to the template PDF"),
37
+ fields: z.record(z.string()).describe("Field name \u2192 string value (from pdf_inspect's fillTemplate)"),
38
+ output_path: z.string().describe("Absolute path to write the filled PDF")
39
+ },
40
+ async ({ pdf_path, fields, output_path }) => {
41
+ try {
42
+ const bytes = await client.fillForm(await readFile(pdf_path), fields);
43
+ await writeFile(output_path, bytes);
44
+ return {
45
+ content: [
46
+ { type: "text", text: `Filled PDF written to ${output_path} (${bytes.byteLength} bytes)` }
47
+ ]
48
+ };
49
+ } catch (e) {
50
+ return { content: [{ type: "text", text: errText(e) }], isError: true };
51
+ }
52
+ }
53
+ );
54
+ server.tool(
55
+ "pdf_merge",
56
+ "Merge two or more PDFs into one, in the order given, and save the result.",
57
+ {
58
+ pdf_paths: z.array(z.string()).min(2).describe("Absolute paths of the PDFs to merge, in order"),
59
+ output_path: z.string().describe("Absolute path to write the merged PDF")
60
+ },
61
+ async ({ pdf_paths, output_path }) => {
62
+ try {
63
+ const inputs = await Promise.all(pdf_paths.map((p) => readFile(p)));
64
+ const bytes = await client.merge(inputs);
65
+ await writeFile(output_path, bytes);
66
+ return {
67
+ content: [
68
+ { type: "text", text: `Merged ${pdf_paths.length} PDFs into ${output_path} (${bytes.byteLength} bytes)` }
69
+ ]
70
+ };
71
+ } catch (e) {
72
+ return { content: [{ type: "text", text: errText(e) }], isError: true };
73
+ }
74
+ }
75
+ );
76
+ server.tool(
77
+ "pdf_invoice",
78
+ 'Generate a complete, professionally laid-out invoice PDF from structured data \u2014 no template needed. Deterministic: the same input produces byte-identical output (safe to re-run). Note: without a paid PDFops key the output carries a small "Generated with pdfops.dev" footer line.',
79
+ {
80
+ invoice: z.object({
81
+ from: z.union([
82
+ z.string(),
83
+ z.object({ name: z.string(), lines: z.array(z.string()).optional() })
84
+ ]),
85
+ to: z.union([
86
+ z.string(),
87
+ z.object({ name: z.string(), lines: z.array(z.string()).optional() })
88
+ ]),
89
+ items: z.array(
90
+ z.object({
91
+ description: z.string(),
92
+ quantity: z.number().positive().optional(),
93
+ unit_price: z.number().nonnegative()
94
+ })
95
+ ).min(1).max(100),
96
+ invoice_number: z.string().optional(),
97
+ date: z.string().optional().describe("Shown on the invoice; also pins metadata for determinism"),
98
+ due: z.string().optional(),
99
+ currency: z.string().regex(/^[A-Z]{3}$/).optional(),
100
+ tax_rate: z.number().min(0).max(100).optional(),
101
+ notes: z.string().max(1e3).optional()
102
+ }).describe("Invoice data"),
103
+ output_path: z.string().describe("Absolute path to write the invoice PDF")
104
+ },
105
+ async ({ invoice, output_path }) => {
106
+ try {
107
+ const bytes = await client.invoice(invoice);
108
+ await writeFile(output_path, bytes);
109
+ return {
110
+ content: [
111
+ { type: "text", text: `Invoice written to ${output_path} (${bytes.byteLength} bytes)` }
112
+ ]
113
+ };
114
+ } catch (e) {
115
+ return { content: [{ type: "text", text: errText(e) }], isError: true };
116
+ }
117
+ }
118
+ );
119
+ server.tool(
120
+ "pdfops_usage",
121
+ "Check the current PDFops API quota for the configured key: tier, limit, used, remaining, reset date. Requires PDFOPS_API_KEY.",
122
+ {},
123
+ async () => {
124
+ try {
125
+ const usage = await client.usage();
126
+ return { content: [{ type: "text", text: JSON.stringify(usage, null, 2) }] };
127
+ } catch (e) {
128
+ return { content: [{ type: "text", text: errText(e) }], isError: true };
129
+ }
130
+ }
131
+ );
132
+ var transport = new StdioServerTransport();
133
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "pdfops-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for the PDFops API \u2014 give AI agents deterministic PDF tools: inspect AcroForm fields, fill forms, merge PDFs, and generate invoices. Works with Claude Code, Claude Desktop, Cursor, and any MCP client.",
5
+ "keywords": [
6
+ "mcp",
7
+ "model-context-protocol",
8
+ "pdf",
9
+ "acroform",
10
+ "fill-pdf",
11
+ "merge-pdf",
12
+ "invoice",
13
+ "ai-agent",
14
+ "claude",
15
+ "cursor"
16
+ ],
17
+ "homepage": "https://pdfops.dev",
18
+ "bugs": "mailto:hello@pdfops.dev",
19
+ "author": "PDFops <hello@pdfops.dev> (https://pdfops.dev)",
20
+ "license": "MIT",
21
+ "type": "module",
22
+ "bin": {
23
+ "pdfops-mcp": "./dist/index.js"
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "README.md"
28
+ ],
29
+ "engines": {
30
+ "node": ">=18"
31
+ },
32
+ "dependencies": {
33
+ "@modelcontextprotocol/sdk": "^1.0.0",
34
+ "pdfops-sdk": "^0.3.0",
35
+ "zod": "^3.23.0"
36
+ }
37
+ }