@exactpdf/mcp 0.2.3

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/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # @exactpdf/mcp
2
+
3
+ <!-- mcp-name: com.exactpdf/mcp -->
4
+
5
+ Model Context Protocol (stdio) server for **ExactPDF** — use PDF tools from **Cursor**, **Claude Desktop**, **Codex**, and any MCP client.
6
+
7
+ ## Setup
8
+
9
+ 1. Create an API key at [exactpdf.com](https://exactpdf.com) (Max → API keys).
10
+ 2. Export:
11
+
12
+ ```bash
13
+ export EXACTPDF_API_KEY="sk_live_…"
14
+ # Optional: default https://exactpdf.com
15
+ export EXACTPDF_API_BASE="https://exactpdf.com"
16
+ # Optional: where API binary outputs are written (default: OS temp dir)
17
+ export EXACTPDF_API_OUTPUT_DIR="/tmp"
18
+ # Backward-compatible alias:
19
+ export EXACTPDF_MERGE_OUTPUT_DIR="/tmp"
20
+ ```
21
+
22
+ 3. Add to your MCP config (Cursor → Settings → MCP):
23
+
24
+ ```json
25
+ {
26
+ "mcpServers": {
27
+ "exactpdf": {
28
+ "command": "npx",
29
+ "args": ["-y", "@exactpdf/mcp"],
30
+ "env": {
31
+ "EXACTPDF_API_KEY": "sk_live_…"
32
+ }
33
+ }
34
+ }
35
+ }
36
+ ```
37
+
38
+ For a local checkout of this monorepo:
39
+
40
+ ```json
41
+ "command": "node",
42
+ "args": ["/path/to/ExactPDF/packages/exactpdf-mcp/dist/run.js"]
43
+ ```
44
+
45
+ ## Tools
46
+
47
+ | Tool | Endpoint | Credits |
48
+ |------|----------|--------:|
49
+ | `exactpdf_account` | GET /api/v1/account | 0 |
50
+ | `exactpdf_pdf_info` | POST /api/v1/pdf-info | 0 |
51
+ | `exactpdf_merge_pdfs` | POST /api/v1/merge | 1 |
52
+ | `exactpdf_split_pdf` | POST /api/v1/split | 1 |
53
+ | `exactpdf_rotate_pdf` | POST /api/v1/rotate | 1 |
54
+ | `exactpdf_compress_pdf` | POST /api/v1/compress | 1 |
55
+ | `exactpdf_images_to_pdf` | POST /api/v1/images-to-pdf | 1 |
56
+ | `exactpdf_extract_text` | POST /api/v1/extract-text | 1 |
57
+ | `exactpdf_pdf_structured_markdown` | POST /api/v1/pdf-structured-markdown | 1 |
58
+
59
+ ## Publish checklist
60
+
61
+ ```bash
62
+ npm run mcp:package:check
63
+ cd packages/exactpdf-mcp
64
+ npm publish --access public --provenance
65
+ npx -y mcp-publisher publish --file server.json
66
+ ```
67
+
68
+ The package must exist on the public npm registry before the MCP Registry can verify `mcpName`.
69
+
70
+ ## Docs
71
+
72
+ - [https://exactpdf.com/docs/api](https://exactpdf.com/docs/api)
73
+
74
+ ## License
75
+
76
+ MIT
package/dist/run.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ExactPDF MCP — stdio server for Cursor / Claude Desktop / Codex.
4
+ * Requires EXACTPDF_API_KEY (sk_live_…) from exactpdf.com (Max dashboard → API keys).
5
+ *
6
+ * @see https://exactpdf.com/docs/api
7
+ */
8
+ export {};
package/dist/run.js ADDED
@@ -0,0 +1,387 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ExactPDF MCP — stdio server for Cursor / Claude Desktop / Codex.
4
+ * Requires EXACTPDF_API_KEY (sk_live_…) from exactpdf.com (Max dashboard → API keys).
5
+ *
6
+ * @see https://exactpdf.com/docs/api
7
+ */
8
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
9
+ import { tmpdir } from 'node:os';
10
+ import { basename, join } from 'node:path';
11
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
12
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
13
+ import { z } from 'zod';
14
+ const BASE = (process.env.EXACTPDF_API_BASE ?? 'https://exactpdf.com').replace(/\/$/, '');
15
+ function outputDir() {
16
+ return (process.env.EXACTPDF_API_OUTPUT_DIR?.trim() ||
17
+ process.env.EXACTPDF_MERGE_OUTPUT_DIR?.trim() ||
18
+ tmpdir());
19
+ }
20
+ function requireKey() {
21
+ const k = process.env.EXACTPDF_API_KEY?.trim();
22
+ if (!k) {
23
+ throw new Error('EXACTPDF_API_KEY is not set. Create a key at exactpdf.com (Max → API keys), then export EXACTPDF_API_KEY=sk_live_…');
24
+ }
25
+ return k;
26
+ }
27
+ const server = new McpServer({ name: 'exactpdf', version: '0.2.3' }, {
28
+ instructions: 'ExactPDF API tools: exactpdf_account + exactpdf_pdf_info (free); merge, split, rotate, compress, images→PDF, extract-text, pdf-structured-markdown (1 credit each on success). Set EXACTPDF_API_KEY.',
29
+ });
30
+ server.registerTool('exactpdf_account', {
31
+ description: 'Return ExactPDF API credit balance and key metadata. Does not consume a credit.',
32
+ inputSchema: z.object({}),
33
+ }, async () => {
34
+ const key = requireKey();
35
+ const res = await fetch(`${BASE}/api/v1/account`, {
36
+ headers: {
37
+ Authorization: `Bearer ${key}`,
38
+ Accept: 'application/json',
39
+ },
40
+ });
41
+ const text = await res.text();
42
+ return {
43
+ content: [{ type: 'text', text: `HTTP ${res.status}\n${text}` }],
44
+ };
45
+ });
46
+ server.registerTool('exactpdf_merge_pdfs', {
47
+ description: 'Merge two or more local PDF files via ExactPDF API (1 credit). Provide absolute file paths in order.',
48
+ inputSchema: z.object({
49
+ paths: z
50
+ .array(z.string())
51
+ .min(2)
52
+ .describe('Absolute paths to PDF files on this machine'),
53
+ }),
54
+ }, async ({ paths }) => {
55
+ const key = requireKey();
56
+ const form = new FormData();
57
+ for (const p of paths) {
58
+ const buf = await readFile(p);
59
+ form.append('file', new Blob([buf], { type: 'application/pdf' }), basename(p));
60
+ }
61
+ const res = await fetch(`${BASE}/api/v1/merge`, {
62
+ method: 'POST',
63
+ headers: {
64
+ Authorization: `Bearer ${key}`,
65
+ },
66
+ body: form,
67
+ });
68
+ const credits = res.headers.get('x-credits-remaining') ?? '?';
69
+ if (!res.ok) {
70
+ const errBody = await res.text();
71
+ return {
72
+ content: [
73
+ {
74
+ type: 'text',
75
+ text: `Merge failed HTTP ${res.status}. Credits: ${credits}\n${errBody.slice(0, 4000)}`,
76
+ },
77
+ ],
78
+ isError: true,
79
+ };
80
+ }
81
+ const buf = Buffer.from(await res.arrayBuffer());
82
+ const dir = outputDir();
83
+ await mkdir(dir, { recursive: true });
84
+ const outPath = join(dir, `exactpdf-merged-${Date.now()}.pdf`);
85
+ await writeFile(outPath, buf);
86
+ return {
87
+ content: [
88
+ {
89
+ type: 'text',
90
+ text: `Merged ${paths.length} PDFs → ${outPath}\nCredits remaining: ${credits}`,
91
+ },
92
+ ],
93
+ };
94
+ });
95
+ server.registerTool('exactpdf_split_pdf', {
96
+ description: 'Split one PDF via ExactPDF API (1 credit). Default: one PDF per page (ZIP). Optional at_pages (e.g. "3,7" splits after pages 3 and 7) or ranges JSON string like [[1,3],[4,10]].',
97
+ inputSchema: z.object({
98
+ path: z.string().describe('Absolute path to a PDF file'),
99
+ at_pages: z
100
+ .string()
101
+ .optional()
102
+ .describe('Comma-separated split points after 1-based page numbers, e.g. "3,7"'),
103
+ ranges_json: z
104
+ .string()
105
+ .optional()
106
+ .describe('JSON array of inclusive [start,end] page ranges, e.g. "[[1,3],[4,7]]"'),
107
+ }),
108
+ }, async ({ path, at_pages, ranges_json }) => {
109
+ const key = requireKey();
110
+ const form = new FormData();
111
+ form.append('file', new Blob([await readFile(path)], { type: 'application/pdf' }), basename(path));
112
+ if (ranges_json?.trim())
113
+ form.append('ranges', ranges_json.trim());
114
+ else if (at_pages?.trim())
115
+ form.append('at_pages', at_pages.trim());
116
+ const res = await fetch(`${BASE}/api/v1/split`, {
117
+ method: 'POST',
118
+ headers: { Authorization: `Bearer ${key}` },
119
+ body: form,
120
+ });
121
+ const credits = res.headers.get('x-credits-remaining') ?? '?';
122
+ if (!res.ok) {
123
+ const errBody = await res.text();
124
+ return {
125
+ content: [
126
+ {
127
+ type: 'text',
128
+ text: `Split failed HTTP ${res.status}. Credits: ${credits}\n${errBody.slice(0, 4000)}`,
129
+ },
130
+ ],
131
+ isError: true,
132
+ };
133
+ }
134
+ const buf = Buffer.from(await res.arrayBuffer());
135
+ const dir = outputDir();
136
+ await mkdir(dir, { recursive: true });
137
+ const outPath = join(dir, `exactpdf-split-${Date.now()}.zip`);
138
+ await writeFile(outPath, buf);
139
+ return {
140
+ content: [
141
+ {
142
+ type: 'text',
143
+ text: `Split → ${outPath}\nCredits remaining: ${credits}`,
144
+ },
145
+ ],
146
+ };
147
+ });
148
+ server.registerTool('exactpdf_rotate_pdf', {
149
+ description: 'Rotate every page of a PDF via ExactPDF API (1 credit). angle: 90, 180, 270, -90, -180, or -270.',
150
+ inputSchema: z.object({
151
+ path: z.string().describe('Absolute path to a PDF file'),
152
+ angle: z
153
+ .union([z.literal(90), z.literal(180), z.literal(270), z.literal(-90), z.literal(-180), z.literal(-270)])
154
+ .describe('Clockwise rotation in degrees'),
155
+ }),
156
+ }, async ({ path, angle }) => {
157
+ const key = requireKey();
158
+ const form = new FormData();
159
+ form.append('file', new Blob([await readFile(path)], { type: 'application/pdf' }), basename(path));
160
+ form.append('angle', String(angle));
161
+ const res = await fetch(`${BASE}/api/v1/rotate`, {
162
+ method: 'POST',
163
+ headers: { Authorization: `Bearer ${key}` },
164
+ body: form,
165
+ });
166
+ const credits = res.headers.get('x-credits-remaining') ?? '?';
167
+ if (!res.ok) {
168
+ const errBody = await res.text();
169
+ return {
170
+ content: [
171
+ {
172
+ type: 'text',
173
+ text: `Rotate failed HTTP ${res.status}. Credits: ${credits}\n${errBody.slice(0, 4000)}`,
174
+ },
175
+ ],
176
+ isError: true,
177
+ };
178
+ }
179
+ const buf = Buffer.from(await res.arrayBuffer());
180
+ const dir = outputDir();
181
+ await mkdir(dir, { recursive: true });
182
+ const outPath = join(dir, `exactpdf-rotated-${Date.now()}.pdf`);
183
+ await writeFile(outPath, buf);
184
+ return {
185
+ content: [
186
+ {
187
+ type: 'text',
188
+ text: `Rotated (${angle}°) → ${outPath}\nCredits remaining: ${credits}`,
189
+ },
190
+ ],
191
+ };
192
+ });
193
+ server.registerTool('exactpdf_compress_pdf', {
194
+ description: 'Compress / repack a PDF via ExactPDF API (1 credit). Uses object-stream save; may not shrink image-heavy PDFs.',
195
+ inputSchema: z.object({
196
+ path: z.string().describe('Absolute path to a PDF file'),
197
+ }),
198
+ }, async ({ path }) => {
199
+ const key = requireKey();
200
+ const form = new FormData();
201
+ form.append('file', new Blob([await readFile(path)], { type: 'application/pdf' }), basename(path));
202
+ const res = await fetch(`${BASE}/api/v1/compress`, {
203
+ method: 'POST',
204
+ headers: { Authorization: `Bearer ${key}` },
205
+ body: form,
206
+ });
207
+ const credits = res.headers.get('x-credits-remaining') ?? '?';
208
+ if (!res.ok) {
209
+ const errBody = await res.text();
210
+ return {
211
+ content: [
212
+ {
213
+ type: 'text',
214
+ text: `Compress failed HTTP ${res.status}. Credits: ${credits}\n${errBody.slice(0, 4000)}`,
215
+ },
216
+ ],
217
+ isError: true,
218
+ };
219
+ }
220
+ const buf = Buffer.from(await res.arrayBuffer());
221
+ const dir = outputDir();
222
+ await mkdir(dir, { recursive: true });
223
+ const outPath = join(dir, `exactpdf-compressed-${Date.now()}.pdf`);
224
+ await writeFile(outPath, buf);
225
+ return {
226
+ content: [
227
+ {
228
+ type: 'text',
229
+ text: `Compressed → ${outPath}\nCredits remaining: ${credits}`,
230
+ },
231
+ ],
232
+ };
233
+ });
234
+ server.registerTool('exactpdf_images_to_pdf', {
235
+ description: 'Combine local PNG/JPEG images into one PDF via ExactPDF API (1 credit). Order matches paths array.',
236
+ inputSchema: z.object({
237
+ paths: z.array(z.string()).min(1).describe('Absolute paths to PNG or JPEG files'),
238
+ }),
239
+ }, async ({ paths }) => {
240
+ const key = requireKey();
241
+ const form = new FormData();
242
+ for (const p of paths) {
243
+ const buf = await readFile(p);
244
+ const lower = p.toLowerCase();
245
+ const type = lower.endsWith('.png')
246
+ ? 'image/png'
247
+ : lower.endsWith('.jpg') || lower.endsWith('.jpeg')
248
+ ? 'image/jpeg'
249
+ : 'application/octet-stream';
250
+ form.append('file', new Blob([buf], { type }), basename(p));
251
+ }
252
+ const res = await fetch(`${BASE}/api/v1/images-to-pdf`, {
253
+ method: 'POST',
254
+ headers: { Authorization: `Bearer ${key}` },
255
+ body: form,
256
+ });
257
+ const credits = res.headers.get('x-credits-remaining') ?? '?';
258
+ if (!res.ok) {
259
+ const errBody = await res.text();
260
+ return {
261
+ content: [
262
+ {
263
+ type: 'text',
264
+ text: `images-to-pdf failed HTTP ${res.status}. Credits: ${credits}\n${errBody.slice(0, 4000)}`,
265
+ },
266
+ ],
267
+ isError: true,
268
+ };
269
+ }
270
+ const buf = Buffer.from(await res.arrayBuffer());
271
+ const dir = outputDir();
272
+ await mkdir(dir, { recursive: true });
273
+ const outPath = join(dir, `exactpdf-from-images-${Date.now()}.pdf`);
274
+ await writeFile(outPath, buf);
275
+ return {
276
+ content: [
277
+ {
278
+ type: 'text',
279
+ text: `Built PDF (${paths.length} images) → ${outPath}\nCredits remaining: ${credits}`,
280
+ },
281
+ ],
282
+ };
283
+ });
284
+ server.registerTool('exactpdf_pdf_info', {
285
+ description: 'Read page count and PDF metadata (title, author, …). POST /api/v1/pdf-info — does not consume a credit.',
286
+ inputSchema: z.object({
287
+ path: z.string().describe('Absolute path to a PDF file'),
288
+ }),
289
+ }, async ({ path }) => {
290
+ const key = requireKey();
291
+ const form = new FormData();
292
+ form.append('file', new Blob([await readFile(path)], { type: 'application/pdf' }), basename(path));
293
+ const res = await fetch(`${BASE}/api/v1/pdf-info`, {
294
+ method: 'POST',
295
+ headers: {
296
+ Authorization: `Bearer ${key}`,
297
+ Accept: 'application/json',
298
+ },
299
+ body: form,
300
+ });
301
+ const raw = await res.text();
302
+ return {
303
+ content: [{ type: 'text', text: `HTTP ${res.status}\n${raw.slice(0, 120_000)}` }],
304
+ };
305
+ });
306
+ server.registerTool('exactpdf_extract_text', {
307
+ description: 'Extract plain text from a PDF via ExactPDF API (1 credit). Returns JSON with text and page_count.',
308
+ inputSchema: z.object({
309
+ path: z.string().describe('Absolute path to a PDF file'),
310
+ }),
311
+ }, async ({ path }) => {
312
+ const key = requireKey();
313
+ const form = new FormData();
314
+ form.append('file', new Blob([await readFile(path)], { type: 'application/pdf' }), basename(path));
315
+ const res = await fetch(`${BASE}/api/v1/extract-text`, {
316
+ method: 'POST',
317
+ headers: {
318
+ Authorization: `Bearer ${key}`,
319
+ Accept: 'application/json',
320
+ },
321
+ body: form,
322
+ });
323
+ const credits = res.headers.get('x-credits-remaining') ?? '?';
324
+ const raw = await res.text();
325
+ if (!res.ok) {
326
+ return {
327
+ content: [
328
+ {
329
+ type: 'text',
330
+ text: `extract-text failed HTTP ${res.status}. Credits: ${credits}\n${raw.slice(0, 4000)}`,
331
+ },
332
+ ],
333
+ isError: true,
334
+ };
335
+ }
336
+ return {
337
+ content: [{ type: 'text', text: `Credits remaining: ${credits}\n${raw.slice(0, 120_000)}` }],
338
+ };
339
+ });
340
+ server.registerTool('exactpdf_pdf_structured_markdown', {
341
+ description: 'Convert PDF→structured Markdown JSON via ExactPDF API (1 credit). mode developer|academic|rag mirrors the web tool presets.',
342
+ inputSchema: z.object({
343
+ path: z.string().describe('Absolute path to a PDF file'),
344
+ mode: z
345
+ .enum(['developer', 'academic', 'rag'])
346
+ .optional()
347
+ .describe('Structured export profile — academic keeps page separators, rag joins short lines.'),
348
+ }),
349
+ }, async ({ path, mode }) => {
350
+ const key = requireKey();
351
+ const form = new FormData();
352
+ form.append('file', new Blob([await readFile(path)], { type: 'application/pdf' }), basename(path));
353
+ if (mode)
354
+ form.append('mode', mode);
355
+ const res = await fetch(`${BASE}/api/v1/pdf-structured-markdown`, {
356
+ method: 'POST',
357
+ headers: {
358
+ Authorization: `Bearer ${key}`,
359
+ Accept: 'application/json',
360
+ },
361
+ body: form,
362
+ });
363
+ const credits = res.headers.get('x-credits-remaining') ?? '?';
364
+ const raw = await res.text();
365
+ if (!res.ok) {
366
+ return {
367
+ content: [
368
+ {
369
+ type: 'text',
370
+ text: `pdf-structured-markdown failed HTTP ${res.status}. Credits: ${credits}\n${raw.slice(0, 8000)}`,
371
+ },
372
+ ],
373
+ isError: true,
374
+ };
375
+ }
376
+ return {
377
+ content: [{ type: 'text', text: `Credits remaining: ${credits}\n${raw.slice(0, 120_000)}` }],
378
+ };
379
+ });
380
+ async function main() {
381
+ const transport = new StdioServerTransport();
382
+ await server.connect(transport);
383
+ }
384
+ main().catch((e) => {
385
+ console.error(e);
386
+ process.exit(1);
387
+ });
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@exactpdf/mcp",
3
+ "version": "0.2.3",
4
+ "description": "MCP server for ExactPDF — PDF tools (merge, split, rotate, compress, images→PDF, metadata) and API credits.",
5
+ "mcpName": "com.exactpdf/mcp",
6
+ "type": "module",
7
+ "main": "./dist/run.js",
8
+ "exports": {
9
+ ".": "./dist/run.js"
10
+ },
11
+ "bin": {
12
+ "exactpdf-mcp": "dist/run.js"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md",
17
+ "server.json"
18
+ ],
19
+ "publishConfig": {
20
+ "access": "public",
21
+ "provenance": true
22
+ },
23
+ "homepage": "https://exactpdf.com/docs/api",
24
+ "bugs": {
25
+ "url": "https://exactpdf.com/docs/api",
26
+ "email": "support@exactpdf.com"
27
+ },
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/exactpdf/exactpdf.git",
31
+ "directory": "packages/exactpdf-mcp"
32
+ },
33
+ "scripts": {
34
+ "build": "tsc -p .",
35
+ "prepack": "npm run build"
36
+ },
37
+ "keywords": [
38
+ "mcp",
39
+ "model-context-protocol",
40
+ "pdf",
41
+ "exactpdf",
42
+ "cursor",
43
+ "claude",
44
+ "claude-desktop",
45
+ "headless-pdf",
46
+ "pdf-api"
47
+ ],
48
+ "author": "ExactPDF",
49
+ "license": "MIT",
50
+ "engines": {
51
+ "node": ">=18"
52
+ },
53
+ "dependencies": {
54
+ "@modelcontextprotocol/sdk": "^1.29.0",
55
+ "zod": "3.25.76"
56
+ },
57
+ "devDependencies": {
58
+ "@types/node": "22.19.17",
59
+ "typescript": "5.6.3"
60
+ }
61
+ }
package/server.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "com.exactpdf/mcp",
4
+ "title": "ExactPDF",
5
+ "description": "MCP server for ExactPDF's agent-facing PDF API: merge, split, rotate, compress, images to PDF, PDF metadata, text extraction, and structured Markdown export.",
6
+ "version": "0.2.3",
7
+ "websiteUrl": "https://exactpdf.com/docs/api",
8
+ "packages": [
9
+ {
10
+ "registryType": "npm",
11
+ "identifier": "@exactpdf/mcp",
12
+ "version": "0.2.3",
13
+ "transport": {
14
+ "type": "stdio"
15
+ }
16
+ }
17
+ ]
18
+ }