gurupdf-mcp 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 GuruPDF
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # GuruPDF MCP — convert PDFs & 100+ file formats from your AI agent
2
+
3
+ [![npm version](https://img.shields.io/npm/v/gurupdf-mcp.svg)](https://www.npmjs.com/package/gurupdf-mcp)
4
+ [![npm downloads](https://img.shields.io/npm/dm/gurupdf-mcp.svg)](https://www.npmjs.com/package/gurupdf-mcp)
5
+ [![license: MIT](https://img.shields.io/npm/l/gurupdf-mcp.svg)](./LICENSE)
6
+
7
+ A free **[Model Context Protocol](https://modelcontextprotocol.io) (MCP) server** that lets Claude, Cursor, VS Code, Windsurf and other AI agents **convert, compress, merge, split and edit PDFs — and convert between 100+ file formats** (Word, Excel, PowerPoint, JPG, PNG, HEIC, ebooks, and more), right on your own machine. Powered by [GuruPDF](https://gurupdf.com).
8
+
9
+ **Languages:** English · [Español](i18n/README.es.md) · [Français](i18n/README.fr.md) · [Deutsch](i18n/README.de.md) · [中文](i18n/README.zh.md) · [Русский](i18n/README.ru.md) · [Українська](i18n/README.uk.md) · [Polski](i18n/README.pl.md) · [Nederlands](i18n/README.nl.md) · [Türkçe](i18n/README.tr.md) · [Čeština](i18n/README.cs.md) · [Ελληνικά](i18n/README.el.md) · [العربية](i18n/README.ar.md)
10
+
11
+ > Ask your assistant: *"compress this PDF"*, *"turn invoice.docx into a PDF"*, or *"merge these three files"* — and it converts the files right on your machine.
12
+
13
+ - 🗂️ **126 tools** — PDF ⇄ Word/Excel/PowerPoint, images, ebooks, OCR, compress, merge, split, rotate, protect, watermark, and more.
14
+ - 💻 **Works on your local files** — reads and writes files on disk, no manual upload/download.
15
+ - 🆓 **Free to start** — every account gets daily credits. No credit card required.
16
+
17
+ ## Install
18
+
19
+ You need **Node.js 18+** and a free **GuruPDF API key**:
20
+
21
+ 1. Sign up at **[gurupdf.com](https://gurupdf.com)**.
22
+ 2. Open **[Profile → API tokens](https://gurupdf.com/profile)** and create a token.
23
+ 3. Add the server to your agent with that key (configs below). No install step — `npx` fetches it on first run.
24
+
25
+ ### Claude Desktop
26
+
27
+ `claude_desktop_config.json`:
28
+
29
+ ```json
30
+ {
31
+ "mcpServers": {
32
+ "gurupdf": {
33
+ "command": "npx",
34
+ "args": ["-y", "gurupdf-mcp"],
35
+ "env": { "GURUPDF_API_KEY": "your_token_here" }
36
+ }
37
+ }
38
+ }
39
+ ```
40
+
41
+ ### Cursor
42
+
43
+ `~/.cursor/mcp.json` (or `.cursor/mcp.json` in a project):
44
+
45
+ ```json
46
+ {
47
+ "mcpServers": {
48
+ "gurupdf": {
49
+ "command": "npx",
50
+ "args": ["-y", "gurupdf-mcp"],
51
+ "env": { "GURUPDF_API_KEY": "your_token_here" }
52
+ }
53
+ }
54
+ }
55
+ ```
56
+
57
+ ### VS Code
58
+
59
+ `.vscode/mcp.json`:
60
+
61
+ ```json
62
+ {
63
+ "servers": {
64
+ "gurupdf": {
65
+ "command": "npx",
66
+ "args": ["-y", "gurupdf-mcp"],
67
+ "env": { "GURUPDF_API_KEY": "your_token_here" }
68
+ }
69
+ }
70
+ }
71
+ ```
72
+
73
+ ### Windsurf
74
+
75
+ `~/.codeium/windsurf/mcp_config.json`:
76
+
77
+ ```json
78
+ {
79
+ "mcpServers": {
80
+ "gurupdf": {
81
+ "command": "npx",
82
+ "args": ["-y", "gurupdf-mcp"],
83
+ "env": { "GURUPDF_API_KEY": "your_token_here" }
84
+ }
85
+ }
86
+ }
87
+ ```
88
+
89
+ ## Tools
90
+
91
+ | Tool | What it does |
92
+ |------|--------------|
93
+ | `convert_file` | Convert/process a local file (or URL). Give it an input and a target format (`pdf`, `png`, `docx`…) or a tool slug (`compress-pdf`, `merge-pdf`…). Saves the result to disk. |
94
+ | `get_status` | Check a conversion job by id and download the result when ready (for long jobs like video). |
95
+ | `list_conversions` | List supported conversions/tools, optionally filtered by an input format. |
96
+ | `check_credits` | Show remaining credits and how to get more. |
97
+
98
+ ### Examples
99
+
100
+ > **"Compress `~/Documents/report.pdf`."**
101
+ > → `convert_file(input: "~/Documents/report.pdf", to: "compress-pdf")`
102
+
103
+ > **"Convert `invoice.docx` to PDF."**
104
+ > → `convert_file(input: "invoice.docx", to: "pdf")`
105
+
106
+ > **"Merge `a.pdf` and `b.pdf` into one."**
107
+ > → `convert_file(input: ["a.pdf", "b.pdf"], to: "merge-pdf")`
108
+
109
+ > **"Password-protect this PDF with `hunter2`."**
110
+ > → `convert_file(input: "secret.pdf", to: "protect-pdf", options: { password: "hunter2" })`
111
+
112
+ > **"Save this web page as a PDF: https://example.com"**
113
+ > → `convert_file(input: "https://example.com", to: "url-to-pdf")`
114
+
115
+ ## Free tier & credits
116
+
117
+ Each tool costs a few credits. Free accounts get **daily credits** (refreshed every day) and **2 conversions/minute, 10/day**. When you run out, the assistant will tell you — you can wait for the daily refresh or [top up / upgrade](https://gurupdf.com/pricing). Conversions run on GuruPDF's servers; files are deleted automatically within an hour.
118
+
119
+ ## Configuration
120
+
121
+ | Env var | Default | Notes |
122
+ |---------|---------|-------|
123
+ | `GURUPDF_API_KEY` | — | **Required.** Your API token from [Profile → API tokens](https://gurupdf.com/profile). |
124
+ | `GURUPDF_API_URL` | `https://gurupdf.com/api/v1` | Override only for self-hosted / staging. |
125
+
126
+ ## Links
127
+
128
+ - Website: [gurupdf.com](https://gurupdf.com)
129
+ - API docs: [gurupdf.com/api/docs](https://gurupdf.com/api/docs)
130
+ - Pricing: [gurupdf.com/pricing](https://gurupdf.com/pricing)
131
+
132
+ ## License
133
+
134
+ MIT
package/dist/client.js ADDED
@@ -0,0 +1,114 @@
1
+ /** An error carrying the GuruPDF API error code / status so callers can map it to friendly copy. */
2
+ export class GuruPdfError extends Error {
3
+ code;
4
+ status;
5
+ meta;
6
+ retryAfter;
7
+ constructor(message, opts = {}) {
8
+ super(message);
9
+ this.name = "GuruPdfError";
10
+ this.code = opts.code ?? "ERROR";
11
+ this.status = opts.status ?? 0;
12
+ this.meta = opts.meta;
13
+ this.retryAfter = opts.retryAfter;
14
+ }
15
+ }
16
+ export class GuruPdfClient {
17
+ apiKey;
18
+ baseUrl;
19
+ constructor(apiKey, baseUrl = "https://gurupdf.com/api/v1") {
20
+ this.apiKey = apiKey;
21
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
22
+ }
23
+ async request(method, path, opts = {}) {
24
+ const headers = {
25
+ Authorization: `Bearer ${this.apiKey}`,
26
+ Accept: "application/json",
27
+ };
28
+ let body;
29
+ if (opts.form) {
30
+ body = opts.form; // fetch sets the multipart boundary itself
31
+ }
32
+ else if (opts.json !== undefined) {
33
+ headers["Content-Type"] = "application/json";
34
+ body = JSON.stringify(opts.json);
35
+ }
36
+ const res = await fetch(`${this.baseUrl}${path}`, { method, headers, body });
37
+ const retryAfter = this.parseRetryAfter(res);
38
+ const raw = await res.text();
39
+ let parsed = null;
40
+ try {
41
+ parsed = raw ? JSON.parse(raw) : null;
42
+ }
43
+ catch {
44
+ /* non-JSON body */
45
+ }
46
+ if (!res.ok) {
47
+ const err = parsed?.error ?? {};
48
+ throw new GuruPdfError(err.message ?? `Request failed (HTTP ${res.status})`, {
49
+ code: err.code ?? `HTTP_${res.status}`,
50
+ status: res.status,
51
+ meta: parsed?.meta ?? err.meta,
52
+ retryAfter,
53
+ });
54
+ }
55
+ return { data: (parsed?.data ?? parsed), meta: parsed?.meta };
56
+ }
57
+ parseRetryAfter(res) {
58
+ const ra = res.headers.get("Retry-After");
59
+ if (ra && /^\d+$/.test(ra))
60
+ return Number(ra);
61
+ const reset = res.headers.get("X-RateLimit-Reset");
62
+ if (reset && /^\d+$/.test(reset)) {
63
+ const secs = Number(reset) - Math.floor(Date.now() / 1000);
64
+ if (secs > 0 && secs < 3600)
65
+ return secs;
66
+ }
67
+ return undefined;
68
+ }
69
+ listTools() {
70
+ return this.request("GET", "/tools");
71
+ }
72
+ getBalance() {
73
+ return this.request("GET", "/account/balance");
74
+ }
75
+ getConversion(uuid) {
76
+ return this.request("GET", `/conversions/${encodeURIComponent(uuid)}`);
77
+ }
78
+ startConversion(slug, files, fields = {}) {
79
+ const form = new FormData();
80
+ for (const f of files) {
81
+ form.append("files[]", new Blob([f.buffer]), f.filename);
82
+ }
83
+ for (const [key, value] of Object.entries(fields)) {
84
+ if (value !== undefined && value !== null)
85
+ form.append(key, String(value));
86
+ }
87
+ return this.request("POST", `/convert/${encodeURIComponent(slug)}`, { form });
88
+ }
89
+ async download(uuid) {
90
+ const res = await fetch(`${this.baseUrl}/conversions/${encodeURIComponent(uuid)}/download`, {
91
+ headers: { Authorization: `Bearer ${this.apiKey}` },
92
+ });
93
+ if (!res.ok) {
94
+ let parsed = null;
95
+ try {
96
+ parsed = await res.json();
97
+ }
98
+ catch {
99
+ /* ignore */
100
+ }
101
+ const err = parsed?.error ?? {};
102
+ throw new GuruPdfError(err.message ?? `Download failed (HTTP ${res.status})`, {
103
+ code: err.code ?? `HTTP_${res.status}`,
104
+ status: res.status,
105
+ meta: parsed?.meta,
106
+ });
107
+ }
108
+ const buffer = Buffer.from(await res.arrayBuffer());
109
+ const disposition = res.headers.get("Content-Disposition") ?? "";
110
+ const match = disposition.match(/filename\*?=(?:UTF-8'')?"?([^";]+)"?/i);
111
+ const filename = match ? decodeURIComponent(match[1]) : uuid;
112
+ return { buffer, filename };
113
+ }
114
+ }
package/dist/index.js ADDED
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import { readFile, writeFile, stat } from "node:fs/promises";
6
+ import { basename, dirname, join, extname } from "node:path";
7
+ import { GuruPdfClient, GuruPdfError } from "./client.js";
8
+ import { outOfCreditsMessage, rateLimitedMessage, fileTooLargeMessage, noApiKeyMessage } from "./messages.js";
9
+ const API_KEY = process.env.GURUPDF_API_KEY ?? "";
10
+ const API_URL = process.env.GURUPDF_API_URL ?? "https://gurupdf.com/api/v1";
11
+ const client = API_KEY ? new GuruPdfClient(API_KEY, API_URL) : null;
12
+ const text = (t) => ({ content: [{ type: "text", text: t }] });
13
+ const isUrl = (s) => /^https?:\/\//i.test(s);
14
+ let toolCache = null;
15
+ async function catalog() {
16
+ if (!toolCache)
17
+ toolCache = (await client.listTools()).data;
18
+ return toolCache;
19
+ }
20
+ /** Map a GuruPDF error to copy the agent can relay; null if it isn't a known case. */
21
+ function friendly(e) {
22
+ if (e instanceof GuruPdfError) {
23
+ if (e.status === 402 || e.code === "INSUFFICIENT_CREDITS")
24
+ return outOfCreditsMessage(e.meta);
25
+ if (e.status === 429)
26
+ return rateLimitedMessage(e.retryAfter);
27
+ return `GuruPDF couldn't do that: ${e.message}`;
28
+ }
29
+ return null;
30
+ }
31
+ /** Resolve `to` (a target format like "pdf"/"png", or a tool slug) + input extension to a tool. */
32
+ function resolveTool(tools, to, inputExt) {
33
+ const direct = tools.find((t) => t.slug === to);
34
+ if (direct)
35
+ return direct;
36
+ const target = "." + to.replace(/^\./, "").toLowerCase();
37
+ const matches = tools.filter((t) => t.output_format?.toLowerCase() === target &&
38
+ (!inputExt || (t.accepted_formats ?? []).map((f) => f.toLowerCase()).includes(inputExt)));
39
+ matches.sort((a, b) => a.slug.length - b.slug.length); // prefer the simplest "<from>-to-<to>"
40
+ return matches[0] ?? null;
41
+ }
42
+ async function pollUntilDone(uuid, timeoutMs = 5 * 60 * 1000) {
43
+ const start = Date.now();
44
+ let delay = 1500;
45
+ // The status endpoint is not rate-limited, so polling is free.
46
+ while (Date.now() - start < timeoutMs) {
47
+ const { data } = await client.getConversion(uuid);
48
+ if (data.status === "completed" || data.status === "error")
49
+ return data;
50
+ await new Promise((r) => setTimeout(r, delay));
51
+ delay = Math.min(delay * 1.4, 8000);
52
+ }
53
+ return (await client.getConversion(uuid)).data;
54
+ }
55
+ const server = new McpServer({ name: "gurupdf", version: "0.1.0" });
56
+ server.tool("convert_file", "Convert or process a local file (or URL) with GuruPDF. Convert between 100+ formats (PDF ⇄ Word/Excel/PowerPoint, images, ebooks, etc.) or run a PDF tool (compress, merge, split, rotate, protect, watermark, OCR…). Saves the result next to the input and returns the path.", {
57
+ input: z
58
+ .union([z.string(), z.array(z.string())])
59
+ .describe("Absolute path to a local file (or several paths for merge), or an https URL for url-to-pdf."),
60
+ to: z
61
+ .string()
62
+ .describe("Target format (e.g. 'pdf', 'docx', 'png') OR a tool slug (e.g. 'compress-pdf', 'merge-pdf', 'protect-pdf')."),
63
+ output_dir: z.string().optional().describe("Folder to save the result in. Defaults to the input file's folder."),
64
+ options: z
65
+ .record(z.union([z.string(), z.number(), z.boolean()]))
66
+ .optional()
67
+ .describe("Tool-specific options, e.g. { password, page_range, rotation, watermark_text }."),
68
+ wait: z.boolean().optional().describe("Wait for the result (default true). If false, returns a job id to check with get_status."),
69
+ }, async ({ input, to, output_dir, options, wait }) => {
70
+ if (!client)
71
+ return text(noApiKeyMessage());
72
+ try {
73
+ const inputs = Array.isArray(input) ? input : [input];
74
+ const urlMode = inputs.length === 1 && isUrl(inputs[0]);
75
+ const inputExt = urlMode ? "" : extname(inputs[0]).toLowerCase();
76
+ const tools = await catalog();
77
+ const tool = resolveTool(tools, to, inputExt);
78
+ if (!tool) {
79
+ return text(`I couldn't find a GuruPDF tool to turn ${inputExt || "that input"} into "${to}". ` +
80
+ `Run list_conversions${inputExt ? ` with from_format "${inputExt.slice(1)}"` : ""} to see the options.`);
81
+ }
82
+ const fields = { ...(options ?? {}) };
83
+ const files = [];
84
+ if (urlMode) {
85
+ fields.url = inputs[0];
86
+ }
87
+ else {
88
+ const maxMb = tool.max_file_size_mb ?? 100;
89
+ for (const p of inputs) {
90
+ let info;
91
+ try {
92
+ info = await stat(p);
93
+ }
94
+ catch {
95
+ return text(`File not found: ${p}`);
96
+ }
97
+ if (info.size > maxMb * 1024 * 1024) {
98
+ return text(fileTooLargeMessage(basename(p), info.size / 1048576, maxMb));
99
+ }
100
+ files.push({ filename: basename(p), buffer: await readFile(p) });
101
+ }
102
+ }
103
+ let conv;
104
+ try {
105
+ const res = await client.startConversion(tool.slug, files, fields);
106
+ conv = res.data.conversions?.[0];
107
+ }
108
+ catch (e) {
109
+ const f = friendly(e);
110
+ if (f)
111
+ return text(f);
112
+ throw e;
113
+ }
114
+ if (!conv)
115
+ return text("GuruPDF didn't start the conversion — please try again.");
116
+ if (wait === false) {
117
+ return text(`Started ${tool.slug} (job id ${conv.uuid}). Use get_status with this id to fetch the result.`);
118
+ }
119
+ const done = await pollUntilDone(conv.uuid);
120
+ if (done.status === "error")
121
+ return text(`The conversion failed: ${done.error_message ?? "unknown error"}.`);
122
+ if (done.status !== "completed") {
123
+ return text(`Still processing (job id ${conv.uuid}) — this one's taking a while. Check back with get_status.`);
124
+ }
125
+ const { buffer, filename } = await client.download(conv.uuid);
126
+ const dir = output_dir ?? (urlMode ? process.cwd() : dirname(inputs[0]));
127
+ const outPath = join(dir, filename);
128
+ await writeFile(outPath, buffer);
129
+ let remaining = "";
130
+ try {
131
+ remaining = ` You have ${(await client.getBalance()).data.total} credits left.`;
132
+ }
133
+ catch {
134
+ /* non-fatal */
135
+ }
136
+ const used = done.credits_used ?? 0;
137
+ return text(`Done — saved to ${outPath}. Used ${used} credit${used === 1 ? "" : "s"}.${remaining}`);
138
+ }
139
+ catch (e) {
140
+ return text(friendly(e) ?? `Something went wrong: ${e.message}`);
141
+ }
142
+ });
143
+ server.tool("get_status", "Check a GuruPDF conversion job by its id (uuid). If it's finished, downloads the result to disk.", {
144
+ uuid: z.string().describe("The job id returned by convert_file."),
145
+ output_dir: z.string().optional().describe("Folder to save the result in. Defaults to the current directory."),
146
+ }, async ({ uuid, output_dir }) => {
147
+ if (!client)
148
+ return text(noApiKeyMessage());
149
+ try {
150
+ const { data } = await client.getConversion(uuid);
151
+ if (data.status === "error")
152
+ return text(`Job ${uuid} failed: ${data.error_message ?? "unknown error"}.`);
153
+ if (data.status !== "completed")
154
+ return text(`Job ${uuid} is "${data.status}". Try again in a few seconds.`);
155
+ const { buffer, filename } = await client.download(uuid);
156
+ const outPath = join(output_dir ?? process.cwd(), filename);
157
+ await writeFile(outPath, buffer);
158
+ return text(`Job ${uuid} is done — saved to ${outPath}.`);
159
+ }
160
+ catch (e) {
161
+ return text(friendly(e) ?? `Couldn't check job ${uuid}: ${e.message}`);
162
+ }
163
+ });
164
+ server.tool("list_conversions", "List the conversions and tools GuruPDF supports, optionally filtered by an input file format.", {
165
+ from_format: z.string().optional().describe("e.g. 'pdf', 'jpg', 'docx' — show only tools that accept this input."),
166
+ }, async ({ from_format }) => {
167
+ if (!client)
168
+ return text(noApiKeyMessage());
169
+ try {
170
+ const tools = await catalog();
171
+ const ext = from_format ? "." + from_format.replace(/^\./, "").toLowerCase() : null;
172
+ const list = ext
173
+ ? tools.filter((t) => (t.accepted_formats ?? []).map((f) => f.toLowerCase()).includes(ext))
174
+ : tools;
175
+ if (!list.length)
176
+ return text(`No GuruPDF tools accept "${from_format}" files.`);
177
+ const lines = list.map((t) => `- ${t.slug}: ${(t.accepted_formats ?? []).join(", ") || "?"} → ${t.output_format ?? "?"} (${t.credit_cost ?? "?"} credits)`);
178
+ return text(`GuruPDF tools${from_format ? ` for .${from_format.replace(/^\./, "")}` : ""} (${list.length}):\n${lines.join("\n")}`);
179
+ }
180
+ catch (e) {
181
+ return text(friendly(e) ?? `Couldn't load the tool list: ${e.message}`);
182
+ }
183
+ });
184
+ server.tool("check_credits", "Show how many GuruPDF credits the user has left, and how to get more.", {}, async () => {
185
+ if (!client)
186
+ return text(noApiKeyMessage());
187
+ try {
188
+ const { data } = await client.getBalance();
189
+ const lowNote = data.total <= 3 ? " Running low — top up here: https://gurupdf.com/pricing#credit-bundles" : "";
190
+ return text(`GuruPDF credits: ${data.total} total (${data.daily} daily, ${data.subscription} subscription, ${data.purchased} purchased).${lowNote}`);
191
+ }
192
+ catch (e) {
193
+ return text(friendly(e) ?? `Couldn't fetch your balance: ${e.message}`);
194
+ }
195
+ });
196
+ const transport = new StdioServerTransport();
197
+ await server.connect(transport);
@@ -0,0 +1,31 @@
1
+ // User-facing copy the AI agent relays verbatim. The whole point: when a user
2
+ // hits a limit, their own assistant reassures them they can keep going, tells
3
+ // them how (wait or buy), and hands them a clickable link.
4
+ const PRICING = "https://gurupdf.com/pricing";
5
+ const BUNDLES = "https://gurupdf.com/pricing#credit-bundles";
6
+ const PROFILE = "https://gurupdf.com/profile";
7
+ export function outOfCreditsMessage(meta) {
8
+ const need = typeof meta?.credits_required === "number" ? meta.credits_required : undefined;
9
+ const have = typeof meta?.credits_available === "number" ? meta.credits_available : undefined;
10
+ const detail = need != null && have != null ? ` This one needs ${need} credit${need === 1 ? "" : "s"} and you have ${have} left.` : "";
11
+ return (`You're out of GuruPDF credits for now — but no problem, you can keep using it.${detail} ` +
12
+ `Your free credits refresh every day, so you can wait and try again tomorrow, ` +
13
+ `or top up instantly (from a couple of dollars) here: ${BUNDLES} . ` +
14
+ `For higher daily limits, a plan: ${PRICING}`);
15
+ }
16
+ export function rateLimitedMessage(retryAfter) {
17
+ const wait = retryAfter && retryAfter > 0 && retryAfter < 3600 ? `about ${retryAfter} second${retryAfter === 1 ? "" : "s"}` : "a moment";
18
+ return (`You're converting a little fast for the free tier (2 per minute, 10 per day). ` +
19
+ `Nothing's wrong — just wait ${wait} and I can try again. ` +
20
+ `Want no waiting and higher limits? Upgrade here: ${PRICING}`);
21
+ }
22
+ export function fileTooLargeMessage(name, sizeMb, limitMb) {
23
+ return (`"${name}" is ${sizeMb.toFixed(1)} MB, which is over the ${limitMb} MB limit on your current GuruPDF plan. ` +
24
+ `You can compress it first, or upgrade for larger files: ${PRICING}`);
25
+ }
26
+ export function noApiKeyMessage() {
27
+ return (`GuruPDF isn't connected yet — I need an API key. ` +
28
+ `Set GURUPDF_API_KEY in this server's MCP config. ` +
29
+ `To get one: sign up at https://gurupdf.com , then open ${PROFILE} → API tokens and create a token. ` +
30
+ `Free accounts include daily credits, so you can start converting right away.`);
31
+ }
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "gurupdf-mcp",
3
+ "version": "0.1.0",
4
+ "description": "Model Context Protocol server for GuruPDF — convert, compress, merge and edit PDFs and 100+ file formats from any AI agent (Claude, Cursor, VS Code, Windsurf).",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "gurupdf-mcp": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "engines": {
16
+ "node": ">=18"
17
+ },
18
+ "scripts": {
19
+ "build": "tsc",
20
+ "start": "node dist/index.js",
21
+ "prepublishOnly": "npm run build"
22
+ },
23
+ "keywords": [
24
+ "mcp",
25
+ "model-context-protocol",
26
+ "pdf",
27
+ "convert",
28
+ "file-conversion",
29
+ "compress-pdf",
30
+ "merge-pdf",
31
+ "pdf-to-word",
32
+ "gurupdf",
33
+ "claude",
34
+ "cursor",
35
+ "ai-agent"
36
+ ],
37
+ "dependencies": {
38
+ "@modelcontextprotocol/sdk": "^1.12.0",
39
+ "zod": "^3.23.8"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^22.7.0",
43
+ "typescript": "^5.6.0"
44
+ },
45
+ "author": "GuruPDF (https://gurupdf.com)",
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/GuruPDF/gurupdf-mcp.git"
49
+ },
50
+ "homepage": "https://github.com/GuruPDF/gurupdf-mcp#readme",
51
+ "bugs": {
52
+ "url": "https://github.com/GuruPDF/gurupdf-mcp/issues"
53
+ },
54
+ "publishConfig": {
55
+ "access": "public"
56
+ }
57
+ }