@exactpdf/mcp 0.2.11 → 0.2.13
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 +34 -42
- package/dist/run.d.ts +4 -1
- package/dist/run.js +34 -185
- package/package.json +13 -4
- package/server.json +3 -3
package/README.md
CHANGED
|
@@ -4,13 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
Model Context Protocol (stdio) server for **ExactPDF** — production PDF tools for **Cursor**, **Claude Desktop**, **Codex**, and any MCP client.
|
|
6
6
|
|
|
7
|
-
Use it when an agent needs to inspect, merge, split, rotate, compress, extract
|
|
7
|
+
Use it when an agent needs to inspect, merge, split, rotate, compress, extract text, export structured Markdown for RAG, or build images-to-PDF workflows without writing fragile PDF plumbing.
|
|
8
8
|
|
|
9
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.
|
|
10
10
|
|
|
11
|
+
Developer quickstart: [https://exactpdf.com/developers](https://exactpdf.com/developers)
|
|
12
|
+
|
|
11
13
|
## Setup
|
|
12
14
|
|
|
13
|
-
1. Create an API key at [exactpdf.com](https://exactpdf.com)
|
|
15
|
+
1. Create an API key at [max.exactpdf.com/account](https://max.exactpdf.com/account). New accounts get **20 free test credits**. Buy one-time credit packs only after the workflow works; no subscription is required for API/MCP.
|
|
14
16
|
2. Export:
|
|
15
17
|
|
|
16
18
|
```bash
|
|
@@ -55,10 +57,20 @@ Safe first run:
|
|
|
55
57
|
2. exactpdf_account
|
|
56
58
|
3. exactpdf_pdf_info(path="/absolute/path/document.pdf")
|
|
57
59
|
4. exactpdf_voice_preview(text="Short sample...", voice_style="professional")
|
|
58
|
-
5. exactpdf_estimate_speech_cost(path="/absolute/path/document.pdf", mode="audiobook")
|
|
60
|
+
5. exactpdf_estimate_speech_cost(path="/absolute/path/document.pdf", mode="audiobook") # planning only while background audio is hardened
|
|
59
61
|
```
|
|
60
62
|
|
|
61
|
-
Document outputs are saved to `EXACTPDF_API_OUTPUT_DIR` or your OS temp directory. Paid document tools consume credits only on successful output.
|
|
63
|
+
Document outputs are saved to `EXACTPDF_API_OUTPUT_DIR` or your OS temp directory. Paid document tools consume credits only on successful output.
|
|
64
|
+
|
|
65
|
+
Background PDF speech, audiobook, translate-and-speak, and presentation narration submissions are currently disabled for paid workloads while ExactPDF finishes dedicated worker hardening and queue-completion smoke. `exactpdf_voice_preview` remains available for short free samples, and `exactpdf_estimate_speech_cost` is planning-only until the API is re-enabled.
|
|
66
|
+
|
|
67
|
+
Credit model:
|
|
68
|
+
|
|
69
|
+
- Free probes: checklist, account, PDF info, voice preview, speech cost estimate, and job polling.
|
|
70
|
+
- New accounts: 20 free test credits for API/MCP workflows.
|
|
71
|
+
- Standard document tools: 1 credit only after a successful output.
|
|
72
|
+
- Background audio jobs: planning-only until dedicated worker smoke passes.
|
|
73
|
+
- One-time packs: available in Max Account when you need more; no subscription required for API/MCP.
|
|
62
74
|
|
|
63
75
|
| Tool | Endpoint | Credits |
|
|
64
76
|
|------|----------|--------:|
|
|
@@ -76,14 +88,14 @@ Document outputs are saved to `EXACTPDF_API_OUTPUT_DIR` or your OS temp director
|
|
|
76
88
|
| `exactpdf_pdf_structured_markdown` | POST /api/v1/pdf-structured-markdown | 1 |
|
|
77
89
|
| `exactpdf_estimate_speech_cost` | local estimate | 0 |
|
|
78
90
|
| `exactpdf_voice_preview` | POST /api/v1/voice-preview | 0 |
|
|
79
|
-
| `exactpdf_pdf_to_speech` | POST /api/v1/pdf-to-speech |
|
|
80
|
-
| `exactpdf_pdf_to_audiobook` | POST /api/v1/pdf-to-audiobook |
|
|
81
|
-
| `exactpdf_generate_audiobook` | POST /api/v1/generate-audiobook |
|
|
82
|
-
| `exactpdf_translate_and_speak` | POST /api/v1/translate-and-speak |
|
|
83
|
-
| `exactpdf_presentation_narration` | POST /api/v1/presentation-narration |
|
|
84
|
-
| `exactpdf_job_status` | GET /api/v1/jobs/:id | 0 |
|
|
85
|
-
| `exactpdf_get_speech_job` | GET /api/v1/speech-jobs/:id | 0 |
|
|
86
|
-
| `exactpdf_download_audio` | GET /api/v1/speech-jobs/:id/download | 0 |
|
|
91
|
+
| `exactpdf_pdf_to_speech` | POST /api/v1/pdf-to-speech | disabled / hardening |
|
|
92
|
+
| `exactpdf_pdf_to_audiobook` | POST /api/v1/pdf-to-audiobook | disabled / hardening |
|
|
93
|
+
| `exactpdf_generate_audiobook` | POST /api/v1/generate-audiobook | disabled / hardening |
|
|
94
|
+
| `exactpdf_translate_and_speak` | POST /api/v1/translate-and-speak | disabled / hardening |
|
|
95
|
+
| `exactpdf_presentation_narration` | POST /api/v1/presentation-narration | disabled / hardening |
|
|
96
|
+
| `exactpdf_job_status` | GET /api/v1/jobs/:id | 0 / legacy polling |
|
|
97
|
+
| `exactpdf_get_speech_job` | GET /api/v1/speech-jobs/:id | 0 / legacy polling |
|
|
98
|
+
| `exactpdf_download_audio` | GET /api/v1/speech-jobs/:id/download | 0 / legacy download |
|
|
87
99
|
|
|
88
100
|
## Tool behavior
|
|
89
101
|
|
|
@@ -93,48 +105,27 @@ Document outputs are saved to `EXACTPDF_API_OUTPUT_DIR` or your OS temp director
|
|
|
93
105
|
| PDF operations | Absolute local PDF paths | Saved PDF/ZIP path plus remaining credits |
|
|
94
106
|
| Text / Markdown | Absolute local PDF path | JSON text/Markdown, page count, structured content |
|
|
95
107
|
| Voice preview | Short text, voice style/language | Saved MP3/WAV preview, no credit charge |
|
|
96
|
-
|
|
|
97
|
-
|
|
|
108
|
+
| Audio planning | Short text, character count, rough PDF size | Voice preview or estimated minutes/credits |
|
|
109
|
+
| Legacy job polling/download | `job_id` | progress/errors/result URL or saved audio/ZIP for jobs created before the hardening gate |
|
|
98
110
|
|
|
99
|
-
|
|
111
|
+
Background audio status:
|
|
100
112
|
|
|
101
113
|
```text
|
|
102
|
-
1. exactpdf_estimate_speech_cost(path="/abs/book.pdf", mode="audiobook")
|
|
114
|
+
1. exactpdf_estimate_speech_cost(path="/abs/book.pdf", mode="audiobook") # planning only
|
|
103
115
|
2. exactpdf_voice_preview(text="Read the first paragraph in an audiobook tone", voice_style="audiobook")
|
|
104
|
-
3. exactpdf_generate_audiobook
|
|
105
|
-
4. exactpdf_get_speech_job(job_id="...", download=false)
|
|
106
|
-
5. exactpdf_download_audio(job_id="...") after status is succeeded
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
`exactpdf_pdf_to_speech`, `exactpdf_pdf_to_audiobook`, and `exactpdf_generate_audiobook` accept optional `callback_url` and `webhook_secret`. ExactPDF signs webhook bodies with `x-exactpdf-signature: sha256=<hmac>` over `timestamp.body`.
|
|
110
|
-
|
|
111
|
-
Multilingual speech flow:
|
|
112
|
-
|
|
113
|
-
```text
|
|
114
|
-
1. exactpdf_translate_and_speak(path="/abs/training.pdf", target_language="hi")
|
|
115
|
-
2. exactpdf_job_status(job_id="...", download=false)
|
|
116
|
-
3. exactpdf_job_status(job_id="...", download=true) after status is succeeded
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
`segmentation_mode="pages"` is useful for presentation narration because each PDF page becomes a trackable audio section.
|
|
120
|
-
|
|
121
|
-
Presentation narration flow:
|
|
122
|
-
|
|
123
|
-
```text
|
|
124
|
-
1. exactpdf_presentation_narration(path="/abs/deck.pdf")
|
|
125
|
-
2. exactpdf_job_status(job_id="...", download=true) after status is succeeded
|
|
116
|
+
3. Do not call exactpdf_generate_audiobook / exactpdf_pdf_to_speech for paid workloads until ExactPDF announces worker smoke has passed.
|
|
126
117
|
```
|
|
127
118
|
|
|
128
|
-
The
|
|
119
|
+
The disabled background audio tools intentionally return an MCP error message before submitting work. This protects users from paying for a path that is still being hardened.
|
|
129
120
|
|
|
130
121
|
## Reliability rules for agent builders
|
|
131
122
|
|
|
132
123
|
- Always call `exactpdf_account` once before paid tools.
|
|
133
124
|
- Use absolute file paths. Relative paths are client-dependent and may fail.
|
|
134
125
|
- Use `exactpdf_pdf_info` before large jobs to confirm the file is readable.
|
|
135
|
-
- Use `exactpdf_estimate_speech_cost`
|
|
136
|
-
- Use `exactpdf_voice_preview` before
|
|
137
|
-
-
|
|
126
|
+
- Use `exactpdf_estimate_speech_cost` for audio planning only until worker smoke passes.
|
|
127
|
+
- Use `exactpdf_voice_preview` before making any narration-quality decision.
|
|
128
|
+
- Do not promise PDF-to-MP3/audiobook output through MCP until background audio is re-enabled.
|
|
138
129
|
- If a tool returns `isError`, show the HTTP status and response body to the user. The API returns structured error details.
|
|
139
130
|
- Keep API keys out of prompts, logs, screenshots, and commits.
|
|
140
131
|
|
|
@@ -151,6 +142,7 @@ The package must exist on the public npm registry before the MCP Registry can ve
|
|
|
151
142
|
|
|
152
143
|
## Docs
|
|
153
144
|
|
|
145
|
+
- [https://exactpdf.com/developers](https://exactpdf.com/developers)
|
|
154
146
|
- [https://exactpdf.com/docs/api](https://exactpdf.com/docs/api)
|
|
155
147
|
|
|
156
148
|
## License
|
package/dist/run.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* ExactPDF MCP — stdio server for Cursor / Claude Desktop / Codex.
|
|
4
|
-
* Requires EXACTPDF_API_KEY (sk_live_…) from exactpdf.com
|
|
4
|
+
* Requires EXACTPDF_API_KEY (sk_live_…) from max.exactpdf.com/account.
|
|
5
|
+
* New accounts get free test credits; one-time credit packs are available
|
|
6
|
+
* after the first workflow works.
|
|
5
7
|
*
|
|
8
|
+
* @see https://exactpdf.com/developers
|
|
6
9
|
* @see https://exactpdf.com/docs/api
|
|
7
10
|
*/
|
|
8
11
|
export {};
|
package/dist/run.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* ExactPDF MCP — stdio server for Cursor / Claude Desktop / Codex.
|
|
4
|
-
* Requires EXACTPDF_API_KEY (sk_live_…) from exactpdf.com
|
|
4
|
+
* Requires EXACTPDF_API_KEY (sk_live_…) from max.exactpdf.com/account.
|
|
5
|
+
* New accounts get free test credits; one-time credit packs are available
|
|
6
|
+
* after the first workflow works.
|
|
5
7
|
*
|
|
8
|
+
* @see https://exactpdf.com/developers
|
|
6
9
|
* @see https://exactpdf.com/docs/api
|
|
7
10
|
*/
|
|
8
11
|
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
@@ -20,12 +23,13 @@ function outputDir() {
|
|
|
20
23
|
function requireKey() {
|
|
21
24
|
const k = process.env.EXACTPDF_API_KEY?.trim();
|
|
22
25
|
if (!k) {
|
|
23
|
-
throw new Error('EXACTPDF_API_KEY is not set. Create a key at exactpdf.com
|
|
26
|
+
throw new Error('EXACTPDF_API_KEY is not set. Create a key with free test credits at https://max.exactpdf.com/account, then export EXACTPDF_API_KEY=sk_live_…. Quickstart: https://exactpdf.com/developers');
|
|
24
27
|
}
|
|
25
28
|
return k;
|
|
26
29
|
}
|
|
27
|
-
const
|
|
28
|
-
|
|
30
|
+
const BACKGROUND_AUDIO_HARDENING_MESSAGE = 'Background PDF speech, audiobook, translate-and-speak, and presentation narration jobs are currently disabled for paid workloads while ExactPDF finishes dedicated worker hardening and queue-completion smoke. Use exactpdf_voice_preview for short free voice samples and exactpdf_estimate_speech_cost for planning only. Do not promise MP3/audiobook delivery through MCP until the API returns enabled status again.';
|
|
31
|
+
const server = new McpServer({ name: 'exactpdf', version: '0.2.13' }, {
|
|
32
|
+
instructions: 'ExactPDF turns local PDFs into production outputs for agents: inspect metadata, merge/split/rotate/compress, create portal-ready merge/images workflows, extract text, export structured Markdown for RAG, and create short voice previews. Start with exactpdf_first_run_checklist, then exactpdf_account and exactpdf_pdf_info before paid work. New users can create a key with free test credits at https://max.exactpdf.com/account; developer quickstart: https://exactpdf.com/developers. Free tools: first-run checklist, account, pdf-info, voice-preview, and speech cost estimate. Standard document tools cost 1 credit on success, including launch portal-ready workflows. Background PDF speech/audiobook/translation/presentation jobs are in production hardening and must be treated as planning-only until worker smoke passes. Use absolute local file paths; binary outputs are saved to EXACTPDF_API_OUTPUT_DIR or the OS temp directory.',
|
|
29
33
|
});
|
|
30
34
|
async function saveBinaryFromResponse(res, prefix, fallbackExt) {
|
|
31
35
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
@@ -68,7 +72,8 @@ server.registerTool('exactpdf_first_run_checklist', {
|
|
|
68
72
|
ok: true,
|
|
69
73
|
setup: {
|
|
70
74
|
required_env: 'EXACTPDF_API_KEY=sk_live_...',
|
|
71
|
-
create_key_url:
|
|
75
|
+
create_key_url: 'https://max.exactpdf.com/account',
|
|
76
|
+
developer_quickstart: `${BASE}/developers`,
|
|
72
77
|
optional_output_dir: 'EXACTPDF_API_OUTPUT_DIR=/absolute/output/folder',
|
|
73
78
|
docs: `${BASE}/docs/api`,
|
|
74
79
|
openapi: `${BASE}/openapi.json`,
|
|
@@ -76,20 +81,22 @@ server.registerTool('exactpdf_first_run_checklist', {
|
|
|
76
81
|
safe_first_run: [
|
|
77
82
|
'Call exactpdf_account to verify the key and credit balance.',
|
|
78
83
|
'Call exactpdf_pdf_info with a small absolute PDF path before extraction or conversion.',
|
|
79
|
-
'Call exactpdf_voice_preview with a short text sample before
|
|
80
|
-
'Call exactpdf_estimate_speech_cost
|
|
84
|
+
'Call exactpdf_voice_preview with a short text sample before any narration-quality decision.',
|
|
85
|
+
'Call exactpdf_estimate_speech_cost only for planning; background audio submission is disabled until worker smoke passes.',
|
|
81
86
|
],
|
|
82
87
|
credit_model: {
|
|
83
88
|
free: ['exactpdf_first_run_checklist', 'exactpdf_account', 'exactpdf_pdf_info', 'exactpdf_voice_preview', 'exactpdf_estimate_speech_cost', 'job polling'],
|
|
89
|
+
starter_trial: 'New accounts get 20 free test credits for API/MCP workflows.',
|
|
84
90
|
standard_document_tools: '1 credit only after successful output',
|
|
91
|
+
credit_packs: 'Buy one-time credit packs when the workflow works; subscription is not required for API/MCP.',
|
|
85
92
|
launch_portal_ready_workflows: '1 credit on successful output: exactpdf_merge_and_compress_pdfs, exactpdf_images_to_pdf_and_compress',
|
|
86
|
-
speech_audiobook_presentation: '
|
|
87
|
-
translate_and_speak: '
|
|
93
|
+
speech_audiobook_presentation: 'Planning only while background audio jobs are disabled for production hardening.',
|
|
94
|
+
translate_and_speak: 'Planning only while background audio jobs are disabled for production hardening.',
|
|
88
95
|
},
|
|
89
96
|
reliability_rules: [
|
|
90
97
|
'Use absolute local file paths; relative paths are client-dependent.',
|
|
91
98
|
'Never paste API keys into prompts, screenshots, commits, or logs.',
|
|
92
|
-
'
|
|
99
|
+
'Background audio tools intentionally return a hardening/disabled message until worker smoke passes.',
|
|
93
100
|
'If a tool returns isError, show the HTTP status and response body to the user.',
|
|
94
101
|
],
|
|
95
102
|
}, null, 2),
|
|
@@ -529,7 +536,7 @@ server.registerTool('exactpdf_pdf_structured_markdown', {
|
|
|
529
536
|
};
|
|
530
537
|
});
|
|
531
538
|
server.registerTool('exactpdf_pdf_to_audiobook', {
|
|
532
|
-
description: '
|
|
539
|
+
description: 'Planning-only placeholder while background audio workers are hardened. Do not use for paid workloads; use exactpdf_voice_preview and exactpdf_estimate_speech_cost instead.',
|
|
533
540
|
inputSchema: z.object({
|
|
534
541
|
path: z.string().describe('Absolute path to a PDF file'),
|
|
535
542
|
output_format: z.enum(['mp3', 'wav', 'zip']).optional().describe('Audio export format. Default: mp3.'),
|
|
@@ -548,58 +555,14 @@ server.registerTool('exactpdf_pdf_to_audiobook', {
|
|
|
548
555
|
webhook_secret: z.string().min(12).max(256).optional().describe('Optional webhook HMAC signing secret.'),
|
|
549
556
|
}),
|
|
550
557
|
}, async ({ path, output_format, voice_style, voice_id, language, speed, page_range, normalize_pauses, preserve_chapters, pronunciation_fixes, callback_url, webhook_secret, }) => {
|
|
551
|
-
const key = requireKey();
|
|
552
|
-
const form = new FormData();
|
|
553
|
-
form.append('file', new Blob([await readFile(path)], { type: 'application/pdf' }), basename(path));
|
|
554
|
-
if (output_format)
|
|
555
|
-
form.append('output_format', output_format);
|
|
556
|
-
if (voice_style)
|
|
557
|
-
form.append('voice_style', voice_style);
|
|
558
|
-
if (voice_id)
|
|
559
|
-
form.append('voice_id', voice_id);
|
|
560
|
-
if (language)
|
|
561
|
-
form.append('language', language);
|
|
562
|
-
if (typeof speed === 'number')
|
|
563
|
-
form.append('speed', String(speed));
|
|
564
|
-
if (page_range)
|
|
565
|
-
form.append('page_range', page_range);
|
|
566
|
-
if (typeof normalize_pauses === 'boolean')
|
|
567
|
-
form.append('normalize_pauses', String(normalize_pauses));
|
|
568
|
-
if (typeof preserve_chapters === 'boolean')
|
|
569
|
-
form.append('preserve_chapters', String(preserve_chapters));
|
|
570
|
-
if (pronunciation_fixes)
|
|
571
|
-
form.append('pronunciation_fixes', pronunciation_fixes);
|
|
572
|
-
if (callback_url)
|
|
573
|
-
form.append('callback_url', callback_url);
|
|
574
|
-
if (webhook_secret)
|
|
575
|
-
form.append('webhook_secret', webhook_secret);
|
|
576
|
-
const res = await fetch(`${BASE}/api/v1/pdf-to-audiobook`, {
|
|
577
|
-
method: 'POST',
|
|
578
|
-
headers: {
|
|
579
|
-
Authorization: `Bearer ${key}`,
|
|
580
|
-
Accept: 'application/json',
|
|
581
|
-
},
|
|
582
|
-
body: form,
|
|
583
|
-
});
|
|
584
|
-
const raw = await res.text();
|
|
585
|
-
if (!res.ok) {
|
|
586
|
-
return {
|
|
587
|
-
content: [
|
|
588
|
-
{
|
|
589
|
-
type: 'text',
|
|
590
|
-
text: `pdf-to-audiobook failed HTTP ${res.status}\n${raw.slice(0, 8000)}`,
|
|
591
|
-
},
|
|
592
|
-
],
|
|
593
|
-
isError: true,
|
|
594
|
-
};
|
|
595
|
-
}
|
|
596
558
|
return {
|
|
597
559
|
content: [
|
|
598
560
|
{
|
|
599
561
|
type: 'text',
|
|
600
|
-
text: `
|
|
562
|
+
text: `pdf-to-audiobook disabled: ${BACKGROUND_AUDIO_HARDENING_MESSAGE}`,
|
|
601
563
|
},
|
|
602
564
|
],
|
|
565
|
+
isError: true,
|
|
603
566
|
};
|
|
604
567
|
});
|
|
605
568
|
server.registerTool('exactpdf_job_status', {
|
|
@@ -655,7 +618,7 @@ server.registerTool('exactpdf_job_status', {
|
|
|
655
618
|
};
|
|
656
619
|
});
|
|
657
620
|
server.registerTool('exactpdf_translate_and_speak', {
|
|
658
|
-
description: '
|
|
621
|
+
description: 'Planning-only placeholder while translate-and-speak workers are hardened. Do not use for paid workloads; use estimates for planning only.',
|
|
659
622
|
inputSchema: z.object({
|
|
660
623
|
path: z.string().describe('Absolute path to a PDF file'),
|
|
661
624
|
target_language: z.string().describe('Target language code, e.g. hi, es, fr, mr, de, ja, ar.'),
|
|
@@ -673,59 +636,18 @@ server.registerTool('exactpdf_translate_and_speak', {
|
|
|
673
636
|
webhook_secret: z.string().min(12).max(256).optional().describe('Optional webhook HMAC signing secret.'),
|
|
674
637
|
}),
|
|
675
638
|
}, async ({ path, target_language, source_language, output_format, voice_style, speed, page_range, segmentation_mode, translation_glossary, callback_url, webhook_secret, }) => {
|
|
676
|
-
const key = requireKey();
|
|
677
|
-
const form = new FormData();
|
|
678
|
-
form.append('file', new Blob([await readFile(path)], { type: 'application/pdf' }), basename(path));
|
|
679
|
-
form.append('target_language', target_language);
|
|
680
|
-
if (source_language)
|
|
681
|
-
form.append('source_language', source_language);
|
|
682
|
-
if (output_format)
|
|
683
|
-
form.append('output_format', output_format);
|
|
684
|
-
if (voice_style)
|
|
685
|
-
form.append('voice_style', voice_style);
|
|
686
|
-
if (typeof speed === 'number')
|
|
687
|
-
form.append('speed', String(speed));
|
|
688
|
-
if (page_range)
|
|
689
|
-
form.append('page_range', page_range);
|
|
690
|
-
if (segmentation_mode)
|
|
691
|
-
form.append('segmentation_mode', segmentation_mode);
|
|
692
|
-
if (translation_glossary)
|
|
693
|
-
form.append('translation_glossary', translation_glossary);
|
|
694
|
-
if (callback_url)
|
|
695
|
-
form.append('callback_url', callback_url);
|
|
696
|
-
if (webhook_secret)
|
|
697
|
-
form.append('webhook_secret', webhook_secret);
|
|
698
|
-
const res = await fetch(`${BASE}/api/v1/translate-and-speak`, {
|
|
699
|
-
method: 'POST',
|
|
700
|
-
headers: {
|
|
701
|
-
Authorization: `Bearer ${key}`,
|
|
702
|
-
Accept: 'application/json',
|
|
703
|
-
},
|
|
704
|
-
body: form,
|
|
705
|
-
});
|
|
706
|
-
const raw = await res.text();
|
|
707
|
-
if (!res.ok) {
|
|
708
|
-
return {
|
|
709
|
-
content: [
|
|
710
|
-
{
|
|
711
|
-
type: 'text',
|
|
712
|
-
text: `translate-and-speak failed HTTP ${res.status}\n${raw.slice(0, 8000)}`,
|
|
713
|
-
},
|
|
714
|
-
],
|
|
715
|
-
isError: true,
|
|
716
|
-
};
|
|
717
|
-
}
|
|
718
639
|
return {
|
|
719
640
|
content: [
|
|
720
641
|
{
|
|
721
642
|
type: 'text',
|
|
722
|
-
text: `
|
|
643
|
+
text: `translate-and-speak disabled: ${BACKGROUND_AUDIO_HARDENING_MESSAGE}`,
|
|
723
644
|
},
|
|
724
645
|
],
|
|
646
|
+
isError: true,
|
|
725
647
|
};
|
|
726
648
|
});
|
|
727
649
|
server.registerTool('exactpdf_presentation_narration', {
|
|
728
|
-
description: '
|
|
650
|
+
description: 'Planning-only placeholder while presentation narration workers are hardened. Do not use for paid workloads; use estimates for planning only.',
|
|
729
651
|
inputSchema: z.object({
|
|
730
652
|
path: z.string().describe('Absolute path to a PDF presentation file'),
|
|
731
653
|
output_format: z.enum(['mp3', 'wav', 'zip']).optional().describe('Default: zip for per-slide tracks and manifest.'),
|
|
@@ -739,48 +661,14 @@ server.registerTool('exactpdf_presentation_narration', {
|
|
|
739
661
|
webhook_secret: z.string().min(12).max(256).optional().describe('Optional webhook HMAC signing secret.'),
|
|
740
662
|
}),
|
|
741
663
|
}, async ({ path, output_format, voice_style, speed, page_range, callback_url, webhook_secret }) => {
|
|
742
|
-
const key = requireKey();
|
|
743
|
-
const form = new FormData();
|
|
744
|
-
form.append('file', new Blob([await readFile(path)], { type: 'application/pdf' }), basename(path));
|
|
745
|
-
if (output_format)
|
|
746
|
-
form.append('output_format', output_format);
|
|
747
|
-
if (voice_style)
|
|
748
|
-
form.append('voice_style', voice_style);
|
|
749
|
-
if (typeof speed === 'number')
|
|
750
|
-
form.append('speed', String(speed));
|
|
751
|
-
if (page_range)
|
|
752
|
-
form.append('page_range', page_range);
|
|
753
|
-
if (callback_url)
|
|
754
|
-
form.append('callback_url', callback_url);
|
|
755
|
-
if (webhook_secret)
|
|
756
|
-
form.append('webhook_secret', webhook_secret);
|
|
757
|
-
const res = await fetch(`${BASE}/api/v1/presentation-narration`, {
|
|
758
|
-
method: 'POST',
|
|
759
|
-
headers: {
|
|
760
|
-
Authorization: `Bearer ${key}`,
|
|
761
|
-
Accept: 'application/json',
|
|
762
|
-
},
|
|
763
|
-
body: form,
|
|
764
|
-
});
|
|
765
|
-
const raw = await res.text();
|
|
766
|
-
if (!res.ok) {
|
|
767
|
-
return {
|
|
768
|
-
content: [
|
|
769
|
-
{
|
|
770
|
-
type: 'text',
|
|
771
|
-
text: `presentation-narration failed HTTP ${res.status}\n${raw.slice(0, 8000)}`,
|
|
772
|
-
},
|
|
773
|
-
],
|
|
774
|
-
isError: true,
|
|
775
|
-
};
|
|
776
|
-
}
|
|
777
664
|
return {
|
|
778
665
|
content: [
|
|
779
666
|
{
|
|
780
667
|
type: 'text',
|
|
781
|
-
text: `
|
|
668
|
+
text: `presentation-narration disabled: ${BACKGROUND_AUDIO_HARDENING_MESSAGE}`,
|
|
782
669
|
},
|
|
783
670
|
],
|
|
671
|
+
isError: true,
|
|
784
672
|
};
|
|
785
673
|
});
|
|
786
674
|
const speechJobSchema = z.object({
|
|
@@ -801,48 +689,9 @@ const speechJobSchema = z.object({
|
|
|
801
689
|
webhook_secret: z.string().min(12).max(256).optional().describe('Optional webhook HMAC signing secret.'),
|
|
802
690
|
});
|
|
803
691
|
async function submitSpeechJob(endpoint, args, label) {
|
|
804
|
-
const key = requireKey();
|
|
805
|
-
const form = new FormData();
|
|
806
|
-
form.append('file', new Blob([await readFile(args.path)], { type: 'application/pdf' }), basename(args.path));
|
|
807
|
-
if (args.output_format)
|
|
808
|
-
form.append('output_format', args.output_format);
|
|
809
|
-
if (args.voice_style)
|
|
810
|
-
form.append('voice_style', args.voice_style);
|
|
811
|
-
if (args.voice_id)
|
|
812
|
-
form.append('voice_id', args.voice_id);
|
|
813
|
-
if (args.language)
|
|
814
|
-
form.append('language', args.language);
|
|
815
|
-
if (typeof args.speed === 'number')
|
|
816
|
-
form.append('speed', String(args.speed));
|
|
817
|
-
if (args.page_range)
|
|
818
|
-
form.append('page_range', args.page_range);
|
|
819
|
-
if (typeof args.normalize_pauses === 'boolean')
|
|
820
|
-
form.append('normalize_pauses', String(args.normalize_pauses));
|
|
821
|
-
if (typeof args.preserve_chapters === 'boolean')
|
|
822
|
-
form.append('preserve_chapters', String(args.preserve_chapters));
|
|
823
|
-
if (args.pronunciation_fixes)
|
|
824
|
-
form.append('pronunciation_fixes', args.pronunciation_fixes);
|
|
825
|
-
if (args.callback_url)
|
|
826
|
-
form.append('callback_url', args.callback_url);
|
|
827
|
-
if (args.webhook_secret)
|
|
828
|
-
form.append('webhook_secret', args.webhook_secret);
|
|
829
|
-
const res = await fetch(`${BASE}${endpoint}`, {
|
|
830
|
-
method: 'POST',
|
|
831
|
-
headers: {
|
|
832
|
-
Authorization: `Bearer ${key}`,
|
|
833
|
-
Accept: 'application/json',
|
|
834
|
-
},
|
|
835
|
-
body: form,
|
|
836
|
-
});
|
|
837
|
-
const raw = await res.text();
|
|
838
|
-
if (!res.ok) {
|
|
839
|
-
return {
|
|
840
|
-
content: [{ type: 'text', text: `${label} failed HTTP ${res.status}\n${raw.slice(0, 8000)}` }],
|
|
841
|
-
isError: true,
|
|
842
|
-
};
|
|
843
|
-
}
|
|
844
692
|
return {
|
|
845
|
-
content: [{ type: 'text', text: `${label}
|
|
693
|
+
content: [{ type: 'text', text: `${label} disabled: ${BACKGROUND_AUDIO_HARDENING_MESSAGE}` }],
|
|
694
|
+
isError: true,
|
|
846
695
|
};
|
|
847
696
|
}
|
|
848
697
|
async function pollSpeechJob(jobId, download, downloadEndpoint = false) {
|
|
@@ -906,7 +755,7 @@ async function pollSpeechJob(jobId, download, downloadEndpoint = false) {
|
|
|
906
755
|
};
|
|
907
756
|
}
|
|
908
757
|
server.registerTool('exactpdf_estimate_speech_cost', {
|
|
909
|
-
description: 'Estimate generated minutes and credits
|
|
758
|
+
description: 'Estimate generated minutes and credits for planning only while background PDF speech/audiobook/translation jobs are hardened. Local-only rough estimate; free and safe to call repeatedly.',
|
|
910
759
|
inputSchema: z.object({
|
|
911
760
|
path: z.string().optional().describe('Absolute path to a PDF file. Used for a rough size-based duration estimate.'),
|
|
912
761
|
characters: z.number().int().positive().optional().describe('Known narration character count, if already extracted.'),
|
|
@@ -941,7 +790,7 @@ server.registerTool('exactpdf_estimate_speech_cost', {
|
|
|
941
790
|
current_credit_cost: credits,
|
|
942
791
|
target_language_applied: selectedMode === 'translate' ? (target_language ?? null) : null,
|
|
943
792
|
ignored_target_language: selectedMode !== 'translate' && target_language ? target_language : null,
|
|
944
|
-
note: '
|
|
793
|
+
note: 'Planning-only while background audio jobs are disabled for production hardening. Intended pricing after re-enable is 1 credit/minute for standard speech/audiobook/presentation and 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.',
|
|
945
794
|
},
|
|
946
795
|
}, null, 2),
|
|
947
796
|
},
|
|
@@ -1000,22 +849,22 @@ server.registerTool('exactpdf_voice_preview', {
|
|
|
1000
849
|
};
|
|
1001
850
|
});
|
|
1002
851
|
server.registerTool('exactpdf_pdf_to_speech', {
|
|
1003
|
-
description: '
|
|
852
|
+
description: 'Planning-only placeholder while PDF-to-speech workers are hardened. Do not use for paid workloads; use exactpdf_voice_preview and exactpdf_estimate_speech_cost instead.',
|
|
1004
853
|
inputSchema: speechJobSchema,
|
|
1005
854
|
}, async (args) => submitSpeechJob('/api/v1/pdf-to-speech', args, 'pdf-to-speech'));
|
|
1006
855
|
server.registerTool('exactpdf_generate_audiobook', {
|
|
1007
|
-
description: '
|
|
856
|
+
description: 'Planning-only placeholder while audiobook workers are hardened. Do not use for paid workloads; use exactpdf_voice_preview and exactpdf_estimate_speech_cost instead.',
|
|
1008
857
|
inputSchema: speechJobSchema,
|
|
1009
858
|
}, async (args) => submitSpeechJob('/api/v1/generate-audiobook', args, 'generate-audiobook'));
|
|
1010
859
|
server.registerTool('exactpdf_get_speech_job', {
|
|
1011
|
-
description: 'Poll a speech/audiobook/translation/presentation job.
|
|
860
|
+
description: 'Poll a legacy speech/audiobook/translation/presentation job. Background audio submission is currently disabled for paid workloads.',
|
|
1012
861
|
inputSchema: z.object({
|
|
1013
862
|
job_id: z.string().describe('ExactPDF speech job id.'),
|
|
1014
863
|
download: z.boolean().optional().describe('When true, save result_url to EXACTPDF_API_OUTPUT_DIR if job succeeded.'),
|
|
1015
864
|
}),
|
|
1016
865
|
}, async ({ job_id, download }) => pollSpeechJob(job_id, Boolean(download)));
|
|
1017
866
|
server.registerTool('exactpdf_download_audio', {
|
|
1018
|
-
description: 'Download a succeeded speech/audiobook job and save the audio/ZIP locally.
|
|
867
|
+
description: 'Download a legacy succeeded speech/audiobook job and save the audio/ZIP locally. New background audio submission is currently disabled for paid workloads.',
|
|
1019
868
|
inputSchema: z.object({
|
|
1020
869
|
job_id: z.string().describe('ExactPDF speech job id.'),
|
|
1021
870
|
}),
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exactpdf/mcp",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "MCP server for ExactPDF — PDF
|
|
3
|
+
"version": "0.2.13",
|
|
4
|
+
"description": "MCP server for ExactPDF — agent PDF workflows, structured Markdown/RAG, merge/split/compress, images-to-PDF, voice previews, and API credits.",
|
|
5
5
|
"mcpName": "com.exactpdf/mcp",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/run.js",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"publishConfig": {
|
|
20
20
|
"access": "public"
|
|
21
21
|
},
|
|
22
|
-
"homepage": "https://exactpdf.com/
|
|
22
|
+
"homepage": "https://exactpdf.com/developers",
|
|
23
23
|
"bugs": {
|
|
24
24
|
"url": "https://exactpdf.com/docs/api",
|
|
25
25
|
"email": "support@exactpdf.com"
|
|
@@ -39,10 +39,19 @@
|
|
|
39
39
|
"pdf",
|
|
40
40
|
"exactpdf",
|
|
41
41
|
"cursor",
|
|
42
|
+
"cursor-mcp",
|
|
42
43
|
"claude",
|
|
43
44
|
"claude-desktop",
|
|
45
|
+
"codex",
|
|
44
46
|
"headless-pdf",
|
|
45
|
-
"pdf-api"
|
|
47
|
+
"pdf-api",
|
|
48
|
+
"pdf-mcp",
|
|
49
|
+
"pdf-compression",
|
|
50
|
+
"pdf-to-markdown",
|
|
51
|
+
"rag",
|
|
52
|
+
"images-to-pdf",
|
|
53
|
+
"merge-pdf",
|
|
54
|
+
"split-pdf"
|
|
46
55
|
],
|
|
47
56
|
"author": "ExactPDF",
|
|
48
57
|
"license": "MIT",
|
package/server.json
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "com.exactpdf/mcp",
|
|
4
4
|
"title": "ExactPDF",
|
|
5
|
-
"description": "Agent-facing PDF API: merge, split, rotate, compress, images, metadata, text, Markdown, voice previews,
|
|
6
|
-
"version": "0.2.
|
|
5
|
+
"description": "Agent-facing PDF API: merge, split, rotate, compress, images-to-PDF, metadata, text extraction, structured Markdown/RAG, voice previews, and API credit checks. Background audio jobs are hardening-only until worker smoke passes.",
|
|
6
|
+
"version": "0.2.13",
|
|
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.13",
|
|
13
13
|
"transport": {
|
|
14
14
|
"type": "stdio"
|
|
15
15
|
}
|