@kolmopdf/mcp-server 1.2.1 → 1.2.2

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
@@ -16,13 +16,15 @@ MCP server for [KolmoPDF](https://www.kolmopdf.com) — high-fidelity PDF→Mark
16
16
  }
17
17
  ```
18
18
 
19
+ On macOS GUI clients, `npx` may not be on the app's PATH even when it works in Terminal. Run `command -v npx` and use that absolute path as `command`. Put `KOLMOPDF_API_KEY` in the client's private MCP `env`, then fully restart the app.
20
+
19
21
  ## Environment variables
20
22
 
21
23
  | Variable | Required | Default | Description |
22
24
  | --- | --- | --- | --- |
23
25
  | `KOLMOPDF_API_KEY` | yes | — | API key from https://www.kolmopdf.com/api-keys |
24
26
  | `KOLMOPDF_BASE_URL` | no | `https://www.kolmopdf.com` | Override for enterprise/debug |
25
- | `KOLMOPDF_OUTPUT_DIR` | no | `./kolmopdf-output` | Extraction root |
27
+ | `KOLMOPDF_OUTPUT_DIR` | no | `~/kolmopdf-output` | Absolute or cwd-relative extraction root |
26
28
  | `KOLMOPDF_POLL_INTERVAL_MS` | no | `2000` | Status poll interval |
27
29
  | `KOLMOPDF_MAX_POLL_MINUTES` | no | `30` | Max client-side wait |
28
30
  | `KOLMOPDF_HTTP_TIMEOUT_MS` | no | `60000` | Per-call HTTP timeout |
package/dist/index.cjs CHANGED
@@ -38,19 +38,19 @@ var ERROR_SPECS = {
38
38
  // --- API codes ---
39
39
  invalid_api_key: {
40
40
  message: "API key is missing or invalid.",
41
- remediation: "Create a key at https://www.kolmopdf.com/api-keys (requires Plus/Pro).",
41
+ remediation: "Create a key at https://www.kolmopdf.com/api-keys. Every account, including PAYG, can create one API key.",
42
42
  httpStatus: 401,
43
43
  source: "api"
44
44
  },
45
45
  insufficient_points: {
46
46
  message: "Not enough credits.",
47
- remediation: "Top up at https://www.kolmopdf.com/subscription.",
47
+ remediation: "Buy one-time credits at https://www.kolmopdf.com/credits \u2014 no subscription required. Credits are shared with the web account. Check the API key spending limit separately. Never start a purchase without the user's confirmation.",
48
48
  httpStatus: 402,
49
49
  source: "api"
50
50
  },
51
51
  points_deduction_failed: {
52
52
  message: "Credit deduction failed.",
53
- remediation: "Retry; if it persists contact support.",
53
+ remediation: "Check your balance and API key limit. Buy one-time credits at https://www.kolmopdf.com/credits; do not retry a paid operation or purchase automatically.",
54
54
  httpStatus: 402,
55
55
  source: "api"
56
56
  },
@@ -474,9 +474,11 @@ var KolmoPdfClient = class {
474
474
  };
475
475
 
476
476
  // src/config.ts
477
+ var import_node_os = require("os");
478
+ var import_node_path = require("path");
477
479
  var DEFAULTS = {
478
480
  baseUrl: "https://www.kolmopdf.com",
479
- outputDir: "./kolmopdf-output",
481
+ outputDir: (0, import_node_path.resolve)((0, import_node_os.homedir)(), "kolmopdf-output"),
480
482
  pollIntervalMs: 2e3,
481
483
  maxPollMinutes: 30,
482
484
  httpTimeoutMs: 6e4,
@@ -490,12 +492,16 @@ function intFromEnv(value, fallback) {
490
492
  function trimTrailingSlash(url) {
491
493
  return url.replace(/\/+$/, "");
492
494
  }
495
+ function normalizeApiKey(value) {
496
+ const trimmed = value?.trim();
497
+ if (!trimmed || /^\$\{KOLMOPDF_API_KEY(?::-[^}]*)?\}$/.test(trimmed)) return void 0;
498
+ return trimmed;
499
+ }
493
500
  function loadConfig(env = process.env) {
494
- const apiKeyRaw = env.KOLMOPDF_API_KEY?.trim();
495
501
  return {
496
- apiKey: apiKeyRaw && apiKeyRaw.length > 0 ? apiKeyRaw : void 0,
502
+ apiKey: normalizeApiKey(env.KOLMOPDF_API_KEY),
497
503
  baseUrl: trimTrailingSlash(env.KOLMOPDF_BASE_URL?.trim() || DEFAULTS.baseUrl),
498
- outputDir: env.KOLMOPDF_OUTPUT_DIR?.trim() || DEFAULTS.outputDir,
504
+ outputDir: (0, import_node_path.resolve)(env.KOLMOPDF_OUTPUT_DIR?.trim() || DEFAULTS.outputDir),
499
505
  pollIntervalMs: intFromEnv(env.KOLMOPDF_POLL_INTERVAL_MS, DEFAULTS.pollIntervalMs),
500
506
  maxPollMinutes: intFromEnv(env.KOLMOPDF_MAX_POLL_MINUTES, DEFAULTS.maxPollMinutes),
501
507
  httpTimeoutMs: intFromEnv(env.KOLMOPDF_HTTP_TIMEOUT_MS, DEFAULTS.httpTimeoutMs),
@@ -533,9 +539,23 @@ async function checkBalanceHandler(_args, ctx) {
533
539
  // src/tools/convert.ts
534
540
  var import_node_fs = require("fs");
535
541
  var import_promises4 = require("fs/promises");
536
- var import_node_path2 = require("path");
542
+ var import_node_path4 = require("path");
537
543
  var import_zod2 = require("zod");
538
544
 
545
+ // src/output.ts
546
+ var import_node_path2 = require("path");
547
+ function resolveOutputRoot(baseDir, subdir) {
548
+ const root = (0, import_node_path2.resolve)(baseDir);
549
+ const candidate = (0, import_node_path2.resolve)(root, subdir);
550
+ const rel = (0, import_node_path2.relative)(root, candidate);
551
+ if (rel.startsWith("..") || (0, import_node_path2.isAbsolute)(rel)) {
552
+ throw new KolmoPdfError("client_local_validation", {
553
+ message: "output_subdir must stay inside KOLMOPDF_OUTPUT_DIR."
554
+ });
555
+ }
556
+ return candidate;
557
+ }
558
+
539
559
  // src/pages.ts
540
560
  var import_promises2 = require("fs/promises");
541
561
  var import_pdf_lib = require("pdf-lib");
@@ -577,7 +597,7 @@ function isRetryable(err) {
577
597
  return false;
578
598
  }
579
599
  function sleep(ms) {
580
- return new Promise((resolve5) => setTimeout(resolve5, ms));
600
+ return new Promise((resolve8) => setTimeout(resolve8, ms));
581
601
  }
582
602
  async function fetchStatusWithRetry(client, taskId) {
583
603
  for (let attempt = 1; attempt <= RETRY_POLICY.maxAttempts; attempt++) {
@@ -716,7 +736,7 @@ async function pollUntilComplete(ctx) {
716
736
 
717
737
  // src/sniff.ts
718
738
  var import_promises3 = require("fs/promises");
719
- var import_node_path = require("path");
739
+ var import_node_path3 = require("path");
720
740
  var EXT = {
721
741
  zip: ".zip",
722
742
  pdf: ".pdf",
@@ -780,10 +800,13 @@ function normalizeFormat(targetFormat) {
780
800
  return targetFormat;
781
801
  }
782
802
  }
803
+ function resolveConvertKind(sniffedKind, targetFormat) {
804
+ return normalizeFormat(targetFormat) === "docx" && sniffedKind === "zip" ? "docx" : sniffedKind;
805
+ }
783
806
  async function convertHandler(args, ctx) {
784
807
  const client = ctx.getClient();
785
- const filePath = (0, import_node_path2.resolve)(args.file_path);
786
- const filename = (0, import_node_path2.basename)(filePath);
808
+ const filePath = (0, import_node_path4.resolve)(args.file_path);
809
+ const filename = (0, import_node_path4.basename)(filePath);
787
810
  const fileSize = await readFileSize(filePath);
788
811
  if (fileSize > MAX_FILE_BYTES) {
789
812
  throw new KolmoPdfError("convert_file_too_large");
@@ -814,13 +837,14 @@ async function convertHandler(args, ctx) {
814
837
  });
815
838
  await ctx.progress?.report("[downloading] Fetching converted file...");
816
839
  const subdir = args.output_subdir || taskId;
817
- const outputRoot = (0, import_node_path2.resolve)(ctx.config.outputDir, subdir);
840
+ const outputRoot = resolveOutputRoot(ctx.config.outputDir, subdir);
818
841
  (0, import_node_fs.mkdirSync)(outputRoot, { recursive: true });
819
- const tempPath = (0, import_node_path2.join)(outputRoot, "download.bin");
842
+ const tempPath = (0, import_node_path4.join)(outputRoot, "download.bin");
820
843
  const ws = (0, import_node_fs.createWriteStream)(tempPath);
821
844
  await client.download(taskId, ws, { destPath: tempPath });
822
- const kind = await sniffFile(tempPath);
823
- const outputPath = (0, import_node_path2.join)(outputRoot, `result${extensionForKind(kind)}`);
845
+ const sniffedKind = await sniffFile(tempPath);
846
+ const kind = resolveConvertKind(sniffedKind, args.target_format);
847
+ const outputPath = (0, import_node_path4.join)(outputRoot, `result${extensionForKind(kind)}`);
824
848
  await (0, import_promises4.rename)(tempPath, outputPath);
825
849
  const output = {
826
850
  task_id: taskId,
@@ -836,7 +860,7 @@ async function convertHandler(args, ctx) {
836
860
  }
837
861
 
838
862
  // src/tools/estimate-cost.ts
839
- var import_node_path3 = require("path");
863
+ var import_node_path5 = require("path");
840
864
  var import_zod3 = require("zod");
841
865
  var estimateCostName = "kolmopdf_estimate_cost";
842
866
  var estimateCostDescription = "Estimate the credit cost of a KolmoPDF operation before running it. Reads page count locally and checks the current balance. Does not spend credits.";
@@ -866,7 +890,7 @@ async function estimateCostHandler(args, ctx) {
866
890
  const client = ctx.getClient();
867
891
  let pages = null;
868
892
  if (args.operation !== "convert") {
869
- const filePath = (0, import_node_path3.resolve)(args.file_path);
893
+ const filePath = (0, import_node_path5.resolve)(args.file_path);
870
894
  pages = await readPageCount(filePath);
871
895
  }
872
896
  const estimatedCredits = estimateCredits(args.operation, pages ?? 1);
@@ -900,12 +924,12 @@ async function getTaskStatusHandler(args, ctx) {
900
924
  // src/tools/parse-pdf.ts
901
925
  var import_node_fs3 = require("fs");
902
926
  var import_promises6 = require("fs/promises");
903
- var import_node_path5 = require("path");
927
+ var import_node_path7 = require("path");
904
928
  var import_zod5 = require("zod");
905
929
 
906
930
  // src/extract.ts
907
931
  var import_node_fs2 = require("fs");
908
- var import_node_path4 = require("path");
932
+ var import_node_path6 = require("path");
909
933
  var import_promises5 = require("stream/promises");
910
934
  var import_yauzl = require("yauzl");
911
935
  function pickPrimaryMarkdownPath(candidates) {
@@ -925,6 +949,24 @@ function pickPrimaryMarkdownPath(candidates) {
925
949
  scored.sort((a, b) => b.score - a.score);
926
950
  return scored[0]?.path ?? null;
927
951
  }
952
+ function safeZipEntryPath(destDir, entryName) {
953
+ const normalized = entryName.replace(/\\/g, "/");
954
+ const segments = normalized.split("/").filter((segment) => segment && segment !== ".");
955
+ if (normalized.includes("\0") || normalized.startsWith("/") || /^[A-Za-z]:/.test(normalized) || segments.includes("..")) {
956
+ throw new KolmoPdfError("client_extract_failed", {
957
+ message: `Unsafe ZIP entry path: ${entryName}`
958
+ });
959
+ }
960
+ const root = (0, import_node_path6.resolve)(destDir);
961
+ const entryPath = (0, import_node_path6.resolve)(root, ...segments);
962
+ const rel = (0, import_node_path6.relative)(root, entryPath);
963
+ if (rel.startsWith("..") || (0, import_node_path6.isAbsolute)(rel)) {
964
+ throw new KolmoPdfError("client_extract_failed", {
965
+ message: `Unsafe ZIP entry path: ${entryName}`
966
+ });
967
+ }
968
+ return entryPath;
969
+ }
928
970
  async function extractZip(zipPath, destDir) {
929
971
  (0, import_node_fs2.mkdirSync)(destDir, { recursive: true });
930
972
  const zipFile = await openZip(zipPath);
@@ -932,7 +974,7 @@ async function extractZip(zipPath, destDir) {
932
974
  const mdCandidates = [];
933
975
  let imagesDir = null;
934
976
  for await (const entry of iterEntries(zipFile)) {
935
- const entryPath = (0, import_node_path4.join)(destDir, entry.fileName);
977
+ const entryPath = safeZipEntryPath(destDir, entry.fileName);
936
978
  if (entry.fileName.endsWith("/")) {
937
979
  (0, import_node_fs2.mkdirSync)(entryPath, { recursive: true });
938
980
  if (entry.fileName.includes("images")) {
@@ -940,7 +982,7 @@ async function extractZip(zipPath, destDir) {
940
982
  }
941
983
  continue;
942
984
  }
943
- (0, import_node_fs2.mkdirSync)((0, import_node_path4.dirname)(entryPath), { recursive: true });
985
+ (0, import_node_fs2.mkdirSync)((0, import_node_path6.dirname)(entryPath), { recursive: true });
944
986
  const readStream = await openReadStream(zipFile, entry);
945
987
  const writeStream = (0, import_node_fs2.createWriteStream)(entryPath);
946
988
  await (0, import_promises5.pipeline)(readStream, writeStream);
@@ -955,36 +997,36 @@ async function extractZip(zipPath, destDir) {
955
997
  }
956
998
  if (!imagesDir && /images\//i.test(entry.fileName)) {
957
999
  const prefix = entry.fileName.split("images/")[0] ?? "";
958
- imagesDir = (0, import_node_path4.join)(destDir, prefix, "images");
1000
+ imagesDir = (0, import_node_path6.join)(destDir, prefix, "images");
959
1001
  }
960
1002
  }
961
1003
  const markdownPath = pickPrimaryMarkdownPath(mdCandidates);
962
1004
  return { markdownPath, imagesDir, outputRoot: destDir, files };
963
1005
  }
964
1006
  function openZip(path) {
965
- return new Promise((resolve5, reject) => {
1007
+ return new Promise((resolve8, reject) => {
966
1008
  (0, import_yauzl.open)(path, { lazyEntries: true }, (err, zf) => {
967
1009
  if (err || !zf) return reject(err ?? new Error("Failed to open zip"));
968
- resolve5(zf);
1010
+ resolve8(zf);
969
1011
  });
970
1012
  });
971
1013
  }
972
1014
  async function* iterEntries(zipFile) {
973
- let resolve5 = null;
1015
+ let resolve8 = null;
974
1016
  const queue = [];
975
1017
  zipFile.on("entry", (entry) => {
976
- if (resolve5) {
977
- const r = resolve5;
978
- resolve5 = null;
1018
+ if (resolve8) {
1019
+ const r = resolve8;
1020
+ resolve8 = null;
979
1021
  r(entry);
980
1022
  } else {
981
1023
  queue.push(entry);
982
1024
  }
983
1025
  });
984
1026
  zipFile.on("end", () => {
985
- if (resolve5) {
986
- const r = resolve5;
987
- resolve5 = null;
1027
+ if (resolve8) {
1028
+ const r = resolve8;
1029
+ resolve8 = null;
988
1030
  r(null);
989
1031
  } else {
990
1032
  queue.push(null);
@@ -993,7 +1035,7 @@ async function* iterEntries(zipFile) {
993
1035
  zipFile.readEntry();
994
1036
  while (true) {
995
1037
  const entry = queue.length > 0 ? queue.shift() : await new Promise((r) => {
996
- resolve5 = r;
1038
+ resolve8 = r;
997
1039
  });
998
1040
  if (entry === null) break;
999
1041
  yield entry;
@@ -1001,10 +1043,10 @@ async function* iterEntries(zipFile) {
1001
1043
  }
1002
1044
  }
1003
1045
  function openReadStream(zipFile, entry) {
1004
- return new Promise((resolve5, reject) => {
1046
+ return new Promise((resolve8, reject) => {
1005
1047
  zipFile.openReadStream(entry, (err, stream) => {
1006
1048
  if (err || !stream) return reject(err ?? new Error("Failed to open entry stream"));
1007
- resolve5(stream);
1049
+ resolve8(stream);
1008
1050
  });
1009
1051
  });
1010
1052
  }
@@ -1029,8 +1071,8 @@ var parsePdfInputSchema = import_zod5.z.object({
1029
1071
  });
1030
1072
  async function parsePdfHandler(args, ctx) {
1031
1073
  const client = ctx.getClient();
1032
- const filePath = (0, import_node_path5.resolve)(args.file_path);
1033
- const filename = (0, import_node_path5.basename)(filePath);
1074
+ const filePath = (0, import_node_path7.resolve)(args.file_path);
1075
+ const filename = (0, import_node_path7.basename)(filePath);
1034
1076
  const fileSize = await readFileSize(filePath);
1035
1077
  if (fileSize > MAX_FILE_BYTES) {
1036
1078
  throw new KolmoPdfError("parse_file_too_large");
@@ -1069,9 +1111,9 @@ async function parsePdfHandler(args, ctx) {
1069
1111
  });
1070
1112
  await ctx.progress?.report("[downloading] Fetching result...");
1071
1113
  const subdir = args.output_subdir || taskId;
1072
- const outputRoot = (0, import_node_path5.resolve)(ctx.config.outputDir, subdir);
1114
+ const outputRoot = resolveOutputRoot(ctx.config.outputDir, subdir);
1073
1115
  (0, import_node_fs3.mkdirSync)(outputRoot, { recursive: true });
1074
- const downloadPath = (0, import_node_path5.join)(outputRoot, "download.bin");
1116
+ const downloadPath = (0, import_node_path7.join)(outputRoot, "download.bin");
1075
1117
  const ws = (0, import_node_fs3.createWriteStream)(downloadPath);
1076
1118
  await client.download(taskId, ws, { destPath: downloadPath });
1077
1119
  let markdownPath;
@@ -1079,14 +1121,14 @@ async function parsePdfHandler(args, ctx) {
1079
1121
  let outputType;
1080
1122
  const kind = await sniffFile(downloadPath);
1081
1123
  if (kind === "zip") {
1082
- const zipPath = (0, import_node_path5.join)(outputRoot, "result.zip");
1124
+ const zipPath = (0, import_node_path7.join)(outputRoot, "result.zip");
1083
1125
  await (0, import_promises6.rename)(downloadPath, zipPath);
1084
1126
  const extracted = await extractZip(zipPath, outputRoot);
1085
- markdownPath = extracted.markdownPath || (0, import_node_path5.join)(outputRoot, "result.md");
1127
+ markdownPath = extracted.markdownPath || (0, import_node_path7.join)(outputRoot, "result.md");
1086
1128
  imagesDir = extracted.imagesDir;
1087
1129
  outputType = "zip_extracted";
1088
1130
  } else {
1089
- markdownPath = (0, import_node_path5.join)(outputRoot, "result.md");
1131
+ markdownPath = (0, import_node_path7.join)(outputRoot, "result.md");
1090
1132
  await (0, import_promises6.rename)(downloadPath, markdownPath);
1091
1133
  outputType = "markdown_file";
1092
1134
  }
@@ -1113,7 +1155,7 @@ async function parsePdfHandler(args, ctx) {
1113
1155
  // src/tools/translate-pdf.ts
1114
1156
  var import_node_fs4 = require("fs");
1115
1157
  var import_promises7 = require("fs/promises");
1116
- var import_node_path6 = require("path");
1158
+ var import_node_path8 = require("path");
1117
1159
  var import_zod6 = require("zod");
1118
1160
  var translatePdfName = "kolmopdf_translate_pdf";
1119
1161
  var translatePdfDescription = "Translate a PDF while preserving its original layout via KolmoPDF. Produces a translated PDF, or a ZIP of PDFs when multiple layout modes are requested.";
@@ -1128,8 +1170,8 @@ var translatePdfInputSchema = import_zod6.z.object({
1128
1170
  });
1129
1171
  async function translatePdfHandler(args, ctx) {
1130
1172
  const client = ctx.getClient();
1131
- const filePath = (0, import_node_path6.resolve)(args.file_path);
1132
- const filename = (0, import_node_path6.basename)(filePath);
1173
+ const filePath = (0, import_node_path8.resolve)(args.file_path);
1174
+ const filename = (0, import_node_path8.basename)(filePath);
1133
1175
  const fileSize = await readFileSize(filePath);
1134
1176
  if (fileSize > MAX_FILE_BYTES) {
1135
1177
  throw new KolmoPdfError("translate_pdf_file_too_large");
@@ -1164,13 +1206,13 @@ async function translatePdfHandler(args, ctx) {
1164
1206
  });
1165
1207
  await ctx.progress?.report("[downloading] Fetching translated result...");
1166
1208
  const subdir = args.output_subdir || taskId;
1167
- const outputRoot = (0, import_node_path6.resolve)(ctx.config.outputDir, subdir);
1209
+ const outputRoot = resolveOutputRoot(ctx.config.outputDir, subdir);
1168
1210
  (0, import_node_fs4.mkdirSync)(outputRoot, { recursive: true });
1169
- const tempPath = (0, import_node_path6.join)(outputRoot, "download.bin");
1211
+ const tempPath = (0, import_node_path8.join)(outputRoot, "download.bin");
1170
1212
  const ws = (0, import_node_fs4.createWriteStream)(tempPath);
1171
1213
  await client.download(taskId, ws, { destPath: tempPath });
1172
1214
  const kind = await sniffFile(tempPath);
1173
- let translatedPdfPath = (0, import_node_path6.join)(outputRoot, `translated${extensionForKind(kind)}`);
1215
+ let translatedPdfPath = (0, import_node_path8.join)(outputRoot, `translated${extensionForKind(kind)}`);
1174
1216
  let archivePath;
1175
1217
  await (0, import_promises7.rename)(tempPath, translatedPdfPath);
1176
1218
  if (kind === "zip") {
@@ -1195,7 +1237,7 @@ async function translatePdfHandler(args, ctx) {
1195
1237
  }
1196
1238
 
1197
1239
  // src/index.ts
1198
- var VERSION = "1.1.0";
1240
+ var VERSION = "1.2.2";
1199
1241
  function buildContext() {
1200
1242
  const config = loadConfig();
1201
1243
  return {