@kolmopdf/mcp-server 1.2.0 → 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/dist/index.js CHANGED
@@ -14,19 +14,19 @@ var ERROR_SPECS = {
14
14
  // --- API codes ---
15
15
  invalid_api_key: {
16
16
  message: "API key is missing or invalid.",
17
- remediation: "Create a key at https://www.kolmopdf.com/api-keys (requires Plus/Pro).",
17
+ remediation: "Create a key at https://www.kolmopdf.com/api-keys. Every account, including PAYG, can create one API key.",
18
18
  httpStatus: 401,
19
19
  source: "api"
20
20
  },
21
21
  insufficient_points: {
22
22
  message: "Not enough credits.",
23
- remediation: "Top up at https://www.kolmopdf.com/subscription.",
23
+ 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.",
24
24
  httpStatus: 402,
25
25
  source: "api"
26
26
  },
27
27
  points_deduction_failed: {
28
28
  message: "Credit deduction failed.",
29
- remediation: "Retry; if it persists contact support.",
29
+ 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.",
30
30
  httpStatus: 402,
31
31
  source: "api"
32
32
  },
@@ -450,9 +450,11 @@ var KolmoPdfClient = class {
450
450
  };
451
451
 
452
452
  // src/config.ts
453
+ import { homedir } from "os";
454
+ import { resolve } from "path";
453
455
  var DEFAULTS = {
454
456
  baseUrl: "https://www.kolmopdf.com",
455
- outputDir: "./kolmopdf-output",
457
+ outputDir: resolve(homedir(), "kolmopdf-output"),
456
458
  pollIntervalMs: 2e3,
457
459
  maxPollMinutes: 30,
458
460
  httpTimeoutMs: 6e4,
@@ -466,12 +468,16 @@ function intFromEnv(value, fallback) {
466
468
  function trimTrailingSlash(url) {
467
469
  return url.replace(/\/+$/, "");
468
470
  }
471
+ function normalizeApiKey(value) {
472
+ const trimmed = value?.trim();
473
+ if (!trimmed || /^\$\{KOLMOPDF_API_KEY(?::-[^}]*)?\}$/.test(trimmed)) return void 0;
474
+ return trimmed;
475
+ }
469
476
  function loadConfig(env = process.env) {
470
- const apiKeyRaw = env.KOLMOPDF_API_KEY?.trim();
471
477
  return {
472
- apiKey: apiKeyRaw && apiKeyRaw.length > 0 ? apiKeyRaw : void 0,
478
+ apiKey: normalizeApiKey(env.KOLMOPDF_API_KEY),
473
479
  baseUrl: trimTrailingSlash(env.KOLMOPDF_BASE_URL?.trim() || DEFAULTS.baseUrl),
474
- outputDir: env.KOLMOPDF_OUTPUT_DIR?.trim() || DEFAULTS.outputDir,
480
+ outputDir: resolve(env.KOLMOPDF_OUTPUT_DIR?.trim() || DEFAULTS.outputDir),
475
481
  pollIntervalMs: intFromEnv(env.KOLMOPDF_POLL_INTERVAL_MS, DEFAULTS.pollIntervalMs),
476
482
  maxPollMinutes: intFromEnv(env.KOLMOPDF_MAX_POLL_MINUTES, DEFAULTS.maxPollMinutes),
477
483
  httpTimeoutMs: intFromEnv(env.KOLMOPDF_HTTP_TIMEOUT_MS, DEFAULTS.httpTimeoutMs),
@@ -509,9 +515,23 @@ async function checkBalanceHandler(_args, ctx) {
509
515
  // src/tools/convert.ts
510
516
  import { createWriteStream, mkdirSync } from "fs";
511
517
  import { readFile as readFile2, rename as rename2 } from "fs/promises";
512
- import { basename, join as join2, resolve } from "path";
518
+ import { basename, join as join2, resolve as resolve3 } from "path";
513
519
  import { z as z2 } from "zod";
514
520
 
521
+ // src/output.ts
522
+ import { isAbsolute, relative, resolve as resolve2 } from "path";
523
+ function resolveOutputRoot(baseDir, subdir) {
524
+ const root = resolve2(baseDir);
525
+ const candidate = resolve2(root, subdir);
526
+ const rel = relative(root, candidate);
527
+ if (rel.startsWith("..") || isAbsolute(rel)) {
528
+ throw new KolmoPdfError("client_local_validation", {
529
+ message: "output_subdir must stay inside KOLMOPDF_OUTPUT_DIR."
530
+ });
531
+ }
532
+ return candidate;
533
+ }
534
+
515
535
  // src/pages.ts
516
536
  import { readFile, stat } from "fs/promises";
517
537
  import { PDFDocument } from "pdf-lib";
@@ -553,7 +573,7 @@ function isRetryable(err) {
553
573
  return false;
554
574
  }
555
575
  function sleep(ms) {
556
- return new Promise((resolve5) => setTimeout(resolve5, ms));
576
+ return new Promise((resolve8) => setTimeout(resolve8, ms));
557
577
  }
558
578
  async function fetchStatusWithRetry(client, taskId) {
559
579
  for (let attempt = 1; attempt <= RETRY_POLICY.maxAttempts; attempt++) {
@@ -756,9 +776,12 @@ function normalizeFormat(targetFormat) {
756
776
  return targetFormat;
757
777
  }
758
778
  }
779
+ function resolveConvertKind(sniffedKind, targetFormat) {
780
+ return normalizeFormat(targetFormat) === "docx" && sniffedKind === "zip" ? "docx" : sniffedKind;
781
+ }
759
782
  async function convertHandler(args, ctx) {
760
783
  const client = ctx.getClient();
761
- const filePath = resolve(args.file_path);
784
+ const filePath = resolve3(args.file_path);
762
785
  const filename = basename(filePath);
763
786
  const fileSize = await readFileSize(filePath);
764
787
  if (fileSize > MAX_FILE_BYTES) {
@@ -790,12 +813,13 @@ async function convertHandler(args, ctx) {
790
813
  });
791
814
  await ctx.progress?.report("[downloading] Fetching converted file...");
792
815
  const subdir = args.output_subdir || taskId;
793
- const outputRoot = resolve(ctx.config.outputDir, subdir);
816
+ const outputRoot = resolveOutputRoot(ctx.config.outputDir, subdir);
794
817
  mkdirSync(outputRoot, { recursive: true });
795
818
  const tempPath = join2(outputRoot, "download.bin");
796
819
  const ws = createWriteStream(tempPath);
797
820
  await client.download(taskId, ws, { destPath: tempPath });
798
- const kind = await sniffFile(tempPath);
821
+ const sniffedKind = await sniffFile(tempPath);
822
+ const kind = resolveConvertKind(sniffedKind, args.target_format);
799
823
  const outputPath = join2(outputRoot, `result${extensionForKind(kind)}`);
800
824
  await rename2(tempPath, outputPath);
801
825
  const output = {
@@ -812,7 +836,7 @@ async function convertHandler(args, ctx) {
812
836
  }
813
837
 
814
838
  // src/tools/estimate-cost.ts
815
- import { resolve as resolve2 } from "path";
839
+ import { resolve as resolve4 } from "path";
816
840
  import { z as z3 } from "zod";
817
841
  var estimateCostName = "kolmopdf_estimate_cost";
818
842
  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.";
@@ -842,7 +866,7 @@ async function estimateCostHandler(args, ctx) {
842
866
  const client = ctx.getClient();
843
867
  let pages = null;
844
868
  if (args.operation !== "convert") {
845
- const filePath = resolve2(args.file_path);
869
+ const filePath = resolve4(args.file_path);
846
870
  pages = await readPageCount(filePath);
847
871
  }
848
872
  const estimatedCredits = estimateCredits(args.operation, pages ?? 1);
@@ -876,12 +900,12 @@ async function getTaskStatusHandler(args, ctx) {
876
900
  // src/tools/parse-pdf.ts
877
901
  import { createWriteStream as createWriteStream3, mkdirSync as mkdirSync3 } from "fs";
878
902
  import { readFile as readFile3, rename as rename3 } from "fs/promises";
879
- import { basename as basename2, join as join4, resolve as resolve3 } from "path";
903
+ import { basename as basename2, join as join4, resolve as resolve6 } from "path";
880
904
  import { z as z5 } from "zod";
881
905
 
882
906
  // src/extract.ts
883
907
  import { createWriteStream as createWriteStream2, mkdirSync as mkdirSync2, readFileSync } from "fs";
884
- import { dirname, join as join3 } from "path";
908
+ import { dirname, isAbsolute as isAbsolute2, join as join3, relative as relative2, resolve as resolve5 } from "path";
885
909
  import { pipeline as pipeline2 } from "stream/promises";
886
910
  import { open as yauzlOpen } from "yauzl";
887
911
  function pickPrimaryMarkdownPath(candidates) {
@@ -901,6 +925,24 @@ function pickPrimaryMarkdownPath(candidates) {
901
925
  scored.sort((a, b) => b.score - a.score);
902
926
  return scored[0]?.path ?? null;
903
927
  }
928
+ function safeZipEntryPath(destDir, entryName) {
929
+ const normalized = entryName.replace(/\\/g, "/");
930
+ const segments = normalized.split("/").filter((segment) => segment && segment !== ".");
931
+ if (normalized.includes("\0") || normalized.startsWith("/") || /^[A-Za-z]:/.test(normalized) || segments.includes("..")) {
932
+ throw new KolmoPdfError("client_extract_failed", {
933
+ message: `Unsafe ZIP entry path: ${entryName}`
934
+ });
935
+ }
936
+ const root = resolve5(destDir);
937
+ const entryPath = resolve5(root, ...segments);
938
+ const rel = relative2(root, entryPath);
939
+ if (rel.startsWith("..") || isAbsolute2(rel)) {
940
+ throw new KolmoPdfError("client_extract_failed", {
941
+ message: `Unsafe ZIP entry path: ${entryName}`
942
+ });
943
+ }
944
+ return entryPath;
945
+ }
904
946
  async function extractZip(zipPath, destDir) {
905
947
  mkdirSync2(destDir, { recursive: true });
906
948
  const zipFile = await openZip(zipPath);
@@ -908,7 +950,7 @@ async function extractZip(zipPath, destDir) {
908
950
  const mdCandidates = [];
909
951
  let imagesDir = null;
910
952
  for await (const entry of iterEntries(zipFile)) {
911
- const entryPath = join3(destDir, entry.fileName);
953
+ const entryPath = safeZipEntryPath(destDir, entry.fileName);
912
954
  if (entry.fileName.endsWith("/")) {
913
955
  mkdirSync2(entryPath, { recursive: true });
914
956
  if (entry.fileName.includes("images")) {
@@ -938,29 +980,29 @@ async function extractZip(zipPath, destDir) {
938
980
  return { markdownPath, imagesDir, outputRoot: destDir, files };
939
981
  }
940
982
  function openZip(path) {
941
- return new Promise((resolve5, reject) => {
983
+ return new Promise((resolve8, reject) => {
942
984
  yauzlOpen(path, { lazyEntries: true }, (err, zf) => {
943
985
  if (err || !zf) return reject(err ?? new Error("Failed to open zip"));
944
- resolve5(zf);
986
+ resolve8(zf);
945
987
  });
946
988
  });
947
989
  }
948
990
  async function* iterEntries(zipFile) {
949
- let resolve5 = null;
991
+ let resolve8 = null;
950
992
  const queue = [];
951
993
  zipFile.on("entry", (entry) => {
952
- if (resolve5) {
953
- const r = resolve5;
954
- resolve5 = null;
994
+ if (resolve8) {
995
+ const r = resolve8;
996
+ resolve8 = null;
955
997
  r(entry);
956
998
  } else {
957
999
  queue.push(entry);
958
1000
  }
959
1001
  });
960
1002
  zipFile.on("end", () => {
961
- if (resolve5) {
962
- const r = resolve5;
963
- resolve5 = null;
1003
+ if (resolve8) {
1004
+ const r = resolve8;
1005
+ resolve8 = null;
964
1006
  r(null);
965
1007
  } else {
966
1008
  queue.push(null);
@@ -969,7 +1011,7 @@ async function* iterEntries(zipFile) {
969
1011
  zipFile.readEntry();
970
1012
  while (true) {
971
1013
  const entry = queue.length > 0 ? queue.shift() : await new Promise((r) => {
972
- resolve5 = r;
1014
+ resolve8 = r;
973
1015
  });
974
1016
  if (entry === null) break;
975
1017
  yield entry;
@@ -977,10 +1019,10 @@ async function* iterEntries(zipFile) {
977
1019
  }
978
1020
  }
979
1021
  function openReadStream(zipFile, entry) {
980
- return new Promise((resolve5, reject) => {
1022
+ return new Promise((resolve8, reject) => {
981
1023
  zipFile.openReadStream(entry, (err, stream) => {
982
1024
  if (err || !stream) return reject(err ?? new Error("Failed to open entry stream"));
983
- resolve5(stream);
1025
+ resolve8(stream);
984
1026
  });
985
1027
  });
986
1028
  }
@@ -1005,7 +1047,7 @@ var parsePdfInputSchema = z5.object({
1005
1047
  });
1006
1048
  async function parsePdfHandler(args, ctx) {
1007
1049
  const client = ctx.getClient();
1008
- const filePath = resolve3(args.file_path);
1050
+ const filePath = resolve6(args.file_path);
1009
1051
  const filename = basename2(filePath);
1010
1052
  const fileSize = await readFileSize(filePath);
1011
1053
  if (fileSize > MAX_FILE_BYTES) {
@@ -1045,7 +1087,7 @@ async function parsePdfHandler(args, ctx) {
1045
1087
  });
1046
1088
  await ctx.progress?.report("[downloading] Fetching result...");
1047
1089
  const subdir = args.output_subdir || taskId;
1048
- const outputRoot = resolve3(ctx.config.outputDir, subdir);
1090
+ const outputRoot = resolveOutputRoot(ctx.config.outputDir, subdir);
1049
1091
  mkdirSync3(outputRoot, { recursive: true });
1050
1092
  const downloadPath = join4(outputRoot, "download.bin");
1051
1093
  const ws = createWriteStream3(downloadPath);
@@ -1089,7 +1131,7 @@ async function parsePdfHandler(args, ctx) {
1089
1131
  // src/tools/translate-pdf.ts
1090
1132
  import { createWriteStream as createWriteStream4, mkdirSync as mkdirSync4 } from "fs";
1091
1133
  import { readFile as readFile4, rename as rename4 } from "fs/promises";
1092
- import { basename as basename3, join as join5, resolve as resolve4 } from "path";
1134
+ import { basename as basename3, join as join5, resolve as resolve7 } from "path";
1093
1135
  import { z as z6 } from "zod";
1094
1136
  var translatePdfName = "kolmopdf_translate_pdf";
1095
1137
  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.";
@@ -1104,7 +1146,7 @@ var translatePdfInputSchema = z6.object({
1104
1146
  });
1105
1147
  async function translatePdfHandler(args, ctx) {
1106
1148
  const client = ctx.getClient();
1107
- const filePath = resolve4(args.file_path);
1149
+ const filePath = resolve7(args.file_path);
1108
1150
  const filename = basename3(filePath);
1109
1151
  const fileSize = await readFileSize(filePath);
1110
1152
  if (fileSize > MAX_FILE_BYTES) {
@@ -1140,7 +1182,7 @@ async function translatePdfHandler(args, ctx) {
1140
1182
  });
1141
1183
  await ctx.progress?.report("[downloading] Fetching translated result...");
1142
1184
  const subdir = args.output_subdir || taskId;
1143
- const outputRoot = resolve4(ctx.config.outputDir, subdir);
1185
+ const outputRoot = resolveOutputRoot(ctx.config.outputDir, subdir);
1144
1186
  mkdirSync4(outputRoot, { recursive: true });
1145
1187
  const tempPath = join5(outputRoot, "download.bin");
1146
1188
  const ws = createWriteStream4(tempPath);
@@ -1171,7 +1213,7 @@ async function translatePdfHandler(args, ctx) {
1171
1213
  }
1172
1214
 
1173
1215
  // src/index.ts
1174
- var VERSION = "1.1.0";
1216
+ var VERSION = "1.2.2";
1175
1217
  function buildContext() {
1176
1218
  const config = loadConfig();
1177
1219
  return {