@exactpdf/mcp 0.2.7 → 0.2.9
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 +39 -2
- package/dist/run.js +26 -24
- package/package.json +2 -3
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- mcp-name: com.exactpdf/mcp -->
|
|
4
4
|
|
|
5
|
-
Model Context Protocol (stdio) server for **ExactPDF** —
|
|
5
|
+
Model Context Protocol (stdio) server for **ExactPDF** — production PDF tools for **Cursor**, **Claude Desktop**, **Codex**, and any MCP client.
|
|
6
|
+
|
|
7
|
+
Use it when an agent needs to inspect, merge, split, rotate, compress, extract, narrate, translate, or export PDFs without writing fragile PDF plumbing.
|
|
8
|
+
|
|
9
|
+
**Design principle:** every tool should work on first run, explain credit cost before risky work, return a local output path for files, and expose precise HTTP/API errors when something fails.
|
|
6
10
|
|
|
7
11
|
## Setup
|
|
8
12
|
|
|
@@ -44,6 +48,17 @@ For a local checkout of this monorepo:
|
|
|
44
48
|
|
|
45
49
|
## Tools
|
|
46
50
|
|
|
51
|
+
Safe first run:
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
1. exactpdf_account
|
|
55
|
+
2. exactpdf_pdf_info(path="/absolute/path/document.pdf")
|
|
56
|
+
3. exactpdf_voice_preview(text="Short sample...", voice_style="professional")
|
|
57
|
+
4. exactpdf_estimate_speech_cost(path="/absolute/path/document.pdf", mode="audiobook")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Document outputs are saved to `EXACTPDF_API_OUTPUT_DIR` or your OS temp directory. Paid document tools consume credits only on successful output. Async speech jobs are metered by generated minutes.
|
|
61
|
+
|
|
47
62
|
| Tool | Endpoint | Credits |
|
|
48
63
|
|------|----------|--------:|
|
|
49
64
|
| `exactpdf_account` | GET /api/v1/account | 0 |
|
|
@@ -66,6 +81,17 @@ For a local checkout of this monorepo:
|
|
|
66
81
|
| `exactpdf_get_speech_job` | GET /api/v1/speech-jobs/:id | 0 |
|
|
67
82
|
| `exactpdf_download_audio` | GET /api/v1/speech-jobs/:id/download | 0 |
|
|
68
83
|
|
|
84
|
+
## Tool behavior
|
|
85
|
+
|
|
86
|
+
| Tool family | What the agent provides | What ExactPDF returns |
|
|
87
|
+
| --- | --- | --- |
|
|
88
|
+
| Account / inspect | API key, local PDF path | JSON balance, page count, metadata |
|
|
89
|
+
| PDF operations | Absolute local PDF paths | Saved PDF/ZIP path plus remaining credits |
|
|
90
|
+
| Text / Markdown | Absolute local PDF path | JSON text/Markdown, page count, structured content |
|
|
91
|
+
| Voice preview | Short text, voice style/language | Saved MP3/WAV preview, no credit charge |
|
|
92
|
+
| Speech jobs | PDF path, page range, style, callback URL | `job_id`, status URL, pricing estimate |
|
|
93
|
+
| Job polling/download | `job_id` | progress/errors/result URL or saved audio/ZIP |
|
|
94
|
+
|
|
69
95
|
Async audiobook flow:
|
|
70
96
|
|
|
71
97
|
```text
|
|
@@ -97,12 +123,23 @@ Presentation narration flow:
|
|
|
97
123
|
|
|
98
124
|
The default ZIP includes one MP3 per PDF page plus `manifest.json` with page numbers and duration seconds.
|
|
99
125
|
|
|
126
|
+
## Reliability rules for agent builders
|
|
127
|
+
|
|
128
|
+
- Always call `exactpdf_account` once before paid tools.
|
|
129
|
+
- Use absolute file paths. Relative paths are client-dependent and may fail.
|
|
130
|
+
- Use `exactpdf_pdf_info` before large jobs to confirm the file is readable.
|
|
131
|
+
- Use `exactpdf_estimate_speech_cost` before speech/audiobook/translation jobs.
|
|
132
|
+
- Use `exactpdf_voice_preview` before full narration when quality matters.
|
|
133
|
+
- Async jobs are not downloads. Submit, poll, then download after `succeeded`.
|
|
134
|
+
- If a tool returns `isError`, show the HTTP status and response body to the user. The API returns structured error details.
|
|
135
|
+
- Keep API keys out of prompts, logs, screenshots, and commits.
|
|
136
|
+
|
|
100
137
|
## Publish checklist
|
|
101
138
|
|
|
102
139
|
```bash
|
|
103
140
|
npm run mcp:package:check
|
|
104
141
|
cd packages/exactpdf-mcp
|
|
105
|
-
npm publish --access public
|
|
142
|
+
npm publish --access public
|
|
106
143
|
npx -y mcp-publisher publish --file server.json
|
|
107
144
|
```
|
|
108
145
|
|
package/dist/run.js
CHANGED
|
@@ -24,8 +24,8 @@ function requireKey() {
|
|
|
24
24
|
}
|
|
25
25
|
return k;
|
|
26
26
|
}
|
|
27
|
-
const server = new McpServer({ name: 'exactpdf', version: '0.2.
|
|
28
|
-
instructions: 'ExactPDF
|
|
27
|
+
const server = new McpServer({ name: 'exactpdf', version: '0.2.8' }, {
|
|
28
|
+
instructions: 'ExactPDF turns local PDFs into production outputs for agents: inspect metadata, merge/split/rotate/compress, extract text/structured Markdown, create voice previews, and queue async PDF speech/audiobook/translation/presentation narration jobs. Free tools: account, pdf-info, voice-preview, speech cost estimate. Standard document tools cost 1 credit on success. Async speech jobs are metered by generated minutes. Use absolute local file paths; binary outputs are saved to EXACTPDF_API_OUTPUT_DIR or the OS temp directory. For async jobs, submit first, then poll exactpdf_get_speech_job, then download with exactpdf_download_audio when succeeded.',
|
|
29
29
|
});
|
|
30
30
|
async function saveBinaryFromResponse(res, prefix, fallbackExt) {
|
|
31
31
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
@@ -48,7 +48,7 @@ function extensionFromContentType(contentType, fallback) {
|
|
|
48
48
|
return fallback;
|
|
49
49
|
}
|
|
50
50
|
server.registerTool('exactpdf_account', {
|
|
51
|
-
description: '
|
|
51
|
+
description: 'Check whether the ExactPDF API key works and return credit balance/key metadata. Free; does not consume credits. Use this before any paid tool.',
|
|
52
52
|
inputSchema: z.object({}),
|
|
53
53
|
}, async () => {
|
|
54
54
|
const key = requireKey();
|
|
@@ -64,7 +64,7 @@ server.registerTool('exactpdf_account', {
|
|
|
64
64
|
};
|
|
65
65
|
});
|
|
66
66
|
server.registerTool('exactpdf_merge_pdfs', {
|
|
67
|
-
description: 'Merge
|
|
67
|
+
description: 'Merge 2-20 local PDF files into one PDF in the exact order provided. Costs 1 credit only on successful output. Saves the merged PDF locally and returns its path.',
|
|
68
68
|
inputSchema: z.object({
|
|
69
69
|
paths: z
|
|
70
70
|
.array(z.string())
|
|
@@ -113,7 +113,7 @@ server.registerTool('exactpdf_merge_pdfs', {
|
|
|
113
113
|
};
|
|
114
114
|
});
|
|
115
115
|
server.registerTool('exactpdf_split_pdf', {
|
|
116
|
-
description: 'Split one PDF
|
|
116
|
+
description: 'Split one local PDF into a ZIP of PDFs. Costs 1 credit on success. Default is one PDF per page; use at_pages for split points or ranges_json for explicit inclusive ranges.',
|
|
117
117
|
inputSchema: z.object({
|
|
118
118
|
path: z.string().describe('Absolute path to a PDF file'),
|
|
119
119
|
at_pages: z
|
|
@@ -166,7 +166,7 @@ server.registerTool('exactpdf_split_pdf', {
|
|
|
166
166
|
};
|
|
167
167
|
});
|
|
168
168
|
server.registerTool('exactpdf_rotate_pdf', {
|
|
169
|
-
description: 'Rotate every page of a PDF
|
|
169
|
+
description: 'Rotate every page of a local PDF by 90/180/270 degrees. Costs 1 credit on success. Saves the rotated PDF locally and returns its path.',
|
|
170
170
|
inputSchema: z.object({
|
|
171
171
|
path: z.string().describe('Absolute path to a PDF file'),
|
|
172
172
|
angle: z
|
|
@@ -211,7 +211,7 @@ server.registerTool('exactpdf_rotate_pdf', {
|
|
|
211
211
|
};
|
|
212
212
|
});
|
|
213
213
|
server.registerTool('exactpdf_compress_pdf', {
|
|
214
|
-
description: 'Compress
|
|
214
|
+
description: 'Compress/repack a local PDF for smaller delivery. Costs 1 credit on success. Best for structure/object cleanup; image-heavy PDFs may need ExactPDF Max target-size compression.',
|
|
215
215
|
inputSchema: z.object({
|
|
216
216
|
path: z.string().describe('Absolute path to a PDF file'),
|
|
217
217
|
}),
|
|
@@ -252,7 +252,7 @@ server.registerTool('exactpdf_compress_pdf', {
|
|
|
252
252
|
};
|
|
253
253
|
});
|
|
254
254
|
server.registerTool('exactpdf_images_to_pdf', {
|
|
255
|
-
description: '
|
|
255
|
+
description: 'Convert local PNG/JPEG images into one ordered PDF. Costs 1 credit on success. Saves the resulting PDF locally and returns its path.',
|
|
256
256
|
inputSchema: z.object({
|
|
257
257
|
paths: z.array(z.string()).min(1).describe('Absolute paths to PNG or JPEG files'),
|
|
258
258
|
}),
|
|
@@ -302,7 +302,7 @@ server.registerTool('exactpdf_images_to_pdf', {
|
|
|
302
302
|
};
|
|
303
303
|
});
|
|
304
304
|
server.registerTool('exactpdf_pdf_info', {
|
|
305
|
-
description: 'Read page count and
|
|
305
|
+
description: 'Read PDF page count and metadata before deciding what to do next. Free; does not consume credits. Good first step before merge/split/speech jobs.',
|
|
306
306
|
inputSchema: z.object({
|
|
307
307
|
path: z.string().describe('Absolute path to a PDF file'),
|
|
308
308
|
}),
|
|
@@ -324,7 +324,7 @@ server.registerTool('exactpdf_pdf_info', {
|
|
|
324
324
|
};
|
|
325
325
|
});
|
|
326
326
|
server.registerTool('exactpdf_extract_text', {
|
|
327
|
-
description: 'Extract plain text from a PDF
|
|
327
|
+
description: 'Extract plain text from a local PDF. Costs 1 credit on success. Returns JSON with text and page_count for summarizers, search, and downstream agents.',
|
|
328
328
|
inputSchema: z.object({
|
|
329
329
|
path: z.string().describe('Absolute path to a PDF file'),
|
|
330
330
|
}),
|
|
@@ -358,7 +358,7 @@ server.registerTool('exactpdf_extract_text', {
|
|
|
358
358
|
};
|
|
359
359
|
});
|
|
360
360
|
server.registerTool('exactpdf_pdf_structured_markdown', {
|
|
361
|
-
description: 'Convert PDF
|
|
361
|
+
description: 'Convert a local PDF into structured Markdown JSON. Costs 1 credit on success. Use developer for docs/API PDFs, academic for papers, and rag for retrieval chunks.',
|
|
362
362
|
inputSchema: z.object({
|
|
363
363
|
path: z.string().describe('Absolute path to a PDF file'),
|
|
364
364
|
mode: z
|
|
@@ -398,7 +398,7 @@ server.registerTool('exactpdf_pdf_structured_markdown', {
|
|
|
398
398
|
};
|
|
399
399
|
});
|
|
400
400
|
server.registerTool('exactpdf_pdf_to_audiobook', {
|
|
401
|
-
description: '
|
|
401
|
+
description: 'Queue an async PDF-to-audiobook job with chapter-aware narration. Metered by generated minutes. Returns job_id/status_url; poll with exactpdf_get_speech_job and download when succeeded.',
|
|
402
402
|
inputSchema: z.object({
|
|
403
403
|
path: z.string().describe('Absolute path to a PDF file'),
|
|
404
404
|
output_format: z.enum(['mp3', 'wav', 'zip']).optional().describe('Audio export format. Default: mp3.'),
|
|
@@ -472,7 +472,7 @@ server.registerTool('exactpdf_pdf_to_audiobook', {
|
|
|
472
472
|
};
|
|
473
473
|
});
|
|
474
474
|
server.registerTool('exactpdf_job_status', {
|
|
475
|
-
description: 'Poll
|
|
475
|
+
description: 'Poll a general ExactPDF async job and optionally save the result when succeeded. Free. Prefer exactpdf_get_speech_job for speech/audiobook jobs.',
|
|
476
476
|
inputSchema: z.object({
|
|
477
477
|
job_id: z.string().describe('ExactPDF async job id returned by exactpdf_pdf_to_audiobook.'),
|
|
478
478
|
download: z.boolean().optional().describe('When true, save result_url to EXACTPDF_API_OUTPUT_DIR.'),
|
|
@@ -524,7 +524,7 @@ server.registerTool('exactpdf_job_status', {
|
|
|
524
524
|
};
|
|
525
525
|
});
|
|
526
526
|
server.registerTool('exactpdf_translate_and_speak', {
|
|
527
|
-
description: '
|
|
527
|
+
description: 'Queue async translate-and-speak: translate PDF content, preserve structure, then generate native-accent audio. Costs more than standard speech; start with hi/es/fr. Poll with exactpdf_get_speech_job.',
|
|
528
528
|
inputSchema: z.object({
|
|
529
529
|
path: z.string().describe('Absolute path to a PDF file'),
|
|
530
530
|
target_language: z.string().describe('Target language code, e.g. hi, es, fr, mr, de, ja, ar.'),
|
|
@@ -594,7 +594,7 @@ server.registerTool('exactpdf_translate_and_speak', {
|
|
|
594
594
|
};
|
|
595
595
|
});
|
|
596
596
|
server.registerTool('exactpdf_presentation_narration', {
|
|
597
|
-
description: '
|
|
597
|
+
description: 'Queue audio-first presentation narration for a PDF deck. Each page becomes a narrated slide track in a ZIP with manifest timings. Metered by generated minutes.',
|
|
598
598
|
inputSchema: z.object({
|
|
599
599
|
path: z.string().describe('Absolute path to a PDF presentation file'),
|
|
600
600
|
output_format: z.enum(['mp3', 'wav', 'zip']).optional().describe('Default: zip for per-slide tracks and manifest.'),
|
|
@@ -775,13 +775,13 @@ async function pollSpeechJob(jobId, download, downloadEndpoint = false) {
|
|
|
775
775
|
};
|
|
776
776
|
}
|
|
777
777
|
server.registerTool('exactpdf_estimate_speech_cost', {
|
|
778
|
-
description: 'Estimate
|
|
778
|
+
description: 'Estimate generated minutes and credits before submitting a paid PDF speech/audiobook/translation job. Local-only rough estimate; free and safe to call repeatedly.',
|
|
779
779
|
inputSchema: z.object({
|
|
780
780
|
path: z.string().optional().describe('Absolute path to a PDF file. Used for a rough size-based duration estimate.'),
|
|
781
781
|
characters: z.number().int().positive().optional().describe('Known narration character count, if already extracted.'),
|
|
782
782
|
minutes: z.number().positive().optional().describe('Known generated minutes, if already estimated.'),
|
|
783
783
|
mode: z.enum(['speech', 'audiobook', 'presentation', 'translate']).optional().describe('Default: audiobook.'),
|
|
784
|
-
target_language: z.string().optional().describe('
|
|
784
|
+
target_language: z.string().optional().describe('Only set when mode is translate. Leave empty for English speech/audiobook/presentation estimates.'),
|
|
785
785
|
}),
|
|
786
786
|
}, async ({ path, characters, minutes, mode, target_language }) => {
|
|
787
787
|
let estimatedChars = characters ?? 0;
|
|
@@ -792,7 +792,7 @@ server.registerTool('exactpdf_estimate_speech_cost', {
|
|
|
792
792
|
estimatedChars = Math.max(1_000, Math.round(info.size / 8));
|
|
793
793
|
}
|
|
794
794
|
const estimatedMinutes = minutes ?? Math.max(1, Math.ceil(estimatedChars / 900));
|
|
795
|
-
const selectedMode =
|
|
795
|
+
const selectedMode = mode === 'translate' ? 'translate' : (mode ?? 'audiobook');
|
|
796
796
|
const creditsPerMinute = selectedMode === 'translate' ? 3 : 1;
|
|
797
797
|
const credits = Math.max(1, estimatedMinutes * creditsPerMinute);
|
|
798
798
|
return {
|
|
@@ -808,7 +808,9 @@ server.registerTool('exactpdf_estimate_speech_cost', {
|
|
|
808
808
|
generated_minutes: estimatedMinutes,
|
|
809
809
|
credits_per_minute: creditsPerMinute,
|
|
810
810
|
current_credit_cost: credits,
|
|
811
|
-
|
|
811
|
+
target_language_applied: selectedMode === 'translate' ? (target_language ?? null) : null,
|
|
812
|
+
ignored_target_language: selectedMode !== 'translate' && target_language ? target_language : null,
|
|
813
|
+
note: 'ExactPDF charges async speech jobs by estimated generated minutes: 1 credit/minute for standard speech/audiobook/presentation, 3 credits/minute for translate-and-speak. A target language only changes pricing when mode is translate. Path-based estimates are rough until the API extracts PDF text.',
|
|
812
814
|
},
|
|
813
815
|
}, null, 2),
|
|
814
816
|
},
|
|
@@ -816,7 +818,7 @@ server.registerTool('exactpdf_estimate_speech_cost', {
|
|
|
816
818
|
};
|
|
817
819
|
});
|
|
818
820
|
server.registerTool('exactpdf_voice_preview', {
|
|
819
|
-
description: 'Generate a short
|
|
821
|
+
description: 'Generate a short MP3/WAV voice sample before spending credits on a full PDF speech job. Free. Saves audio locally so users can judge quality first.',
|
|
820
822
|
inputSchema: z.object({
|
|
821
823
|
text: z.string().optional().describe('Preview text, capped server-side at 600 characters.'),
|
|
822
824
|
output_format: z.enum(['mp3', 'wav']).optional().describe('Audio preview format. Default: mp3.'),
|
|
@@ -867,22 +869,22 @@ server.registerTool('exactpdf_voice_preview', {
|
|
|
867
869
|
};
|
|
868
870
|
});
|
|
869
871
|
server.registerTool('exactpdf_pdf_to_speech', {
|
|
870
|
-
description: '
|
|
872
|
+
description: 'Queue async PDF-to-speech for reports, study notes, or accessibility audio. Metered by generated minutes. Returns job_id; poll exactpdf_get_speech_job.',
|
|
871
873
|
inputSchema: speechJobSchema,
|
|
872
874
|
}, async (args) => submitSpeechJob('/api/v1/pdf-to-speech', args, 'pdf-to-speech'));
|
|
873
875
|
server.registerTool('exactpdf_generate_audiobook', {
|
|
874
|
-
description: '
|
|
876
|
+
description: 'Queue async audiobook generation with voice style, page range, chapters, pause normalization, and optional webhook. Metered by generated minutes.',
|
|
875
877
|
inputSchema: speechJobSchema,
|
|
876
878
|
}, async (args) => submitSpeechJob('/api/v1/generate-audiobook', args, 'generate-audiobook'));
|
|
877
879
|
server.registerTool('exactpdf_get_speech_job', {
|
|
878
|
-
description: 'Poll /
|
|
880
|
+
description: 'Poll a speech/audiobook/translation/presentation job. Free. Returns status, progress, error details, and result_url when succeeded; set download=true to save result_url locally.',
|
|
879
881
|
inputSchema: z.object({
|
|
880
882
|
job_id: z.string().describe('ExactPDF speech job id.'),
|
|
881
883
|
download: z.boolean().optional().describe('When true, save result_url to EXACTPDF_API_OUTPUT_DIR if job succeeded.'),
|
|
882
884
|
}),
|
|
883
885
|
}, async ({ job_id, download }) => pollSpeechJob(job_id, Boolean(download)));
|
|
884
886
|
server.registerTool('exactpdf_download_audio', {
|
|
885
|
-
description: 'Download a succeeded speech job
|
|
887
|
+
description: 'Download a succeeded speech/audiobook job and save the audio/ZIP locally. Free. Use only after exactpdf_get_speech_job reports succeeded.',
|
|
886
888
|
inputSchema: z.object({
|
|
887
889
|
job_id: z.string().describe('ExactPDF speech job id.'),
|
|
888
890
|
}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exactpdf/mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"description": "MCP server for ExactPDF — PDF tools, voice previews, async PDF speech/audiobook jobs, polling, and API credits.",
|
|
5
5
|
"mcpName": "com.exactpdf/mcp",
|
|
6
6
|
"type": "module",
|
|
@@ -17,8 +17,7 @@
|
|
|
17
17
|
"server.json"
|
|
18
18
|
],
|
|
19
19
|
"publishConfig": {
|
|
20
|
-
"access": "public"
|
|
21
|
-
"provenance": true
|
|
20
|
+
"access": "public"
|
|
22
21
|
},
|
|
23
22
|
"homepage": "https://exactpdf.com/docs/api",
|
|
24
23
|
"bugs": {
|
package/server.json
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
"name": "com.exactpdf/mcp",
|
|
4
4
|
"title": "ExactPDF",
|
|
5
5
|
"description": "Agent-facing PDF API: merge, split, rotate, compress, images, metadata, text, Markdown, voice previews, async speech/audiobook, multilingual speech, and presentation narration jobs.",
|
|
6
|
-
"version": "0.2.
|
|
6
|
+
"version": "0.2.9",
|
|
7
7
|
"websiteUrl": "https://exactpdf.com/docs/api",
|
|
8
8
|
"packages": [
|
|
9
9
|
{
|
|
10
10
|
"registryType": "npm",
|
|
11
11
|
"identifier": "@exactpdf/mcp",
|
|
12
|
-
"version": "0.2.
|
|
12
|
+
"version": "0.2.9",
|
|
13
13
|
"transport": {
|
|
14
14
|
"type": "stdio"
|
|
15
15
|
}
|