pdfops-mcp 0.2.0 → 0.2.1

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/LICENSE +21 -0
  2. package/dist/index.js +125 -116
  3. package/package.json +11 -3
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 PDFops (pdfops.dev)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.js CHANGED
@@ -1,133 +1,142 @@
1
1
  #!/usr/bin/env node
2
- #!/usr/bin/env node
3
- import { readFile, writeFile } from "node:fs/promises";
4
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
- import { z } from "zod";
7
- import { PdfOps, PdfOpsError } from "pdfops-sdk";
2
+ // pdfops-mcp — MCP server exposing the PDFops API as agent tools.
3
+ //
4
+ // Design: tools take/return FILE PATHS, not base64 blobs. This server
5
+ // runs locally (npx pdfops-mcp) beside the agent, so the filesystem is
6
+ // the natural interface an agent says "fill /tmp/form.pdf and save
7
+ // to /tmp/out.pdf" and the PDF bytes never transit the model context.
8
+ //
9
+ // Env:
10
+ // PDFOPS_API_KEY optional — free key from https://pdfops.dev/pricing
11
+ // (250 req/mo; keyless works at 100 req/IP/mo)
12
+ // PDFOPS_BASE_URL optional — API origin override (testing)
13
+ import { readFile, writeFile } from 'node:fs/promises';
14
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
15
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
16
+ import { z } from 'zod';
17
+ import { PdfOps, PdfOpsError } from 'pdfops-sdk';
8
18
  const client = new PdfOps({
9
- apiKey: process.env.PDFOPS_API_KEY,
10
- baseUrl: process.env.PDFOPS_BASE_URL,
11
- clientTag: "mcp"
19
+ apiKey: process.env.PDFOPS_API_KEY,
20
+ baseUrl: process.env.PDFOPS_BASE_URL,
21
+ clientTag: 'mcp',
12
22
  });
13
23
  const server = new McpServer({
14
- name: "pdfops",
15
- version: "0.1.1"
24
+ name: 'pdfops',
25
+ version: '0.2.1', // keep in sync with package.json
16
26
  });
17
- const 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);
18
- server.tool(
19
- "pdf_inspect",
20
- "List a PDF's AcroForm form fields \u2014 names, types, options, current values, per-field maxLength where declared \u2014 plus a paste-ready fillTemplate object for pdf_fill and a hasXFA flag (hybrid AcroForm/XFA inputs lose their XFA layer when filled). 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, and values longer than a field's maxLength are rejected.",
21
- { pdf_path: z.string().describe("Absolute path to the PDF to inspect") },
22
- async ({ pdf_path }) => {
27
+ const errText = (e) => e instanceof PdfOpsError
28
+ ? `PDFops API error ${e.status} (${e.code}): ${e.message}` +
29
+ (e.code === 'rate_limited'
30
+ ? ' get a free API key (250/mo) at https://pdfops.dev/pricing and set PDFOPS_API_KEY'
31
+ : '')
32
+ : String(e);
33
+ server.tool('pdf_inspect', 'List a PDF\'s AcroForm form fields — names, types, options, current values, per-field maxLength where declared — plus a paste-ready fillTemplate object for pdf_fill and a hasXFA flag (hybrid AcroForm/XFA inputs lose their XFA layer when filled). 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, and values longer than a field\'s maxLength are rejected.', { pdf_path: z.string().describe('Absolute path to the PDF to inspect') }, async ({ pdf_path }) => {
23
34
  try {
24
- const result = await client.inspect(await readFile(pdf_path));
25
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
26
- } catch (e) {
27
- return { content: [{ type: "text", text: errText(e) }], isError: true };
35
+ const result = await client.inspect(await readFile(pdf_path));
36
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
37
+ }
38
+ catch (e) {
39
+ return { content: [{ type: 'text', text: errText(e) }], isError: true };
28
40
  }
29
- }
30
- );
31
- server.tool(
32
- "pdf_fill",
33
- `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; text values must respect the field's maxLength from pdf_inspect. Encrypted PDFs are rejected with decrypt advice (common for government blanks with an empty user password).`,
34
- {
35
- pdf_path: z.string().describe("Absolute path to the template PDF"),
36
- fields: z.record(z.string()).describe("Field name \u2192 string value (from pdf_inspect's fillTemplate)"),
37
- output_path: z.string().describe("Absolute path to write the filled PDF"),
38
- flatten: z.boolean().optional().describe("Bake values into page content and drop the AcroForm so fields are no longer editable")
39
- },
40
- async ({ pdf_path, fields, output_path, flatten }) => {
41
+ });
42
+ server.tool('pdf_fill', '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; text values must respect the field\'s maxLength from pdf_inspect. Encrypted PDFs are rejected with decrypt advice (common for government blanks with an empty user password).', {
43
+ pdf_path: z.string().describe('Absolute path to the template PDF'),
44
+ fields: z
45
+ .record(z.string())
46
+ .describe('Field name → string value (from pdf_inspect\'s fillTemplate)'),
47
+ output_path: z.string().describe('Absolute path to write the filled PDF'),
48
+ flatten: z
49
+ .boolean()
50
+ .optional()
51
+ .describe('Bake values into page content and drop the AcroForm so fields are no longer editable'),
52
+ }, async ({ pdf_path, fields, output_path, flatten }) => {
41
53
  try {
42
- const bytes = await client.fillForm(await readFile(pdf_path), fields, { flatten });
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 };
54
+ const bytes = await client.fillForm(await readFile(pdf_path), fields, { flatten });
55
+ await writeFile(output_path, bytes);
56
+ return {
57
+ content: [
58
+ { type: 'text', text: `Filled PDF written to ${output_path} (${bytes.byteLength} bytes)` },
59
+ ],
60
+ };
51
61
  }
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
+ catch (e) {
63
+ return { content: [{ type: 'text', text: errText(e) }], isError: true };
64
+ }
65
+ });
66
+ server.tool('pdf_merge', 'Merge two or more PDFs into one, in the order given, and save the result.', {
67
+ pdf_paths: z
68
+ .array(z.string())
69
+ .min(2)
70
+ .describe('Absolute paths of the PDFs to merge, in order'),
71
+ output_path: z.string().describe('Absolute path to write the merged PDF'),
72
+ }, async ({ pdf_paths, output_path }) => {
62
73
  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 };
74
+ const inputs = await Promise.all(pdf_paths.map((p) => readFile(p)));
75
+ const bytes = await client.merge(inputs);
76
+ await writeFile(output_path, bytes);
77
+ return {
78
+ content: [
79
+ { type: 'text', text: `Merged ${pdf_paths.length} PDFs into ${output_path} (${bytes.byteLength} bytes)` },
80
+ ],
81
+ };
82
+ }
83
+ catch (e) {
84
+ return { content: [{ type: 'text', text: errText(e) }], isError: true };
73
85
  }
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 }) => {
86
+ });
87
+ server.tool('pdf_invoice', 'Generate a complete, professionally laid-out invoice PDF from structured data — 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.', {
88
+ invoice: z
89
+ .object({
90
+ from: z.union([
91
+ z.string(),
92
+ z.object({ name: z.string(), lines: z.array(z.string()).optional() }),
93
+ ]),
94
+ to: z.union([
95
+ z.string(),
96
+ z.object({ name: z.string(), lines: z.array(z.string()).optional() }),
97
+ ]),
98
+ items: z
99
+ .array(z.object({
100
+ description: z.string(),
101
+ quantity: z.number().positive().optional(),
102
+ unit_price: z.number().nonnegative(),
103
+ }))
104
+ .min(1)
105
+ .max(100),
106
+ invoice_number: z.string().optional(),
107
+ date: z
108
+ .string()
109
+ .optional()
110
+ .describe('Shown on the invoice; also pins metadata for determinism'),
111
+ due: z.string().optional(),
112
+ currency: z.string().regex(/^[A-Z]{3}$/).optional(),
113
+ tax_rate: z.number().min(0).max(100).optional(),
114
+ notes: z.string().max(1000).optional(),
115
+ })
116
+ .describe('Invoice data'),
117
+ output_path: z.string().describe('Absolute path to write the invoice PDF'),
118
+ }, async ({ invoice, output_path }) => {
106
119
  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 };
120
+ const bytes = await client.invoice(invoice);
121
+ await writeFile(output_path, bytes);
122
+ return {
123
+ content: [
124
+ { type: 'text', text: `Invoice written to ${output_path} (${bytes.byteLength} bytes)` },
125
+ ],
126
+ };
127
+ }
128
+ catch (e) {
129
+ return { content: [{ type: 'text', text: errText(e) }], isError: true };
116
130
  }
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 () => {
131
+ });
132
+ server.tool('pdfops_usage', 'Check the current PDFops API quota for the configured key: tier, limit, used, remaining, reset date. Requires PDFOPS_API_KEY.', {}, async () => {
124
133
  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 };
134
+ const usage = await client.usage();
135
+ return { content: [{ type: 'text', text: JSON.stringify(usage, null, 2) }] };
129
136
  }
130
- }
131
- );
137
+ catch (e) {
138
+ return { content: [{ type: 'text', text: errText(e) }], isError: true };
139
+ }
140
+ });
132
141
  const transport = new StdioServerTransport();
133
142
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pdfops-mcp",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "mcpName": "dev.pdfops/pdfops-mcp",
5
5
  "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.",
6
6
  "keywords": [
@@ -20,6 +20,10 @@
20
20
  "author": "PDFops <hello@pdfops.dev> (https://pdfops.dev)",
21
21
  "license": "MIT",
22
22
  "type": "module",
23
+ "scripts": {
24
+ "build": "rm -rf dist && tsc",
25
+ "prepare": "npm run build"
26
+ },
23
27
  "bin": {
24
28
  "pdfops-mcp": "./dist/index.js"
25
29
  },
@@ -28,7 +32,7 @@
28
32
  "README.md"
29
33
  ],
30
34
  "engines": {
31
- "node": ">=18"
35
+ "node": ">=20"
32
36
  },
33
37
  "dependencies": {
34
38
  "@modelcontextprotocol/sdk": "^1.0.0",
@@ -38,5 +42,9 @@
38
42
  "repository": {
39
43
  "type": "git",
40
44
  "url": "git+https://github.com/pdfops/pdfops-mcp.git"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^22.10.0",
48
+ "typescript": "^5.6.0"
41
49
  }
42
- }
50
+ }