pdfops-mcp 0.1.1 → 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.
- package/LICENSE +21 -0
- package/README.md +1 -1
- package/dist/index.js +125 -114
- package/package.json +12 -4
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/README.md
CHANGED
|
@@ -33,7 +33,7 @@ claude mcp add pdfops -- npx -y pdfops-mcp
|
|
|
33
33
|
| Tool | What it does |
|
|
34
34
|
|---|---|
|
|
35
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. |
|
|
36
|
+
| `pdf_fill` | Fill AcroForm fields → write the filled PDF. Optional `flatten` bakes values in and drops the form. |
|
|
37
37
|
| `pdf_merge` | Merge ≥2 PDFs in order → write the result. |
|
|
38
38
|
| `pdf_invoice` | Structured data → complete invoice PDF. Deterministic: same input, byte-identical output. |
|
|
39
39
|
| `pdfops_usage` | Quota check for the configured key. |
|
package/dist/index.js
CHANGED
|
@@ -1,131 +1,142 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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';
|
|
7
18
|
const client = new PdfOps({
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
19
|
+
apiKey: process.env.PDFOPS_API_KEY,
|
|
20
|
+
baseUrl: process.env.PDFOPS_BASE_URL,
|
|
21
|
+
clientTag: 'mcp',
|
|
11
22
|
});
|
|
12
23
|
const server = new McpServer({
|
|
13
|
-
|
|
14
|
-
|
|
24
|
+
name: 'pdfops',
|
|
25
|
+
version: '0.2.1', // keep in sync with package.json
|
|
15
26
|
});
|
|
16
|
-
const errText = (e) => e instanceof PdfOpsError
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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 }) => {
|
|
22
34
|
try {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
} catch (e) {
|
|
26
|
-
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) }] };
|
|
27
37
|
}
|
|
28
|
-
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
catch (e) {
|
|
39
|
+
return { content: [{ type: 'text', text: errText(e) }], isError: true };
|
|
40
|
+
}
|
|
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 }) => {
|
|
39
53
|
try {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
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
|
+
};
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
return { content: [{ type: 'text', text: errText(e) }], isError: true };
|
|
49
64
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
},
|
|
59
|
-
async ({ pdf_paths, output_path }) => {
|
|
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 }) => {
|
|
60
73
|
try {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
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 };
|
|
71
85
|
}
|
|
72
|
-
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
z.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
z
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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 }) => {
|
|
104
119
|
try {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
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 };
|
|
114
130
|
}
|
|
115
|
-
|
|
116
|
-
)
|
|
117
|
-
server.tool(
|
|
118
|
-
"pdfops_usage",
|
|
119
|
-
"Check the current PDFops API quota for the configured key: tier, limit, used, remaining, reset date. Requires PDFOPS_API_KEY.",
|
|
120
|
-
{},
|
|
121
|
-
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 () => {
|
|
122
133
|
try {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
} catch (e) {
|
|
126
|
-
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) }] };
|
|
127
136
|
}
|
|
128
|
-
|
|
129
|
-
);
|
|
137
|
+
catch (e) {
|
|
138
|
+
return { content: [{ type: 'text', text: errText(e) }], isError: true };
|
|
139
|
+
}
|
|
140
|
+
});
|
|
130
141
|
const transport = new StdioServerTransport();
|
|
131
142
|
await server.connect(transport);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pdfops-mcp",
|
|
3
|
-
"version": "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,15 +32,19 @@
|
|
|
28
32
|
"README.md"
|
|
29
33
|
],
|
|
30
34
|
"engines": {
|
|
31
|
-
"node": ">=
|
|
35
|
+
"node": ">=20"
|
|
32
36
|
},
|
|
33
37
|
"dependencies": {
|
|
34
38
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
35
|
-
"pdfops-sdk": "^0.
|
|
39
|
+
"pdfops-sdk": "^0.4.0",
|
|
36
40
|
"zod": "^3.23.0"
|
|
37
41
|
},
|
|
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
|
+
}
|