pdfops-mcp 0.2.0 → 0.3.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.
- package/LICENSE +21 -0
- package/README.md +10 -1
- package/dist/index.js +126 -118
- package/dist/source.js +108 -0
- 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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
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
4
|
|
|
5
|
-
|
|
5
|
+
Beside a local agent, tools operate on 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. Every PDF input also accepts an `https://` URL or a `data:application/pdf;base64,…` URI, and every output can be returned inline instead of written — which is what makes the server work on hosted runtimes (see below).
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -38,6 +38,15 @@ claude mcp add pdfops -- npx -y pdfops-mcp
|
|
|
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. |
|
|
40
40
|
|
|
41
|
+
## Running remotely (Smithery, Glama hosted, cloud IDE gateways)
|
|
42
|
+
|
|
43
|
+
A hosted MCP runtime executes this server on a machine where your agent's file paths do not exist. Nothing changes in the config — pass sources the server can reach and skip `output_path`:
|
|
44
|
+
|
|
45
|
+
- **Inputs** (`pdf_path`, `pdf_paths`): an `https://` URL the server can fetch (≤50 MB), or a `data:application/pdf;base64,…` URI for small files.
|
|
46
|
+
- **Outputs**: omit `output_path` and `pdf_fill` / `pdf_merge` / `pdf_invoice` return the PDF inline as an embedded `application/pdf` resource (`pdfops://filled.pdf`, …) that the client saves. With `output_path` set, the file is written where the *server* runs.
|
|
47
|
+
|
|
48
|
+
Locally, absolute paths keep working exactly as before and remain the recommended form — bytes stay off the model context.
|
|
49
|
+
|
|
41
50
|
## Example agent flow
|
|
42
51
|
|
|
43
52
|
> "Fill the W-9 template at ~/docs/w9.pdf for Ada Lovelace and merge it with ~/docs/cover.pdf"
|
package/dist/index.js
CHANGED
|
@@ -1,133 +1,141 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
// pdfops-mcp — MCP server exposing the PDFops API as agent tools.
|
|
3
|
+
//
|
|
4
|
+
// Design: tools take PDF *sources* and return files or inline PDFs.
|
|
5
|
+
// Beside a local agent (npx pdfops-mcp) the filesystem is the natural
|
|
6
|
+
// interface — "fill /tmp/form.pdf and save to /tmp/out.pdf" — and the PDF
|
|
7
|
+
// bytes never transit the model context. On a hosted runtime (Smithery,
|
|
8
|
+
// Glama hosted, cloud IDE gateways) the agent's paths do not exist on this
|
|
9
|
+
// machine, so every source also accepts an https:// URL or a
|
|
10
|
+
// data:application/pdf;base64 URI, and omitting output_path returns the
|
|
11
|
+
// result inline as an application/pdf resource. See src/source.ts.
|
|
12
|
+
//
|
|
13
|
+
// Env:
|
|
14
|
+
// PDFOPS_API_KEY optional — free key from https://pdfops.dev/pricing
|
|
15
|
+
// (250 req/mo; keyless works at 100 req/IP/mo)
|
|
16
|
+
// PDFOPS_BASE_URL optional — API origin override (testing)
|
|
17
|
+
import { writeFile } from 'node:fs/promises';
|
|
18
|
+
import { createRequire } from 'node:module';
|
|
19
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
20
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
21
|
+
import { z } from 'zod';
|
|
22
|
+
import { PdfOps, PdfOpsError } from 'pdfops-sdk';
|
|
23
|
+
import { describeSource, pdfResult, resolveSource } from './source.js';
|
|
24
|
+
// Version comes from package.json so the string MCP clients display can no
|
|
25
|
+
// longer drift from the published one (0.2.0 shipped reporting 0.1.1).
|
|
26
|
+
const { version } = createRequire(import.meta.url)('../package.json');
|
|
8
27
|
const client = new PdfOps({
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
});
|
|
13
|
-
const server = new McpServer({
|
|
14
|
-
name: "pdfops",
|
|
15
|
-
version: "0.1.1"
|
|
28
|
+
apiKey: process.env.PDFOPS_API_KEY,
|
|
29
|
+
baseUrl: process.env.PDFOPS_BASE_URL,
|
|
30
|
+
clientTag: 'mcp',
|
|
16
31
|
});
|
|
17
|
-
const
|
|
18
|
-
server.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
32
|
+
const server = new McpServer({ name: 'pdfops', version });
|
|
33
|
+
const SOURCE_DOC = 'PDF source: an absolute file path, an https:// URL, or a data:application/pdf;base64,… URI. Use a URL or data URI when this server runs remotely (Smithery, hosted gateways) where local paths do not exist.';
|
|
34
|
+
const OUTPUT_DOC = 'Absolute path to write the result. Omit when running remotely: the PDF is then returned inline as an application/pdf resource for the client to save.';
|
|
35
|
+
const errText = (e) => e instanceof PdfOpsError
|
|
36
|
+
? `PDFops API error ${e.status} (${e.code}): ${e.message}` +
|
|
37
|
+
(e.code === 'rate_limited'
|
|
38
|
+
? ' — get a free API key (250/mo) at https://pdfops.dev/pricing and set PDFOPS_API_KEY'
|
|
39
|
+
: '')
|
|
40
|
+
: e instanceof Error
|
|
41
|
+
? e.message
|
|
42
|
+
: String(e);
|
|
43
|
+
const fail = (e) => ({ content: [{ type: 'text', text: errText(e) }], isError: true });
|
|
44
|
+
const emit = async (bytes, name, summary, output_path) => {
|
|
45
|
+
if (output_path)
|
|
46
|
+
await writeFile(output_path, bytes);
|
|
47
|
+
return pdfResult(bytes, name, summary, output_path);
|
|
48
|
+
};
|
|
49
|
+
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(SOURCE_DOC) }, async ({ pdf_path }) => {
|
|
23
50
|
try {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
51
|
+
const result = await client.inspect(await resolveSource(pdf_path));
|
|
52
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
return fail(e);
|
|
28
56
|
}
|
|
29
|
-
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
57
|
+
});
|
|
58
|
+
server.tool('pdf_fill', 'Fill AcroForm form fields in a PDF and save or return 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).', {
|
|
59
|
+
pdf_path: z.string().describe(`Template ${SOURCE_DOC}`),
|
|
60
|
+
fields: z
|
|
61
|
+
.record(z.string())
|
|
62
|
+
.describe('Field name → string value (from pdf_inspect\'s fillTemplate)'),
|
|
63
|
+
output_path: z.string().optional().describe(OUTPUT_DOC),
|
|
64
|
+
flatten: z
|
|
65
|
+
.boolean()
|
|
66
|
+
.optional()
|
|
67
|
+
.describe('Bake values into page content and drop the AcroForm so fields are no longer editable'),
|
|
68
|
+
}, async ({ pdf_path, fields, output_path, flatten }) => {
|
|
41
69
|
try {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
]
|
|
48
|
-
};
|
|
49
|
-
} catch (e) {
|
|
50
|
-
return { content: [{ type: "text", text: errText(e) }], isError: true };
|
|
70
|
+
const bytes = await client.fillForm(await resolveSource(pdf_path), fields, { flatten });
|
|
71
|
+
return await emit(bytes, 'filled.pdf', `Filled ${describeSource(pdf_path)}`, output_path);
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
return fail(e);
|
|
51
75
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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 }) => {
|
|
76
|
+
});
|
|
77
|
+
server.tool('pdf_merge', 'Merge two or more PDFs into one, in the order given, and save or return the result.', {
|
|
78
|
+
pdf_paths: z.array(z.string()).min(2).describe(`In order, each a ${SOURCE_DOC}`),
|
|
79
|
+
output_path: z.string().optional().describe(OUTPUT_DOC),
|
|
80
|
+
}, async ({ pdf_paths, output_path }) => {
|
|
62
81
|
try {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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 };
|
|
82
|
+
const inputs = await Promise.all(pdf_paths.map((p) => resolveSource(p)));
|
|
83
|
+
const bytes = await client.merge(inputs);
|
|
84
|
+
return await emit(bytes, 'merged.pdf', `Merged ${pdf_paths.length} PDFs`, output_path);
|
|
73
85
|
}
|
|
74
|
-
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
86
|
+
catch (e) {
|
|
87
|
+
return fail(e);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
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.', {
|
|
91
|
+
invoice: z
|
|
92
|
+
.object({
|
|
93
|
+
from: z.union([
|
|
94
|
+
z.string(),
|
|
95
|
+
z.object({ name: z.string(), lines: z.array(z.string()).optional() }),
|
|
96
|
+
]),
|
|
97
|
+
to: z.union([
|
|
98
|
+
z.string(),
|
|
99
|
+
z.object({ name: z.string(), lines: z.array(z.string()).optional() }),
|
|
100
|
+
]),
|
|
101
|
+
items: z
|
|
102
|
+
.array(z.object({
|
|
103
|
+
description: z.string(),
|
|
104
|
+
quantity: z.number().positive().optional(),
|
|
105
|
+
unit_price: z.number().nonnegative(),
|
|
106
|
+
}))
|
|
107
|
+
.min(1)
|
|
108
|
+
.max(100),
|
|
109
|
+
invoice_number: z.string().optional(),
|
|
110
|
+
date: z
|
|
111
|
+
.string()
|
|
112
|
+
.optional()
|
|
113
|
+
.describe('Shown on the invoice; also pins metadata for determinism'),
|
|
114
|
+
due: z.string().optional(),
|
|
115
|
+
currency: z.string().regex(/^[A-Z]{3}$/).optional(),
|
|
116
|
+
tax_rate: z.number().min(0).max(100).optional(),
|
|
117
|
+
notes: z.string().max(1000).optional(),
|
|
118
|
+
})
|
|
119
|
+
.describe('Invoice data'),
|
|
120
|
+
output_path: z.string().optional().describe(OUTPUT_DOC),
|
|
121
|
+
}, async ({ invoice, output_path }) => {
|
|
106
122
|
try {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
};
|
|
114
|
-
} catch (e) {
|
|
115
|
-
return { content: [{ type: "text", text: errText(e) }], isError: true };
|
|
123
|
+
const bytes = await client.invoice(invoice);
|
|
124
|
+
const name = invoice.invoice_number ? `invoice-${invoice.invoice_number}.pdf` : 'invoice.pdf';
|
|
125
|
+
return await emit(bytes, name, 'Invoice', output_path);
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
return fail(e);
|
|
116
129
|
}
|
|
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 () => {
|
|
130
|
+
});
|
|
131
|
+
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
132
|
try {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
133
|
+
const usage = await client.usage();
|
|
134
|
+
return { content: [{ type: 'text', text: JSON.stringify(usage, null, 2) }] };
|
|
135
|
+
}
|
|
136
|
+
catch (e) {
|
|
137
|
+
return fail(e);
|
|
129
138
|
}
|
|
130
|
-
|
|
131
|
-
);
|
|
139
|
+
});
|
|
132
140
|
const transport = new StdioServerTransport();
|
|
133
141
|
await server.connect(transport);
|
package/dist/source.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// PDF input/output plumbing shared by every tool.
|
|
2
|
+
//
|
|
3
|
+
// The server was designed around local file paths (bytes never transit the
|
|
4
|
+
// model context). That is still the right default beside a local agent — but
|
|
5
|
+
// every hosted MCP runtime (Smithery, Glama hosted, cloud IDE gateways) runs
|
|
6
|
+
// this process on a machine where the agent's paths do not exist, so inspect/
|
|
7
|
+
// fill/merge were broken by construction there (TASK-102). A "source" string is
|
|
8
|
+
// now one of three things, told apart by prefix, with no schema change:
|
|
9
|
+
//
|
|
10
|
+
// /abs/path/file.pdf read from disk (local default)
|
|
11
|
+
// https://host/file.pdf fetched (hosted default)
|
|
12
|
+
// data:application/pdf;base64,JVBERi0x... decoded inline
|
|
13
|
+
//
|
|
14
|
+
// Outputs mirror it: with `output_path` the PDF is written to disk; without it
|
|
15
|
+
// the PDF comes back as an embedded application/pdf resource the client saves.
|
|
16
|
+
import { readFile } from 'node:fs/promises';
|
|
17
|
+
/** Upper bound on a fetched or inline PDF. The API rejects larger bodies anyway. */
|
|
18
|
+
export const MAX_INPUT_BYTES = 50 * 1024 * 1024;
|
|
19
|
+
export const classifySource = (s) => {
|
|
20
|
+
if (/^data:/i.test(s))
|
|
21
|
+
return 'data';
|
|
22
|
+
if (/^https?:\/\//i.test(s))
|
|
23
|
+
return 'url';
|
|
24
|
+
return 'path';
|
|
25
|
+
};
|
|
26
|
+
/** Short, safe label for tool output — never echoes inline payloads. */
|
|
27
|
+
export const describeSource = (s) => {
|
|
28
|
+
switch (classifySource(s)) {
|
|
29
|
+
case 'data':
|
|
30
|
+
return 'inline data URI';
|
|
31
|
+
case 'url':
|
|
32
|
+
try {
|
|
33
|
+
return new URL(s).host + new URL(s).pathname;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return 'url';
|
|
37
|
+
}
|
|
38
|
+
default:
|
|
39
|
+
return s;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const decodeDataUri = (s) => {
|
|
43
|
+
const m = /^data:([^;,]*)((?:;[^;,]*)*),(.*)$/is.exec(s);
|
|
44
|
+
if (!m)
|
|
45
|
+
throw new Error('Malformed data: URI');
|
|
46
|
+
const params = m[2].toLowerCase();
|
|
47
|
+
if (!params.includes(';base64')) {
|
|
48
|
+
throw new Error('data: URI must be base64-encoded (data:application/pdf;base64,...)');
|
|
49
|
+
}
|
|
50
|
+
const bytes = Buffer.from(m[3].replace(/\s+/g, ''), 'base64');
|
|
51
|
+
if (bytes.byteLength === 0)
|
|
52
|
+
throw new Error('data: URI decoded to zero bytes');
|
|
53
|
+
return new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
54
|
+
};
|
|
55
|
+
const fetchUrl = async (url, fetchImpl) => {
|
|
56
|
+
const res = await fetchImpl(url, {
|
|
57
|
+
redirect: 'follow',
|
|
58
|
+
signal: AbortSignal.timeout(30_000),
|
|
59
|
+
headers: { accept: 'application/pdf,*/*' },
|
|
60
|
+
});
|
|
61
|
+
if (!res.ok)
|
|
62
|
+
throw new Error(`Fetching ${describeSource(url)} failed: HTTP ${res.status}`);
|
|
63
|
+
const declared = Number(res.headers.get('content-length') ?? 0);
|
|
64
|
+
if (declared > MAX_INPUT_BYTES) {
|
|
65
|
+
throw new Error(`${describeSource(url)} is ${declared} bytes; limit is ${MAX_INPUT_BYTES}`);
|
|
66
|
+
}
|
|
67
|
+
const buf = new Uint8Array(await res.arrayBuffer());
|
|
68
|
+
if (buf.byteLength > MAX_INPUT_BYTES) {
|
|
69
|
+
throw new Error(`${describeSource(url)} is ${buf.byteLength} bytes; limit is ${MAX_INPUT_BYTES}`);
|
|
70
|
+
}
|
|
71
|
+
if (buf.byteLength === 0)
|
|
72
|
+
throw new Error(`${describeSource(url)} returned an empty body`);
|
|
73
|
+
return buf;
|
|
74
|
+
};
|
|
75
|
+
/** Resolve a source string (path | https URL | data: URI) to PDF bytes. */
|
|
76
|
+
export const resolveSource = async (source, fetchImpl = fetch) => {
|
|
77
|
+
switch (classifySource(source)) {
|
|
78
|
+
case 'data':
|
|
79
|
+
return decodeDataUri(source);
|
|
80
|
+
case 'url':
|
|
81
|
+
return fetchUrl(source, fetchImpl);
|
|
82
|
+
default: {
|
|
83
|
+
const buf = await readFile(source);
|
|
84
|
+
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Tool result for a produced PDF. With `wroteTo` (the caller already wrote the
|
|
90
|
+
* file) it is a one-line confirmation; without it the bytes travel back inline
|
|
91
|
+
* as an embedded application/pdf resource, which is what a hosted server must
|
|
92
|
+
* do since it has no filesystem the agent can reach.
|
|
93
|
+
*/
|
|
94
|
+
export const pdfResult = (bytes, name, summary, wroteTo) => {
|
|
95
|
+
if (wroteTo) {
|
|
96
|
+
return { content: [{ type: 'text', text: `${summary} written to ${wroteTo} (${bytes.byteLength} bytes)` }] };
|
|
97
|
+
}
|
|
98
|
+
const uri = `pdfops://${name}`;
|
|
99
|
+
return {
|
|
100
|
+
content: [
|
|
101
|
+
{
|
|
102
|
+
type: 'text',
|
|
103
|
+
text: `${summary} (${bytes.byteLength} bytes) returned inline as ${uri} — no output_path was given. Save the attached application/pdf resource; pass output_path to write to disk instead.`,
|
|
104
|
+
},
|
|
105
|
+
{ type: 'resource', resource: { uri, mimeType: 'application/pdf', blob: Buffer.from(bytes).toString('base64') } },
|
|
106
|
+
],
|
|
107
|
+
};
|
|
108
|
+
};
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pdfops-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"mcpName": "dev.pdfops/pdfops-mcp",
|
|
5
|
-
"description": "MCP server for the PDFops API
|
|
5
|
+
"description": "MCP server for the PDFops API — 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": [
|
|
7
7
|
"mcp",
|
|
8
8
|
"model-context-protocol",
|
|
@@ -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": ">=
|
|
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
|
+
}
|