@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 CHANGED
@@ -2,7 +2,11 @@
2
2
 
3
3
  <!-- mcp-name: com.exactpdf/mcp -->
4
4
 
5
- Model Context Protocol (stdio) server for **ExactPDF** — use PDF tools from **Cursor**, **Claude Desktop**, **Codex**, and any MCP client.
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 --provenance
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.7' }, {
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); voice-preview (free), async pdf-to-speech/pdf-to-audiobook/generate-audiobook (10 credits), translate-and-speak (20 credits), presentation narration (10 credits), plus speech job polling/download. Set EXACTPDF_API_KEY.',
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: 'Return ExactPDF API credit balance and key metadata. Does not consume a credit.',
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 two or more local PDF files via ExactPDF API (1 credit). Provide absolute file paths in order.',
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 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]].',
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 via ExactPDF API (1 credit). angle: 90, 180, 270, -90, -180, or -270.',
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 / repack a PDF via ExactPDF API (1 credit). Uses object-stream save; may not shrink image-heavy PDFs.',
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: 'Combine local PNG/JPEG images into one PDF via ExactPDF API (1 credit). Order matches paths array.',
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 PDF metadata (title, author, …). POST /api/v1/pdf-info does not consume a credit.',
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 via ExactPDF API (1 credit). Returns JSON with text and page_count.',
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 PDFstructured Markdown JSON via ExactPDF API (1 credit). mode developer|academic|rag mirrors the web tool presets.',
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: 'Create an async PDFaudiobook job via ExactPDF API (10 credits). Returns a job_id; poll with exactpdf_job_status.',
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 an ExactPDF async API job. Set download=true to save the result when the job has succeeded.',
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: '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.',
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: 'Create an audio-first PDF presentation narration job (10 credits). Each page becomes a narrated slide track in a ZIP manifest.',
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 PDF speech duration and current ExactPDF API credit cost before submitting a paid job. No API credits consumed.',
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('Set for translation + speech estimates.'),
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 = target_language ? 'translate' : (mode ?? 'audiobook');
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
- note: 'ExactPDF charges async speech jobs by estimated generated minutes: 1 credit/minute for standard speech, 3 credits/minute for translate-and-speak. Path-based estimates are rough until the API extracts PDF text.',
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 server-side voice preview before submitting a paid PDF speech job. Saves MP3/WAV locally. Does not consume credits.',
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: 'Create an async PDF→speech job via /api/v1/pdf-to-speech (10 credits). Alias of the production audiobook pipeline.',
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: 'Create an async audiobook job via /api/v1/generate-audiobook (10 credits). Alias of the production audiobook pipeline.',
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 /api/v1/speech-jobs/:id for async PDF speech, audiobook, translation, or presentation narration jobs.',
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 through /api/v1/speech-jobs/:id/download and save it locally.',
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.7",
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.7",
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.7",
12
+ "version": "0.2.9",
13
13
  "transport": {
14
14
  "type": "stdio"
15
15
  }