@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/README.md +3 -1
- package/dist/index.cjs +91 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +77 -35
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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:
|
|
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((
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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((
|
|
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
|
-
|
|
986
|
+
resolve8(zf);
|
|
945
987
|
});
|
|
946
988
|
});
|
|
947
989
|
}
|
|
948
990
|
async function* iterEntries(zipFile) {
|
|
949
|
-
let
|
|
991
|
+
let resolve8 = null;
|
|
950
992
|
const queue = [];
|
|
951
993
|
zipFile.on("entry", (entry) => {
|
|
952
|
-
if (
|
|
953
|
-
const r =
|
|
954
|
-
|
|
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 (
|
|
962
|
-
const r =
|
|
963
|
-
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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.
|
|
1216
|
+
var VERSION = "1.2.2";
|
|
1175
1217
|
function buildContext() {
|
|
1176
1218
|
const config = loadConfig();
|
|
1177
1219
|
return {
|