@exactpdf/mcp 0.2.3 → 0.2.5
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 +33 -0
- package/dist/run.js +277 -2
- package/package.json +3 -3
- package/server.json +3 -3
package/README.md
CHANGED
|
@@ -55,6 +55,39 @@ For a local checkout of this monorepo:
|
|
|
55
55
|
| `exactpdf_images_to_pdf` | POST /api/v1/images-to-pdf | 1 |
|
|
56
56
|
| `exactpdf_extract_text` | POST /api/v1/extract-text | 1 |
|
|
57
57
|
| `exactpdf_pdf_structured_markdown` | POST /api/v1/pdf-structured-markdown | 1 |
|
|
58
|
+
| `exactpdf_pdf_to_audiobook` | POST /api/v1/pdf-to-audiobook | 10 |
|
|
59
|
+
| `exactpdf_translate_and_speak` | POST /api/v1/translate-and-speak | 20 |
|
|
60
|
+
| `exactpdf_presentation_narration` | POST /api/v1/presentation-narration | 10 |
|
|
61
|
+
| `exactpdf_job_status` | GET /api/v1/jobs/:id | 0 |
|
|
62
|
+
|
|
63
|
+
Async audiobook flow:
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
1. exactpdf_pdf_to_audiobook(path="/abs/book.pdf", voice_style="audiobook")
|
|
67
|
+
2. exactpdf_job_status(job_id="...", download=false)
|
|
68
|
+
3. exactpdf_job_status(job_id="...", download=true) after status is succeeded
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`exactpdf_pdf_to_audiobook` accepts optional `callback_url` and `webhook_secret`. ExactPDF signs webhook bodies with `x-exactpdf-signature: sha256=<hmac>` over `timestamp.body`.
|
|
72
|
+
|
|
73
|
+
Multilingual speech flow:
|
|
74
|
+
|
|
75
|
+
```text
|
|
76
|
+
1. exactpdf_translate_and_speak(path="/abs/training.pdf", target_language="hi")
|
|
77
|
+
2. exactpdf_job_status(job_id="...", download=false)
|
|
78
|
+
3. exactpdf_job_status(job_id="...", download=true) after status is succeeded
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`segmentation_mode="pages"` is useful for presentation narration because each PDF page becomes a trackable audio section.
|
|
82
|
+
|
|
83
|
+
Presentation narration flow:
|
|
84
|
+
|
|
85
|
+
```text
|
|
86
|
+
1. exactpdf_presentation_narration(path="/abs/deck.pdf")
|
|
87
|
+
2. exactpdf_job_status(job_id="...", download=true) after status is succeeded
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The default ZIP includes one MP3 per PDF page plus `manifest.json` with page numbers and duration seconds.
|
|
58
91
|
|
|
59
92
|
## Publish checklist
|
|
60
93
|
|
package/dist/run.js
CHANGED
|
@@ -24,9 +24,29 @@ function requireKey() {
|
|
|
24
24
|
}
|
|
25
25
|
return k;
|
|
26
26
|
}
|
|
27
|
-
const server = new McpServer({ name: 'exactpdf', version: '0.2.
|
|
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.',
|
|
27
|
+
const server = new McpServer({ name: 'exactpdf', version: '0.2.5' }, {
|
|
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); async pdf-to-audiobook (10 credits), translate-and-speak (20 credits), presentation narration (10 credits), plus job polling. Set EXACTPDF_API_KEY.',
|
|
29
29
|
});
|
|
30
|
+
async function saveBinaryFromResponse(res, prefix, fallbackExt) {
|
|
31
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
32
|
+
const dir = outputDir();
|
|
33
|
+
await mkdir(dir, { recursive: true });
|
|
34
|
+
const outPath = join(dir, `${prefix}-${Date.now()}.${fallbackExt}`);
|
|
35
|
+
await writeFile(outPath, buf);
|
|
36
|
+
return outPath;
|
|
37
|
+
}
|
|
38
|
+
function extensionFromContentType(contentType, fallback) {
|
|
39
|
+
const ct = contentType?.toLowerCase() ?? '';
|
|
40
|
+
if (ct.includes('audio/mpeg') || ct.includes('audio/mp3'))
|
|
41
|
+
return 'mp3';
|
|
42
|
+
if (ct.includes('audio/wav') || ct.includes('audio/x-wav'))
|
|
43
|
+
return 'wav';
|
|
44
|
+
if (ct.includes('zip'))
|
|
45
|
+
return 'zip';
|
|
46
|
+
if (ct.includes('pdf'))
|
|
47
|
+
return 'pdf';
|
|
48
|
+
return fallback;
|
|
49
|
+
}
|
|
30
50
|
server.registerTool('exactpdf_account', {
|
|
31
51
|
description: 'Return ExactPDF API credit balance and key metadata. Does not consume a credit.',
|
|
32
52
|
inputSchema: z.object({}),
|
|
@@ -377,6 +397,261 @@ server.registerTool('exactpdf_pdf_structured_markdown', {
|
|
|
377
397
|
content: [{ type: 'text', text: `Credits remaining: ${credits}\n${raw.slice(0, 120_000)}` }],
|
|
378
398
|
};
|
|
379
399
|
});
|
|
400
|
+
server.registerTool('exactpdf_pdf_to_audiobook', {
|
|
401
|
+
description: 'Create an async PDF→audiobook job via ExactPDF API (10 credits). Returns a job_id; poll with exactpdf_job_status.',
|
|
402
|
+
inputSchema: z.object({
|
|
403
|
+
path: z.string().describe('Absolute path to a PDF file'),
|
|
404
|
+
output_format: z.enum(['mp3', 'wav', 'zip']).optional().describe('Audio export format. Default: mp3.'),
|
|
405
|
+
voice_style: z
|
|
406
|
+
.enum(['professional', 'audiobook', 'educational', 'presenter', 'conversational'])
|
|
407
|
+
.optional()
|
|
408
|
+
.describe('Narration style. Default: audiobook.'),
|
|
409
|
+
voice_id: z.string().optional().describe('Optional provider voice id, if your account supports it.'),
|
|
410
|
+
language: z.string().optional().describe('Language hint such as en, hi, es, fr.'),
|
|
411
|
+
speed: z.number().min(0.5).max(2).optional().describe('Speech speed multiplier, 0.5-2.0.'),
|
|
412
|
+
page_range: z.string().optional().describe('Optional page range, e.g. "1-10" or "2,4-7".'),
|
|
413
|
+
normalize_pauses: z.boolean().optional().describe('Normalize pauses between extracted sections.'),
|
|
414
|
+
preserve_chapters: z.boolean().optional().describe('Keep detected headings as audiobook chapters.'),
|
|
415
|
+
pronunciation_fixes: z.string().optional().describe('JSON object of pronunciation replacements.'),
|
|
416
|
+
callback_url: z.string().url().optional().describe('Optional https webhook URL for job completion.'),
|
|
417
|
+
webhook_secret: z.string().min(12).max(256).optional().describe('Optional webhook HMAC signing secret.'),
|
|
418
|
+
}),
|
|
419
|
+
}, async ({ path, output_format, voice_style, voice_id, language, speed, page_range, normalize_pauses, preserve_chapters, pronunciation_fixes, callback_url, webhook_secret, }) => {
|
|
420
|
+
const key = requireKey();
|
|
421
|
+
const form = new FormData();
|
|
422
|
+
form.append('file', new Blob([await readFile(path)], { type: 'application/pdf' }), basename(path));
|
|
423
|
+
if (output_format)
|
|
424
|
+
form.append('output_format', output_format);
|
|
425
|
+
if (voice_style)
|
|
426
|
+
form.append('voice_style', voice_style);
|
|
427
|
+
if (voice_id)
|
|
428
|
+
form.append('voice_id', voice_id);
|
|
429
|
+
if (language)
|
|
430
|
+
form.append('language', language);
|
|
431
|
+
if (typeof speed === 'number')
|
|
432
|
+
form.append('speed', String(speed));
|
|
433
|
+
if (page_range)
|
|
434
|
+
form.append('page_range', page_range);
|
|
435
|
+
if (typeof normalize_pauses === 'boolean')
|
|
436
|
+
form.append('normalize_pauses', String(normalize_pauses));
|
|
437
|
+
if (typeof preserve_chapters === 'boolean')
|
|
438
|
+
form.append('preserve_chapters', String(preserve_chapters));
|
|
439
|
+
if (pronunciation_fixes)
|
|
440
|
+
form.append('pronunciation_fixes', pronunciation_fixes);
|
|
441
|
+
if (callback_url)
|
|
442
|
+
form.append('callback_url', callback_url);
|
|
443
|
+
if (webhook_secret)
|
|
444
|
+
form.append('webhook_secret', webhook_secret);
|
|
445
|
+
const res = await fetch(`${BASE}/api/v1/pdf-to-audiobook`, {
|
|
446
|
+
method: 'POST',
|
|
447
|
+
headers: {
|
|
448
|
+
Authorization: `Bearer ${key}`,
|
|
449
|
+
Accept: 'application/json',
|
|
450
|
+
},
|
|
451
|
+
body: form,
|
|
452
|
+
});
|
|
453
|
+
const raw = await res.text();
|
|
454
|
+
if (!res.ok) {
|
|
455
|
+
return {
|
|
456
|
+
content: [
|
|
457
|
+
{
|
|
458
|
+
type: 'text',
|
|
459
|
+
text: `pdf-to-audiobook failed HTTP ${res.status}\n${raw.slice(0, 8000)}`,
|
|
460
|
+
},
|
|
461
|
+
],
|
|
462
|
+
isError: true,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
return {
|
|
466
|
+
content: [
|
|
467
|
+
{
|
|
468
|
+
type: 'text',
|
|
469
|
+
text: `Audiobook job submitted HTTP ${res.status}\n${raw.slice(0, 120_000)}`,
|
|
470
|
+
},
|
|
471
|
+
],
|
|
472
|
+
};
|
|
473
|
+
});
|
|
474
|
+
server.registerTool('exactpdf_job_status', {
|
|
475
|
+
description: 'Poll an ExactPDF async API job. Set download=true to save the result when the job has succeeded.',
|
|
476
|
+
inputSchema: z.object({
|
|
477
|
+
job_id: z.string().describe('ExactPDF async job id returned by exactpdf_pdf_to_audiobook.'),
|
|
478
|
+
download: z.boolean().optional().describe('When true, save result_url to EXACTPDF_API_OUTPUT_DIR.'),
|
|
479
|
+
}),
|
|
480
|
+
}, async ({ job_id, download }) => {
|
|
481
|
+
const key = requireKey();
|
|
482
|
+
const res = await fetch(`${BASE}/api/v1/jobs/${encodeURIComponent(job_id)}`, {
|
|
483
|
+
headers: {
|
|
484
|
+
Authorization: `Bearer ${key}`,
|
|
485
|
+
Accept: 'application/json',
|
|
486
|
+
},
|
|
487
|
+
});
|
|
488
|
+
const raw = await res.text();
|
|
489
|
+
if (!res.ok) {
|
|
490
|
+
return {
|
|
491
|
+
content: [
|
|
492
|
+
{
|
|
493
|
+
type: 'text',
|
|
494
|
+
text: `job-status failed HTTP ${res.status}\n${raw.slice(0, 8000)}`,
|
|
495
|
+
},
|
|
496
|
+
],
|
|
497
|
+
isError: true,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
let saved = '';
|
|
501
|
+
if (download) {
|
|
502
|
+
const parsed = JSON.parse(raw);
|
|
503
|
+
const resultUrl = parsed.job?.result_url;
|
|
504
|
+
if (parsed.job?.status === 'succeeded' && resultUrl) {
|
|
505
|
+
const fileRes = await fetch(resultUrl);
|
|
506
|
+
if (!fileRes.ok) {
|
|
507
|
+
return {
|
|
508
|
+
content: [
|
|
509
|
+
{
|
|
510
|
+
type: 'text',
|
|
511
|
+
text: `Job succeeded, but result download failed HTTP ${fileRes.status}\n${raw.slice(0, 8000)}`,
|
|
512
|
+
},
|
|
513
|
+
],
|
|
514
|
+
isError: true,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
const fallback = parsed.job?.kind === 'audiobook' ? 'mp3' : 'pdf';
|
|
518
|
+
const ext = extensionFromContentType(fileRes.headers.get('content-type'), fallback);
|
|
519
|
+
saved = `\nSaved result: ${await saveBinaryFromResponse(fileRes, `exactpdf-job-${job_id}`, ext)}`;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return {
|
|
523
|
+
content: [{ type: 'text', text: `HTTP ${res.status}\n${raw.slice(0, 120_000)}${saved}` }],
|
|
524
|
+
};
|
|
525
|
+
});
|
|
526
|
+
server.registerTool('exactpdf_translate_and_speak', {
|
|
527
|
+
description: 'Create an async PDF translation + native speech job via ExactPDF API (20 credits). Start with target_language hi, es, or fr; poll with exactpdf_job_status.',
|
|
528
|
+
inputSchema: z.object({
|
|
529
|
+
path: z.string().describe('Absolute path to a PDF file'),
|
|
530
|
+
target_language: z.string().describe('Target language code, e.g. hi, es, fr, mr, de, ja, ar.'),
|
|
531
|
+
source_language: z.string().optional().describe('Source language code. Default: auto.'),
|
|
532
|
+
output_format: z.enum(['mp3', 'wav', 'zip']).optional().describe('Audio export format. Default: mp3.'),
|
|
533
|
+
voice_style: z
|
|
534
|
+
.enum(['professional', 'audiobook', 'educational', 'presenter', 'conversational'])
|
|
535
|
+
.optional()
|
|
536
|
+
.describe('Narration style. Default: audiobook.'),
|
|
537
|
+
speed: z.number().min(0.5).max(2).optional().describe('Speech speed multiplier, 0.5-2.0.'),
|
|
538
|
+
page_range: z.string().optional().describe('Optional page range, e.g. "1-10" or "2,4-7".'),
|
|
539
|
+
segmentation_mode: z.enum(['chapters', 'pages']).optional().describe('Use pages for slide/page narration.'),
|
|
540
|
+
translation_glossary: z.string().optional().describe('JSON array like [{"source":"ExactPDF","target":"ExactPDF"}].'),
|
|
541
|
+
callback_url: z.string().url().optional().describe('Optional https webhook URL for job completion.'),
|
|
542
|
+
webhook_secret: z.string().min(12).max(256).optional().describe('Optional webhook HMAC signing secret.'),
|
|
543
|
+
}),
|
|
544
|
+
}, async ({ path, target_language, source_language, output_format, voice_style, speed, page_range, segmentation_mode, translation_glossary, callback_url, webhook_secret, }) => {
|
|
545
|
+
const key = requireKey();
|
|
546
|
+
const form = new FormData();
|
|
547
|
+
form.append('file', new Blob([await readFile(path)], { type: 'application/pdf' }), basename(path));
|
|
548
|
+
form.append('target_language', target_language);
|
|
549
|
+
if (source_language)
|
|
550
|
+
form.append('source_language', source_language);
|
|
551
|
+
if (output_format)
|
|
552
|
+
form.append('output_format', output_format);
|
|
553
|
+
if (voice_style)
|
|
554
|
+
form.append('voice_style', voice_style);
|
|
555
|
+
if (typeof speed === 'number')
|
|
556
|
+
form.append('speed', String(speed));
|
|
557
|
+
if (page_range)
|
|
558
|
+
form.append('page_range', page_range);
|
|
559
|
+
if (segmentation_mode)
|
|
560
|
+
form.append('segmentation_mode', segmentation_mode);
|
|
561
|
+
if (translation_glossary)
|
|
562
|
+
form.append('translation_glossary', translation_glossary);
|
|
563
|
+
if (callback_url)
|
|
564
|
+
form.append('callback_url', callback_url);
|
|
565
|
+
if (webhook_secret)
|
|
566
|
+
form.append('webhook_secret', webhook_secret);
|
|
567
|
+
const res = await fetch(`${BASE}/api/v1/translate-and-speak`, {
|
|
568
|
+
method: 'POST',
|
|
569
|
+
headers: {
|
|
570
|
+
Authorization: `Bearer ${key}`,
|
|
571
|
+
Accept: 'application/json',
|
|
572
|
+
},
|
|
573
|
+
body: form,
|
|
574
|
+
});
|
|
575
|
+
const raw = await res.text();
|
|
576
|
+
if (!res.ok) {
|
|
577
|
+
return {
|
|
578
|
+
content: [
|
|
579
|
+
{
|
|
580
|
+
type: 'text',
|
|
581
|
+
text: `translate-and-speak failed HTTP ${res.status}\n${raw.slice(0, 8000)}`,
|
|
582
|
+
},
|
|
583
|
+
],
|
|
584
|
+
isError: true,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
return {
|
|
588
|
+
content: [
|
|
589
|
+
{
|
|
590
|
+
type: 'text',
|
|
591
|
+
text: `Multilingual speech job submitted HTTP ${res.status}\n${raw.slice(0, 120_000)}`,
|
|
592
|
+
},
|
|
593
|
+
],
|
|
594
|
+
};
|
|
595
|
+
});
|
|
596
|
+
server.registerTool('exactpdf_presentation_narration', {
|
|
597
|
+
description: 'Create an audio-first PDF presentation narration job (10 credits). Each page becomes a narrated slide track in a ZIP manifest.',
|
|
598
|
+
inputSchema: z.object({
|
|
599
|
+
path: z.string().describe('Absolute path to a PDF presentation file'),
|
|
600
|
+
output_format: z.enum(['mp3', 'wav', 'zip']).optional().describe('Default: zip for per-slide tracks and manifest.'),
|
|
601
|
+
voice_style: z
|
|
602
|
+
.enum(['professional', 'audiobook', 'educational', 'presenter', 'conversational'])
|
|
603
|
+
.optional()
|
|
604
|
+
.describe('Narration style. Default: presenter.'),
|
|
605
|
+
speed: z.number().min(0.5).max(2).optional().describe('Speech speed multiplier, 0.5-2.0.'),
|
|
606
|
+
page_range: z.string().optional().describe('Optional slide/page range, e.g. "1-10" or "2,4-7".'),
|
|
607
|
+
callback_url: z.string().url().optional().describe('Optional https webhook URL for job completion.'),
|
|
608
|
+
webhook_secret: z.string().min(12).max(256).optional().describe('Optional webhook HMAC signing secret.'),
|
|
609
|
+
}),
|
|
610
|
+
}, async ({ path, output_format, voice_style, speed, page_range, callback_url, webhook_secret }) => {
|
|
611
|
+
const key = requireKey();
|
|
612
|
+
const form = new FormData();
|
|
613
|
+
form.append('file', new Blob([await readFile(path)], { type: 'application/pdf' }), basename(path));
|
|
614
|
+
if (output_format)
|
|
615
|
+
form.append('output_format', output_format);
|
|
616
|
+
if (voice_style)
|
|
617
|
+
form.append('voice_style', voice_style);
|
|
618
|
+
if (typeof speed === 'number')
|
|
619
|
+
form.append('speed', String(speed));
|
|
620
|
+
if (page_range)
|
|
621
|
+
form.append('page_range', page_range);
|
|
622
|
+
if (callback_url)
|
|
623
|
+
form.append('callback_url', callback_url);
|
|
624
|
+
if (webhook_secret)
|
|
625
|
+
form.append('webhook_secret', webhook_secret);
|
|
626
|
+
const res = await fetch(`${BASE}/api/v1/presentation-narration`, {
|
|
627
|
+
method: 'POST',
|
|
628
|
+
headers: {
|
|
629
|
+
Authorization: `Bearer ${key}`,
|
|
630
|
+
Accept: 'application/json',
|
|
631
|
+
},
|
|
632
|
+
body: form,
|
|
633
|
+
});
|
|
634
|
+
const raw = await res.text();
|
|
635
|
+
if (!res.ok) {
|
|
636
|
+
return {
|
|
637
|
+
content: [
|
|
638
|
+
{
|
|
639
|
+
type: 'text',
|
|
640
|
+
text: `presentation-narration failed HTTP ${res.status}\n${raw.slice(0, 8000)}`,
|
|
641
|
+
},
|
|
642
|
+
],
|
|
643
|
+
isError: true,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
return {
|
|
647
|
+
content: [
|
|
648
|
+
{
|
|
649
|
+
type: 'text',
|
|
650
|
+
text: `Presentation narration job submitted HTTP ${res.status}\n${raw.slice(0, 120_000)}`,
|
|
651
|
+
},
|
|
652
|
+
],
|
|
653
|
+
};
|
|
654
|
+
});
|
|
380
655
|
async function main() {
|
|
381
656
|
const transport = new StdioServerTransport();
|
|
382
657
|
await server.connect(transport);
|
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 tools
|
|
3
|
+
"version": "0.2.5",
|
|
4
|
+
"description": "MCP server for ExactPDF — PDF tools, async PDF to audiobook jobs, polling, and API credits.",
|
|
5
5
|
"mcpName": "com.exactpdf/mcp",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/run.js",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"repository": {
|
|
29
29
|
"type": "git",
|
|
30
|
-
"url": "https://github.com/exactpdf/exactpdf.git",
|
|
30
|
+
"url": "git+https://github.com/exactpdf/exactpdf.git",
|
|
31
31
|
"directory": "packages/exactpdf-mcp"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
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": "
|
|
6
|
-
"version": "0.2.
|
|
5
|
+
"description": "Agent-facing PDF API: merge, split, rotate, compress, images, metadata, text, Markdown, async audiobook, multilingual speech, and presentation narration jobs.",
|
|
6
|
+
"version": "0.2.5",
|
|
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.5",
|
|
13
13
|
"transport": {
|
|
14
14
|
"type": "stdio"
|
|
15
15
|
}
|