@kolmopdf/mcp-server 1.0.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/.claude-plugin/marketplace.json +25 -0
- package/.github/ISSUE_TEMPLATE/bug-report.yml +75 -0
- package/.github/workflows/ci.yml +98 -0
- package/.github/workflows/release.yml +52 -0
- package/CHANGELOG.md +12 -0
- package/LICENSE +21 -0
- package/README.md +63 -0
- package/biome.json +33 -0
- package/codex-skill/kolmopdf/SKILL.md +108 -0
- package/codex-skill/kolmopdf/references/chain-recipes.md +35 -0
- package/codex-skill/kolmopdf/references/parameter-glossary.md +72 -0
- package/doc/apidocs/Format_Conversion_API_Guide.md +117 -0
- package/doc/apidocs/PDF_Layout_Translation_API_Guide.md +138 -0
- package/doc/apidocs/PDF_Parsing_API_Guide.md +364 -0
- package/doc/plan/DEVELOPMENT.md +896 -0
- package/doc/plan/DISTRIBUTION.md +377 -0
- package/doc/plan/TESTING_AND_USAGE.md +370 -0
- package/package.json +30 -0
- package/packages/mcp-server/LICENSE +21 -0
- package/packages/mcp-server/README.md +37 -0
- package/packages/mcp-server/dist/index.cjs +1004 -0
- package/packages/mcp-server/dist/index.cjs.map +1 -0
- package/packages/mcp-server/dist/index.d.cts +29 -0
- package/packages/mcp-server/dist/index.d.ts +29 -0
- package/packages/mcp-server/dist/index.js +979 -0
- package/packages/mcp-server/dist/index.js.map +1 -0
- package/packages/mcp-server/package.json +54 -0
- package/packages/mcp-server/src/client.ts +235 -0
- package/packages/mcp-server/src/config.ts +62 -0
- package/packages/mcp-server/src/context.ts +27 -0
- package/packages/mcp-server/src/errors.ts +271 -0
- package/packages/mcp-server/src/extract.ts +102 -0
- package/packages/mcp-server/src/index.ts +142 -0
- package/packages/mcp-server/src/pages.ts +16 -0
- package/packages/mcp-server/src/polling.ts +84 -0
- package/packages/mcp-server/src/progress.ts +48 -0
- package/packages/mcp-server/src/tools/check-balance.ts +33 -0
- package/packages/mcp-server/src/tools/convert.ts +130 -0
- package/packages/mcp-server/src/tools/estimate-cost.ts +82 -0
- package/packages/mcp-server/src/tools/get-task-status.ts +24 -0
- package/packages/mcp-server/src/tools/parse-pdf.ts +147 -0
- package/packages/mcp-server/src/tools/translate-pdf.ts +110 -0
- package/packages/mcp-server/tests/integration/smoke.test.ts +33 -0
- package/packages/mcp-server/tests/unit/config.test.ts +49 -0
- package/packages/mcp-server/tests/unit/convert.test.ts +28 -0
- package/packages/mcp-server/tests/unit/errors.test.ts +112 -0
- package/packages/mcp-server/tests/unit/estimate-cost.test.ts +28 -0
- package/packages/mcp-server/tests/unit/polling.test.ts +24 -0
- package/packages/mcp-server/tsconfig.json +9 -0
- package/packages/mcp-server/tsup.config.ts +13 -0
- package/packages/mcp-server/vitest.config.ts +13 -0
- package/plugins/kolmopdf/.claude-plugin/plugin.json +16 -0
- package/plugins/kolmopdf/.mcp.json +11 -0
- package/plugins/kolmopdf/README.md +28 -0
- package/plugins/kolmopdf/commands/balance.md +6 -0
- package/plugins/kolmopdf/commands/convert.md +14 -0
- package/plugins/kolmopdf/commands/parse.md +14 -0
- package/plugins/kolmopdf/commands/translate.md +14 -0
- package/plugins/kolmopdf/skills/kolmopdf/SKILL.md +108 -0
- package/plugins/kolmopdf/skills/kolmopdf/references/chain-recipes.md +35 -0
- package/plugins/kolmopdf/skills/kolmopdf/references/parameter-glossary.md +72 -0
- package/pnpm-workspace.yaml +2 -0
- package/smithery.yaml +21 -0
- package/tsconfig.base.json +21 -0
|
@@ -0,0 +1,1004 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/index.ts
|
|
22
|
+
var index_exports = {};
|
|
23
|
+
__export(index_exports, {
|
|
24
|
+
createServer: () => createServer,
|
|
25
|
+
jsonResult: () => jsonResult
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
29
|
+
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
30
|
+
|
|
31
|
+
// src/client.ts
|
|
32
|
+
var import_node_stream = require("stream");
|
|
33
|
+
var import_promises = require("stream/promises");
|
|
34
|
+
|
|
35
|
+
// src/errors.ts
|
|
36
|
+
var ERROR_SPECS = {
|
|
37
|
+
// --- API codes ---
|
|
38
|
+
invalid_api_key: {
|
|
39
|
+
message: "API key is missing or invalid.",
|
|
40
|
+
remediation: "Create a key at https://www.kolmopdf.com/api-keys (requires Plus/Pro).",
|
|
41
|
+
httpStatus: 401,
|
|
42
|
+
source: "api"
|
|
43
|
+
},
|
|
44
|
+
insufficient_points: {
|
|
45
|
+
message: "Not enough credits.",
|
|
46
|
+
remediation: "Top up at https://www.kolmopdf.com/subscription.",
|
|
47
|
+
httpStatus: 402,
|
|
48
|
+
source: "api"
|
|
49
|
+
},
|
|
50
|
+
points_deduction_failed: {
|
|
51
|
+
message: "Credit deduction failed.",
|
|
52
|
+
remediation: "Retry; if it persists contact support.",
|
|
53
|
+
httpStatus: 402,
|
|
54
|
+
source: "api"
|
|
55
|
+
},
|
|
56
|
+
no_file_found: {
|
|
57
|
+
message: "Request missing file field.",
|
|
58
|
+
remediation: "(internal) MCP server bug, please report.",
|
|
59
|
+
httpStatus: 400,
|
|
60
|
+
source: "api"
|
|
61
|
+
},
|
|
62
|
+
parse_file_too_large: {
|
|
63
|
+
message: "PDF exceeds 300MB.",
|
|
64
|
+
remediation: "Split the PDF locally.",
|
|
65
|
+
httpStatus: 400,
|
|
66
|
+
source: "api"
|
|
67
|
+
},
|
|
68
|
+
parse_page_limit_exceeded: {
|
|
69
|
+
message: "PDF exceeds 800 pages.",
|
|
70
|
+
remediation: "Split the PDF locally.",
|
|
71
|
+
httpStatus: 400,
|
|
72
|
+
source: "api"
|
|
73
|
+
},
|
|
74
|
+
parse_file_not_pdf: {
|
|
75
|
+
message: "File is not a valid PDF.",
|
|
76
|
+
remediation: "Upload a .pdf file.",
|
|
77
|
+
httpStatus: 400,
|
|
78
|
+
source: "api"
|
|
79
|
+
},
|
|
80
|
+
translate_pdf_file_too_large: {
|
|
81
|
+
message: "PDF exceeds 300MB.",
|
|
82
|
+
remediation: "Split the PDF locally.",
|
|
83
|
+
httpStatus: 400,
|
|
84
|
+
source: "api"
|
|
85
|
+
},
|
|
86
|
+
translate_pdf_file_not_pdf: {
|
|
87
|
+
message: "File is not a valid PDF.",
|
|
88
|
+
remediation: "Upload a .pdf file.",
|
|
89
|
+
httpStatus: 400,
|
|
90
|
+
source: "api"
|
|
91
|
+
},
|
|
92
|
+
translate_pdf_page_limit_exceeded: {
|
|
93
|
+
message: "PDF exceeds 800 pages.",
|
|
94
|
+
remediation: "Split the PDF locally.",
|
|
95
|
+
httpStatus: 400,
|
|
96
|
+
source: "api"
|
|
97
|
+
},
|
|
98
|
+
convert_file_too_large: {
|
|
99
|
+
message: "File exceeds 300MB.",
|
|
100
|
+
remediation: "Reduce file size.",
|
|
101
|
+
httpStatus: 400,
|
|
102
|
+
source: "api"
|
|
103
|
+
},
|
|
104
|
+
convert_file_type_unsupported: {
|
|
105
|
+
message: "File must be .md / .markdown / .zip.",
|
|
106
|
+
remediation: "Convert source to markdown first.",
|
|
107
|
+
httpStatus: 400,
|
|
108
|
+
source: "api"
|
|
109
|
+
},
|
|
110
|
+
convert_target_format_unsupported: {
|
|
111
|
+
message: "Target format unsupported.",
|
|
112
|
+
remediation: "Use word/docx/html/pdf/latex/tex.",
|
|
113
|
+
httpStatus: 400,
|
|
114
|
+
source: "api"
|
|
115
|
+
},
|
|
116
|
+
file_upload_failed: {
|
|
117
|
+
message: "Upload to storage failed.",
|
|
118
|
+
remediation: "Check network and retry.",
|
|
119
|
+
httpStatus: 500,
|
|
120
|
+
source: "api"
|
|
121
|
+
},
|
|
122
|
+
task_creation_failed: {
|
|
123
|
+
message: "Task creation failed.",
|
|
124
|
+
remediation: "Retry.",
|
|
125
|
+
httpStatus: 500,
|
|
126
|
+
source: "api"
|
|
127
|
+
},
|
|
128
|
+
parse_error: {
|
|
129
|
+
message: "Parsing failed.",
|
|
130
|
+
remediation: "Retry; if it persists, split and try again.",
|
|
131
|
+
httpStatus: 500,
|
|
132
|
+
source: "api"
|
|
133
|
+
},
|
|
134
|
+
parse_file_invalid: {
|
|
135
|
+
message: "PDF is malformed.",
|
|
136
|
+
remediation: "Re-export the PDF.",
|
|
137
|
+
httpStatus: 500,
|
|
138
|
+
source: "api"
|
|
139
|
+
},
|
|
140
|
+
parse_timeout: {
|
|
141
|
+
message: "Server-side timeout.",
|
|
142
|
+
remediation: "Split into smaller PDFs.",
|
|
143
|
+
httpStatus: 500,
|
|
144
|
+
source: "api"
|
|
145
|
+
},
|
|
146
|
+
api_task_error: {
|
|
147
|
+
message: "Generic task error.",
|
|
148
|
+
remediation: "Retry; if it persists contact support.",
|
|
149
|
+
httpStatus: 500,
|
|
150
|
+
source: "api"
|
|
151
|
+
},
|
|
152
|
+
// --- client codes ---
|
|
153
|
+
client_polling_timeout: {
|
|
154
|
+
message: "Local polling exceeded KOLMOPDF_MAX_POLL_MINUTES.",
|
|
155
|
+
remediation: "Task may still be running. Use kolmopdf_get_task_status with task_id.",
|
|
156
|
+
httpStatus: null,
|
|
157
|
+
source: "client"
|
|
158
|
+
},
|
|
159
|
+
client_network_error: {
|
|
160
|
+
message: "Network error after retries.",
|
|
161
|
+
remediation: "Check network.",
|
|
162
|
+
httpStatus: null,
|
|
163
|
+
source: "client"
|
|
164
|
+
},
|
|
165
|
+
client_local_validation: {
|
|
166
|
+
message: "Local pre-check failed (page count / file size).",
|
|
167
|
+
remediation: "See message for the specific limit that was exceeded.",
|
|
168
|
+
httpStatus: null,
|
|
169
|
+
source: "client"
|
|
170
|
+
},
|
|
171
|
+
client_extract_failed: {
|
|
172
|
+
message: "ZIP extraction failed.",
|
|
173
|
+
remediation: "Check disk permissions on output dir.",
|
|
174
|
+
httpStatus: null,
|
|
175
|
+
source: "client"
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
var UNKNOWN_SPEC = {
|
|
179
|
+
message: "Unknown error.",
|
|
180
|
+
remediation: "Retry; if it persists contact https://www.kolmopdf.com/contact.",
|
|
181
|
+
httpStatus: null,
|
|
182
|
+
source: "client"
|
|
183
|
+
};
|
|
184
|
+
var KolmoPdfError = class extends Error {
|
|
185
|
+
errorCode;
|
|
186
|
+
httpStatus;
|
|
187
|
+
remediation;
|
|
188
|
+
pointsRequired;
|
|
189
|
+
currentPoints;
|
|
190
|
+
source;
|
|
191
|
+
constructor(errorCode, opts = {}) {
|
|
192
|
+
const spec = ERROR_SPECS[errorCode] ?? UNKNOWN_SPEC;
|
|
193
|
+
super(opts.message ?? spec.message);
|
|
194
|
+
this.name = "KolmoPdfError";
|
|
195
|
+
this.errorCode = errorCode;
|
|
196
|
+
this.httpStatus = opts.httpStatus !== void 0 ? opts.httpStatus : spec.httpStatus;
|
|
197
|
+
this.remediation = opts.remediation ?? spec.remediation;
|
|
198
|
+
this.pointsRequired = opts.pointsRequired;
|
|
199
|
+
this.currentPoints = opts.currentPoints;
|
|
200
|
+
this.source = spec.source;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
function toMcpErrorResult(err) {
|
|
204
|
+
const kerr = err instanceof KolmoPdfError ? err : new KolmoPdfError("api_task_error", {
|
|
205
|
+
message: err instanceof Error ? err.message : String(err)
|
|
206
|
+
});
|
|
207
|
+
const payload = {
|
|
208
|
+
error_code: kerr.errorCode,
|
|
209
|
+
message: kerr.message,
|
|
210
|
+
http_status: kerr.httpStatus,
|
|
211
|
+
remediation: kerr.remediation
|
|
212
|
+
};
|
|
213
|
+
if (kerr.pointsRequired !== void 0) payload.points_required = kerr.pointsRequired;
|
|
214
|
+
if (kerr.currentPoints !== void 0) payload.current_points = kerr.currentPoints;
|
|
215
|
+
return {
|
|
216
|
+
isError: true,
|
|
217
|
+
content: [{ type: "text", text: JSON.stringify(payload) }]
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function errorFromApiBody(body, httpStatus) {
|
|
221
|
+
const code = body.error_code ?? "api_task_error";
|
|
222
|
+
return new KolmoPdfError(code, {
|
|
223
|
+
message: body.message,
|
|
224
|
+
httpStatus: httpStatus ?? null,
|
|
225
|
+
pointsRequired: body.points_required,
|
|
226
|
+
currentPoints: body.current_points
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/client.ts
|
|
231
|
+
var KolmoPdfClient = class {
|
|
232
|
+
apiKey;
|
|
233
|
+
baseUrl;
|
|
234
|
+
httpTimeoutMs;
|
|
235
|
+
uploadTimeoutMs;
|
|
236
|
+
constructor(opts) {
|
|
237
|
+
this.apiKey = opts.apiKey;
|
|
238
|
+
this.baseUrl = opts.baseUrl;
|
|
239
|
+
this.httpTimeoutMs = opts.httpTimeoutMs;
|
|
240
|
+
this.uploadTimeoutMs = opts.uploadTimeoutMs;
|
|
241
|
+
}
|
|
242
|
+
get apiBase() {
|
|
243
|
+
return `${this.baseUrl}/api/pdf-to-markdown-proxy`;
|
|
244
|
+
}
|
|
245
|
+
headers() {
|
|
246
|
+
return { "X-API-Key": this.apiKey };
|
|
247
|
+
}
|
|
248
|
+
async jsonRequest(url, init) {
|
|
249
|
+
const res = await fetch(url, init);
|
|
250
|
+
const body = await res.json();
|
|
251
|
+
if (!res.ok || body.success === false) {
|
|
252
|
+
throw errorFromApiBody(
|
|
253
|
+
body,
|
|
254
|
+
res.status
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
return body;
|
|
258
|
+
}
|
|
259
|
+
async buildFileForm(file, filename) {
|
|
260
|
+
const form = new FormData();
|
|
261
|
+
let blob;
|
|
262
|
+
if (Buffer.isBuffer(file)) {
|
|
263
|
+
blob = new Blob([file]);
|
|
264
|
+
} else {
|
|
265
|
+
const chunks = [];
|
|
266
|
+
for await (const chunk of file) {
|
|
267
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
268
|
+
}
|
|
269
|
+
blob = new Blob([Buffer.concat(chunks)]);
|
|
270
|
+
}
|
|
271
|
+
form.append("file", blob, filename);
|
|
272
|
+
return form;
|
|
273
|
+
}
|
|
274
|
+
async parse(file, form, filename) {
|
|
275
|
+
const fd = await this.buildFileForm(file, filename);
|
|
276
|
+
if (form.table_mode) fd.append("table_mode", form.table_mode);
|
|
277
|
+
if (form.formula_format) fd.append("formula_format", form.formula_format);
|
|
278
|
+
if (form.enable_translation !== void 0)
|
|
279
|
+
fd.append("enable_translation", String(form.enable_translation));
|
|
280
|
+
if (form.target_language) fd.append("target_language", form.target_language);
|
|
281
|
+
if (form.output_options?.length) fd.append("output_options", form.output_options.join(","));
|
|
282
|
+
if (form.images_as_url !== void 0) fd.append("images_as_url", String(form.images_as_url));
|
|
283
|
+
if (form.skip_rotation_detection !== void 0)
|
|
284
|
+
fd.append("skip_rotation_detection", String(form.skip_rotation_detection));
|
|
285
|
+
if (form.enable_cross_page_merge !== void 0)
|
|
286
|
+
fd.append("enable_cross_page_merge", String(form.enable_cross_page_merge));
|
|
287
|
+
const body = await this.jsonRequest(`${this.apiBase}/parse`, {
|
|
288
|
+
method: "POST",
|
|
289
|
+
headers: this.headers(),
|
|
290
|
+
body: fd,
|
|
291
|
+
signal: AbortSignal.timeout(this.uploadTimeoutMs)
|
|
292
|
+
});
|
|
293
|
+
return body;
|
|
294
|
+
}
|
|
295
|
+
async translatePdf(file, form, filename) {
|
|
296
|
+
const fd = await this.buildFileForm(file, filename);
|
|
297
|
+
if (form.source_language) fd.append("sourceLanguage", form.source_language);
|
|
298
|
+
if (form.target_language) fd.append("targetLanguage", form.target_language);
|
|
299
|
+
if (form.layout_modes?.length) fd.append("layoutModes", form.layout_modes.join(","));
|
|
300
|
+
if (form.enable_image_translation !== void 0)
|
|
301
|
+
fd.append("enableImageTranslation", String(form.enable_image_translation));
|
|
302
|
+
if (form.enable_table_translation !== void 0)
|
|
303
|
+
fd.append("enableTableTranslation", String(form.enable_table_translation));
|
|
304
|
+
const body = await this.jsonRequest(`${this.apiBase}/translate-pdf`, {
|
|
305
|
+
method: "POST",
|
|
306
|
+
headers: this.headers(),
|
|
307
|
+
body: fd,
|
|
308
|
+
signal: AbortSignal.timeout(this.uploadTimeoutMs)
|
|
309
|
+
});
|
|
310
|
+
return body;
|
|
311
|
+
}
|
|
312
|
+
async convert(file, form, filename) {
|
|
313
|
+
const fd = await this.buildFileForm(file, filename);
|
|
314
|
+
if (form.target_format) fd.append("targetFormat", form.target_format);
|
|
315
|
+
const body = await this.jsonRequest(`${this.apiBase}/convert`, {
|
|
316
|
+
method: "POST",
|
|
317
|
+
headers: this.headers(),
|
|
318
|
+
body: fd,
|
|
319
|
+
signal: AbortSignal.timeout(this.uploadTimeoutMs)
|
|
320
|
+
});
|
|
321
|
+
return body;
|
|
322
|
+
}
|
|
323
|
+
async getStatus(taskId) {
|
|
324
|
+
const body = await this.jsonRequest(`${this.apiBase}/status/${taskId}`, {
|
|
325
|
+
method: "GET",
|
|
326
|
+
headers: this.headers(),
|
|
327
|
+
signal: AbortSignal.timeout(this.httpTimeoutMs)
|
|
328
|
+
});
|
|
329
|
+
return body;
|
|
330
|
+
}
|
|
331
|
+
async download(taskId, dest) {
|
|
332
|
+
const res = await fetch(`${this.apiBase}/download/${taskId}`, {
|
|
333
|
+
method: "GET",
|
|
334
|
+
headers: this.headers(),
|
|
335
|
+
signal: AbortSignal.timeout(this.uploadTimeoutMs)
|
|
336
|
+
});
|
|
337
|
+
if (!res.ok) {
|
|
338
|
+
throw new KolmoPdfError("api_task_error", {
|
|
339
|
+
message: `Download failed with HTTP ${res.status}`,
|
|
340
|
+
httpStatus: res.status
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
const contentType = res.headers.get("content-type");
|
|
344
|
+
const isZip = contentType?.includes("zip") || contentType?.includes("octet-stream") || false;
|
|
345
|
+
const body = res.body;
|
|
346
|
+
if (!body) {
|
|
347
|
+
throw new KolmoPdfError("api_task_error", { message: "Empty download response body" });
|
|
348
|
+
}
|
|
349
|
+
const reader = body.getReader();
|
|
350
|
+
let bytesWritten = 0;
|
|
351
|
+
async function* generate() {
|
|
352
|
+
while (true) {
|
|
353
|
+
const { done, value } = await reader.read();
|
|
354
|
+
if (done) break;
|
|
355
|
+
bytesWritten += value.byteLength;
|
|
356
|
+
yield Buffer.from(value);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const readable = import_node_stream.Readable.from(generate());
|
|
360
|
+
await (0, import_promises.pipeline)(readable, dest);
|
|
361
|
+
return { contentType, isZip, bytesWritten };
|
|
362
|
+
}
|
|
363
|
+
async getBalance() {
|
|
364
|
+
const body = await this.jsonRequest(`${this.apiBase}/balance`, {
|
|
365
|
+
method: "GET",
|
|
366
|
+
headers: this.headers(),
|
|
367
|
+
signal: AbortSignal.timeout(this.httpTimeoutMs)
|
|
368
|
+
});
|
|
369
|
+
return body;
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
// src/config.ts
|
|
374
|
+
var DEFAULTS = {
|
|
375
|
+
baseUrl: "https://www.kolmopdf.com",
|
|
376
|
+
outputDir: "./kolmopdf-output",
|
|
377
|
+
pollIntervalMs: 2e3,
|
|
378
|
+
maxPollMinutes: 30,
|
|
379
|
+
httpTimeoutMs: 6e4,
|
|
380
|
+
uploadTimeoutMs: 6e5
|
|
381
|
+
};
|
|
382
|
+
function intFromEnv(value, fallback) {
|
|
383
|
+
if (value === void 0 || value.trim() === "") return fallback;
|
|
384
|
+
const parsed = Number.parseInt(value, 10);
|
|
385
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
386
|
+
}
|
|
387
|
+
function trimTrailingSlash(url) {
|
|
388
|
+
return url.replace(/\/+$/, "");
|
|
389
|
+
}
|
|
390
|
+
function loadConfig(env = process.env) {
|
|
391
|
+
const apiKeyRaw = env.KOLMOPDF_API_KEY?.trim();
|
|
392
|
+
return {
|
|
393
|
+
apiKey: apiKeyRaw && apiKeyRaw.length > 0 ? apiKeyRaw : void 0,
|
|
394
|
+
baseUrl: trimTrailingSlash(env.KOLMOPDF_BASE_URL?.trim() || DEFAULTS.baseUrl),
|
|
395
|
+
outputDir: env.KOLMOPDF_OUTPUT_DIR?.trim() || DEFAULTS.outputDir,
|
|
396
|
+
pollIntervalMs: intFromEnv(env.KOLMOPDF_POLL_INTERVAL_MS, DEFAULTS.pollIntervalMs),
|
|
397
|
+
maxPollMinutes: intFromEnv(env.KOLMOPDF_MAX_POLL_MINUTES, DEFAULTS.maxPollMinutes),
|
|
398
|
+
httpTimeoutMs: intFromEnv(env.KOLMOPDF_HTTP_TIMEOUT_MS, DEFAULTS.httpTimeoutMs),
|
|
399
|
+
uploadTimeoutMs: intFromEnv(env.KOLMOPDF_UPLOAD_TIMEOUT_MS, DEFAULTS.uploadTimeoutMs)
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
function maskApiKey(apiKey) {
|
|
403
|
+
if (apiKey.length <= 10) return "***";
|
|
404
|
+
return `${apiKey.slice(0, 6)}***${apiKey.slice(-4)}`;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// src/context.ts
|
|
408
|
+
function jsonResult(data) {
|
|
409
|
+
return {
|
|
410
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
411
|
+
structuredContent: data
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// src/tools/check-balance.ts
|
|
416
|
+
var import_zod = require("zod");
|
|
417
|
+
var checkBalanceName = "kolmopdf_check_balance";
|
|
418
|
+
var checkBalanceDescription = "Show the current KolmoPDF credit balance for the configured API key.";
|
|
419
|
+
var checkBalanceInputSchema = import_zod.z.object({});
|
|
420
|
+
async function checkBalanceHandler(_args, ctx) {
|
|
421
|
+
const client = ctx.getClient();
|
|
422
|
+
const balance = await client.getBalance();
|
|
423
|
+
const output = {
|
|
424
|
+
points: balance.points,
|
|
425
|
+
api_key_masked: maskApiKey(ctx.config.apiKey || "")
|
|
426
|
+
};
|
|
427
|
+
return jsonResult(output);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// src/tools/convert.ts
|
|
431
|
+
var import_node_fs = require("fs");
|
|
432
|
+
var import_promises3 = require("fs/promises");
|
|
433
|
+
var import_node_path = require("path");
|
|
434
|
+
var import_zod2 = require("zod");
|
|
435
|
+
|
|
436
|
+
// src/pages.ts
|
|
437
|
+
var import_promises2 = require("fs/promises");
|
|
438
|
+
var import_pdf_lib = require("pdf-lib");
|
|
439
|
+
var MAX_PAGES = 800;
|
|
440
|
+
var MAX_FILE_BYTES = 300 * 1024 * 1024;
|
|
441
|
+
async function readPageCount(filePath) {
|
|
442
|
+
const data = await (0, import_promises2.readFile)(filePath);
|
|
443
|
+
const doc = await import_pdf_lib.PDFDocument.load(data, { ignoreEncryption: true });
|
|
444
|
+
return doc.getPageCount();
|
|
445
|
+
}
|
|
446
|
+
async function readFileSize(filePath) {
|
|
447
|
+
const s = await (0, import_promises2.stat)(filePath);
|
|
448
|
+
return s.size;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// src/progress.ts
|
|
452
|
+
function humanizeStatus(status, aheadTasks) {
|
|
453
|
+
if (status === "waiting" && typeof aheadTasks === "number") {
|
|
454
|
+
return `[waiting] ${aheadTasks} tasks ahead`;
|
|
455
|
+
}
|
|
456
|
+
return `[${status}]`;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// src/polling.ts
|
|
460
|
+
var TERMINAL_OK = "completed";
|
|
461
|
+
var TERMINAL_FAIL = "failed";
|
|
462
|
+
var RETRY_POLICY = {
|
|
463
|
+
maxAttempts: 3,
|
|
464
|
+
baseDelayMs: 1e3,
|
|
465
|
+
factor: 2
|
|
466
|
+
};
|
|
467
|
+
function backoffDelayMs(attempt) {
|
|
468
|
+
return RETRY_POLICY.baseDelayMs * RETRY_POLICY.factor ** (attempt - 1);
|
|
469
|
+
}
|
|
470
|
+
function isRetryable(err) {
|
|
471
|
+
const transientCodes = ["ECONNRESET", "ETIMEDOUT", "ECONNREFUSED", "EAI_AGAIN"];
|
|
472
|
+
if (err.code && transientCodes.includes(err.code)) return true;
|
|
473
|
+
if (typeof err.httpStatus === "number" && err.httpStatus >= 500) return true;
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
function sleep(ms) {
|
|
477
|
+
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
478
|
+
}
|
|
479
|
+
async function fetchStatusWithRetry(client, taskId) {
|
|
480
|
+
for (let attempt = 1; attempt <= RETRY_POLICY.maxAttempts; attempt++) {
|
|
481
|
+
try {
|
|
482
|
+
return await client.getStatus(taskId);
|
|
483
|
+
} catch (err) {
|
|
484
|
+
const retryable = err instanceof KolmoPdfError ? isRetryable(err) : isRetryable({ code: err.code });
|
|
485
|
+
if (!retryable || attempt === RETRY_POLICY.maxAttempts) throw err;
|
|
486
|
+
await sleep(backoffDelayMs(attempt));
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
throw new KolmoPdfError("client_network_error");
|
|
490
|
+
}
|
|
491
|
+
async function pollUntilComplete(ctx) {
|
|
492
|
+
const { client, taskId, options, progress } = ctx;
|
|
493
|
+
const deadline = Date.now() + options.maxPollMinutes * 6e4;
|
|
494
|
+
while (true) {
|
|
495
|
+
if (Date.now() > deadline) {
|
|
496
|
+
throw new KolmoPdfError("client_polling_timeout");
|
|
497
|
+
}
|
|
498
|
+
const result = await fetchStatusWithRetry(client, taskId);
|
|
499
|
+
if (result.status === TERMINAL_OK) {
|
|
500
|
+
await progress?.report(`[completed] Task ${taskId} done`);
|
|
501
|
+
return result;
|
|
502
|
+
}
|
|
503
|
+
if (result.status === TERMINAL_FAIL) {
|
|
504
|
+
throw new KolmoPdfError(result.error_code || "api_task_error", {
|
|
505
|
+
message: result.message || "Task failed"
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
const aheadTasks = result.queue_info?.ahead_tasks;
|
|
509
|
+
await progress?.report(humanizeStatus(result.status, aheadTasks));
|
|
510
|
+
await sleep(options.pollIntervalMs);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// src/tools/convert.ts
|
|
515
|
+
var convertName = "kolmopdf_convert_markdown";
|
|
516
|
+
var convertDescription = "Convert a Markdown file (or a ZIP of markdown + images) to DOCX, HTML, PDF, or LaTeX via KolmoPDF.";
|
|
517
|
+
var convertInputSchema = import_zod2.z.object({
|
|
518
|
+
file_path: import_zod2.z.string().describe("Path to a .md/.markdown file or .zip containing markdown + images."),
|
|
519
|
+
target_format: import_zod2.z.enum(["word", "docx", "html", "pdf", "latex", "tex"]).optional().default("word"),
|
|
520
|
+
output_subdir: import_zod2.z.string().optional()
|
|
521
|
+
});
|
|
522
|
+
function formatToExtension(targetFormat) {
|
|
523
|
+
switch (targetFormat) {
|
|
524
|
+
case "word":
|
|
525
|
+
case "docx":
|
|
526
|
+
return ".docx";
|
|
527
|
+
case "html":
|
|
528
|
+
return ".html";
|
|
529
|
+
case "pdf":
|
|
530
|
+
return ".pdf";
|
|
531
|
+
case "latex":
|
|
532
|
+
case "tex":
|
|
533
|
+
return ".tex";
|
|
534
|
+
default:
|
|
535
|
+
return ".out";
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
function normalizeFormat(targetFormat) {
|
|
539
|
+
switch (targetFormat) {
|
|
540
|
+
case "word":
|
|
541
|
+
case "docx":
|
|
542
|
+
return "docx";
|
|
543
|
+
case "latex":
|
|
544
|
+
case "tex":
|
|
545
|
+
return "tex";
|
|
546
|
+
default:
|
|
547
|
+
return targetFormat;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
async function convertHandler(args, ctx) {
|
|
551
|
+
const client = ctx.getClient();
|
|
552
|
+
const filePath = (0, import_node_path.resolve)(args.file_path);
|
|
553
|
+
const filename = (0, import_node_path.basename)(filePath);
|
|
554
|
+
const fileSize = await readFileSize(filePath);
|
|
555
|
+
if (fileSize > MAX_FILE_BYTES) {
|
|
556
|
+
throw new KolmoPdfError("convert_file_too_large");
|
|
557
|
+
}
|
|
558
|
+
const ext = filePath.toLowerCase();
|
|
559
|
+
if (!ext.endsWith(".md") && !ext.endsWith(".markdown") && !ext.endsWith(".zip")) {
|
|
560
|
+
throw new KolmoPdfError("convert_file_type_unsupported");
|
|
561
|
+
}
|
|
562
|
+
await ctx.progress?.report("[uploading] Sending file for conversion...");
|
|
563
|
+
const fileBuffer = await (0, import_promises3.readFile)(filePath);
|
|
564
|
+
const submitResult = await client.convert(
|
|
565
|
+
fileBuffer,
|
|
566
|
+
{
|
|
567
|
+
target_format: args.target_format
|
|
568
|
+
},
|
|
569
|
+
filename
|
|
570
|
+
);
|
|
571
|
+
const taskId = submitResult.task_id;
|
|
572
|
+
await ctx.progress?.report(`[submitted] Task ${taskId} created`);
|
|
573
|
+
await pollUntilComplete({
|
|
574
|
+
client,
|
|
575
|
+
taskId,
|
|
576
|
+
options: {
|
|
577
|
+
pollIntervalMs: ctx.config.pollIntervalMs,
|
|
578
|
+
maxPollMinutes: ctx.config.maxPollMinutes
|
|
579
|
+
},
|
|
580
|
+
progress: ctx.progress
|
|
581
|
+
});
|
|
582
|
+
await ctx.progress?.report("[downloading] Fetching converted file...");
|
|
583
|
+
const subdir = args.output_subdir || taskId;
|
|
584
|
+
const outputRoot = (0, import_node_path.resolve)(ctx.config.outputDir, subdir);
|
|
585
|
+
(0, import_node_fs.mkdirSync)(outputRoot, { recursive: true });
|
|
586
|
+
const outExt = formatToExtension(args.target_format);
|
|
587
|
+
const outputPath = (0, import_node_path.join)(outputRoot, `result${outExt}`);
|
|
588
|
+
const ws = (0, import_node_fs.createWriteStream)(outputPath);
|
|
589
|
+
await client.download(taskId, ws);
|
|
590
|
+
const output = {
|
|
591
|
+
task_id: taskId,
|
|
592
|
+
points_deducted: submitResult.points_deducted,
|
|
593
|
+
remaining_points: submitResult.remaining_points,
|
|
594
|
+
output: {
|
|
595
|
+
output_path: outputPath,
|
|
596
|
+
target_format: normalizeFormat(args.target_format)
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
return jsonResult(output);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// src/tools/estimate-cost.ts
|
|
603
|
+
var import_node_path2 = require("path");
|
|
604
|
+
var import_zod3 = require("zod");
|
|
605
|
+
var estimateCostName = "kolmopdf_estimate_cost";
|
|
606
|
+
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.";
|
|
607
|
+
var estimateCostInputSchema = import_zod3.z.object({
|
|
608
|
+
file_path: import_zod3.z.string(),
|
|
609
|
+
operation: import_zod3.z.enum(["parse", "parse_translate", "translate", "convert"]),
|
|
610
|
+
options: import_zod3.z.object({
|
|
611
|
+
images_as_url: import_zod3.z.boolean().optional()
|
|
612
|
+
}).optional()
|
|
613
|
+
});
|
|
614
|
+
function estimateCredits(operation, pages) {
|
|
615
|
+
switch (operation) {
|
|
616
|
+
case "parse":
|
|
617
|
+
return pages * 2;
|
|
618
|
+
case "parse_translate":
|
|
619
|
+
return pages * 3;
|
|
620
|
+
case "translate":
|
|
621
|
+
return pages * 2;
|
|
622
|
+
case "convert":
|
|
623
|
+
return 1;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
function buildRecommendation(shortfall) {
|
|
627
|
+
return shortfall > 0 ? `Need top-up at https://www.kolmopdf.com/subscription (short by ${shortfall} credits).` : "Sufficient";
|
|
628
|
+
}
|
|
629
|
+
async function estimateCostHandler(args, ctx) {
|
|
630
|
+
const client = ctx.getClient();
|
|
631
|
+
let pages = null;
|
|
632
|
+
if (args.operation !== "convert") {
|
|
633
|
+
const filePath = (0, import_node_path2.resolve)(args.file_path);
|
|
634
|
+
pages = await readPageCount(filePath);
|
|
635
|
+
}
|
|
636
|
+
const estimatedCredits = estimateCredits(args.operation, pages ?? 1);
|
|
637
|
+
const balance = await client.getBalance();
|
|
638
|
+
const currentBalance = balance.points;
|
|
639
|
+
const shortfall = Math.max(0, estimatedCredits - currentBalance);
|
|
640
|
+
const output = {
|
|
641
|
+
pages,
|
|
642
|
+
estimated_credits: estimatedCredits,
|
|
643
|
+
current_balance: currentBalance,
|
|
644
|
+
sufficient: shortfall === 0,
|
|
645
|
+
shortfall,
|
|
646
|
+
recommendation: buildRecommendation(shortfall)
|
|
647
|
+
};
|
|
648
|
+
return jsonResult(output);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// src/tools/get-task-status.ts
|
|
652
|
+
var import_zod4 = require("zod");
|
|
653
|
+
var getTaskStatusName = "kolmopdf_get_task_status";
|
|
654
|
+
var getTaskStatusDescription = "Advanced/debug tool. Returns raw status for a KolmoPDF task. Use only when explicitly asked to inspect a task by ID, or when troubleshooting a stuck task.";
|
|
655
|
+
var getTaskStatusInputSchema = import_zod4.z.object({
|
|
656
|
+
task_id: import_zod4.z.string()
|
|
657
|
+
});
|
|
658
|
+
async function getTaskStatusHandler(args, ctx) {
|
|
659
|
+
const client = ctx.getClient();
|
|
660
|
+
const status = await client.getStatus(args.task_id);
|
|
661
|
+
return jsonResult(status);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// src/tools/parse-pdf.ts
|
|
665
|
+
var import_node_fs3 = require("fs");
|
|
666
|
+
var import_promises5 = require("fs/promises");
|
|
667
|
+
var import_node_path4 = require("path");
|
|
668
|
+
var import_zod5 = require("zod");
|
|
669
|
+
|
|
670
|
+
// src/extract.ts
|
|
671
|
+
var import_node_fs2 = require("fs");
|
|
672
|
+
var import_node_path3 = require("path");
|
|
673
|
+
var import_promises4 = require("stream/promises");
|
|
674
|
+
var import_yauzl = require("yauzl");
|
|
675
|
+
async function extractZip(zipPath, destDir) {
|
|
676
|
+
(0, import_node_fs2.mkdirSync)(destDir, { recursive: true });
|
|
677
|
+
const zipFile = await openZip(zipPath);
|
|
678
|
+
const files = [];
|
|
679
|
+
let markdownPath = null;
|
|
680
|
+
let imagesDir = null;
|
|
681
|
+
for await (const entry of iterEntries(zipFile)) {
|
|
682
|
+
const entryPath = (0, import_node_path3.join)(destDir, entry.fileName);
|
|
683
|
+
if (entry.fileName.endsWith("/")) {
|
|
684
|
+
(0, import_node_fs2.mkdirSync)(entryPath, { recursive: true });
|
|
685
|
+
if (entry.fileName.includes("images")) {
|
|
686
|
+
imagesDir = entryPath;
|
|
687
|
+
}
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
(0, import_node_fs2.mkdirSync)((0, import_node_path3.dirname)(entryPath), { recursive: true });
|
|
691
|
+
const readStream = await openReadStream(zipFile, entry);
|
|
692
|
+
const writeStream = (0, import_node_fs2.createWriteStream)(entryPath);
|
|
693
|
+
await (0, import_promises4.pipeline)(readStream, writeStream);
|
|
694
|
+
files.push(entryPath);
|
|
695
|
+
if (!markdownPath && /\.md$/i.test(entry.fileName)) {
|
|
696
|
+
markdownPath = entryPath;
|
|
697
|
+
}
|
|
698
|
+
if (!imagesDir && /images\//i.test(entry.fileName)) {
|
|
699
|
+
const prefix = entry.fileName.split("images/")[0] ?? "";
|
|
700
|
+
imagesDir = (0, import_node_path3.join)(destDir, prefix, "images");
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
return { markdownPath, imagesDir, outputRoot: destDir, files };
|
|
704
|
+
}
|
|
705
|
+
function openZip(path) {
|
|
706
|
+
return new Promise((resolve5, reject) => {
|
|
707
|
+
(0, import_yauzl.open)(path, { lazyEntries: true }, (err, zf) => {
|
|
708
|
+
if (err || !zf) return reject(err ?? new Error("Failed to open zip"));
|
|
709
|
+
resolve5(zf);
|
|
710
|
+
});
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
async function* iterEntries(zipFile) {
|
|
714
|
+
let resolve5 = null;
|
|
715
|
+
const queue = [];
|
|
716
|
+
zipFile.on("entry", (entry) => {
|
|
717
|
+
if (resolve5) {
|
|
718
|
+
const r = resolve5;
|
|
719
|
+
resolve5 = null;
|
|
720
|
+
r(entry);
|
|
721
|
+
} else {
|
|
722
|
+
queue.push(entry);
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
zipFile.on("end", () => {
|
|
726
|
+
if (resolve5) {
|
|
727
|
+
const r = resolve5;
|
|
728
|
+
resolve5 = null;
|
|
729
|
+
r(null);
|
|
730
|
+
} else {
|
|
731
|
+
queue.push(null);
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
zipFile.readEntry();
|
|
735
|
+
while (true) {
|
|
736
|
+
const entry = queue.length > 0 ? queue.shift() : await new Promise((r) => {
|
|
737
|
+
resolve5 = r;
|
|
738
|
+
});
|
|
739
|
+
if (entry === null) break;
|
|
740
|
+
yield entry;
|
|
741
|
+
zipFile.readEntry();
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
function openReadStream(zipFile, entry) {
|
|
745
|
+
return new Promise((resolve5, reject) => {
|
|
746
|
+
zipFile.openReadStream(entry, (err, stream) => {
|
|
747
|
+
if (err || !stream) return reject(err ?? new Error("Failed to open entry stream"));
|
|
748
|
+
resolve5(stream);
|
|
749
|
+
});
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// src/tools/parse-pdf.ts
|
|
754
|
+
var parsePdfName = "kolmopdf_parse_pdf";
|
|
755
|
+
var parsePdfDescription = "Parse a local PDF into Markdown via KolmoPDF. Handles formulas, tables, multi-column layouts, and code blocks. Optionally translates while parsing.";
|
|
756
|
+
var parsePdfInputSchema = import_zod5.z.object({
|
|
757
|
+
file_path: import_zod5.z.string().describe("Absolute or cwd-relative path to a local PDF file."),
|
|
758
|
+
table_mode: import_zod5.z.enum(["markdown", "image"]).optional(),
|
|
759
|
+
formula_format: import_zod5.z.enum(["dollar", "bracket"]).optional(),
|
|
760
|
+
enable_translation: import_zod5.z.boolean().optional(),
|
|
761
|
+
target_language: import_zod5.z.enum(["zh", "en", "ja", "ko", "fr", "de", "es", "ru"]).optional(),
|
|
762
|
+
output_options: import_zod5.z.array(import_zod5.z.enum(["original", "translated", "bilingual"])).optional(),
|
|
763
|
+
images_as_url: import_zod5.z.boolean().optional(),
|
|
764
|
+
skip_rotation_detection: import_zod5.z.boolean().optional(),
|
|
765
|
+
enable_cross_page_merge: import_zod5.z.boolean().optional(),
|
|
766
|
+
output_subdir: import_zod5.z.string().optional().describe("Subdirectory name under KOLMOPDF_OUTPUT_DIR. Defaults to <task_id>.")
|
|
767
|
+
});
|
|
768
|
+
async function parsePdfHandler(args, ctx) {
|
|
769
|
+
const client = ctx.getClient();
|
|
770
|
+
const filePath = (0, import_node_path4.resolve)(args.file_path);
|
|
771
|
+
const filename = (0, import_node_path4.basename)(filePath);
|
|
772
|
+
const fileSize = await readFileSize(filePath);
|
|
773
|
+
if (fileSize > MAX_FILE_BYTES) {
|
|
774
|
+
throw new KolmoPdfError("parse_file_too_large");
|
|
775
|
+
}
|
|
776
|
+
const pageCount = await readPageCount(filePath);
|
|
777
|
+
if (pageCount > MAX_PAGES) {
|
|
778
|
+
throw new KolmoPdfError("parse_page_limit_exceeded");
|
|
779
|
+
}
|
|
780
|
+
await ctx.progress?.report("[uploading] Sending PDF to KolmoPDF...");
|
|
781
|
+
const fileBuffer = await (0, import_promises5.readFile)(filePath);
|
|
782
|
+
const submitResult = await client.parse(
|
|
783
|
+
fileBuffer,
|
|
784
|
+
{
|
|
785
|
+
table_mode: args.table_mode,
|
|
786
|
+
formula_format: args.formula_format,
|
|
787
|
+
enable_translation: args.enable_translation,
|
|
788
|
+
target_language: args.target_language,
|
|
789
|
+
output_options: args.output_options,
|
|
790
|
+
images_as_url: args.images_as_url,
|
|
791
|
+
skip_rotation_detection: args.skip_rotation_detection,
|
|
792
|
+
enable_cross_page_merge: args.enable_cross_page_merge
|
|
793
|
+
},
|
|
794
|
+
filename
|
|
795
|
+
);
|
|
796
|
+
const taskId = submitResult.task_id;
|
|
797
|
+
await ctx.progress?.report(`[submitted] Task ${taskId} created`);
|
|
798
|
+
await pollUntilComplete({
|
|
799
|
+
client,
|
|
800
|
+
taskId,
|
|
801
|
+
options: {
|
|
802
|
+
pollIntervalMs: ctx.config.pollIntervalMs,
|
|
803
|
+
maxPollMinutes: ctx.config.maxPollMinutes
|
|
804
|
+
},
|
|
805
|
+
progress: ctx.progress
|
|
806
|
+
});
|
|
807
|
+
await ctx.progress?.report("[downloading] Fetching result...");
|
|
808
|
+
const subdir = args.output_subdir || taskId;
|
|
809
|
+
const outputRoot = (0, import_node_path4.resolve)(ctx.config.outputDir, subdir);
|
|
810
|
+
(0, import_node_fs3.mkdirSync)(outputRoot, { recursive: true });
|
|
811
|
+
const isUrlMode = args.images_as_url === true;
|
|
812
|
+
let markdownPath;
|
|
813
|
+
let imagesDir = null;
|
|
814
|
+
let outputType;
|
|
815
|
+
if (isUrlMode) {
|
|
816
|
+
markdownPath = (0, import_node_path4.join)(outputRoot, "result.md");
|
|
817
|
+
const ws = (0, import_node_fs3.createWriteStream)(markdownPath);
|
|
818
|
+
await client.download(taskId, ws);
|
|
819
|
+
outputType = "markdown_file";
|
|
820
|
+
} else {
|
|
821
|
+
const zipPath = (0, import_node_path4.join)(outputRoot, "result.zip");
|
|
822
|
+
const ws = (0, import_node_fs3.createWriteStream)(zipPath);
|
|
823
|
+
await client.download(taskId, ws);
|
|
824
|
+
const extracted = await extractZip(zipPath, outputRoot);
|
|
825
|
+
markdownPath = extracted.markdownPath || (0, import_node_path4.join)(outputRoot, "result.md");
|
|
826
|
+
imagesDir = extracted.imagesDir;
|
|
827
|
+
outputType = "zip_extracted";
|
|
828
|
+
}
|
|
829
|
+
const mdContent = await (0, import_promises5.readFile)(markdownPath, "utf-8").catch(() => "");
|
|
830
|
+
const preview = mdContent.slice(0, 500);
|
|
831
|
+
const ptsPerPage = args.enable_translation ? 3 : 2;
|
|
832
|
+
const pagesParsed = Math.round(submitResult.points_deducted / ptsPerPage);
|
|
833
|
+
const output = {
|
|
834
|
+
task_id: taskId,
|
|
835
|
+
pages_parsed: pagesParsed,
|
|
836
|
+
points_deducted: submitResult.points_deducted,
|
|
837
|
+
remaining_points: submitResult.remaining_points,
|
|
838
|
+
output: {
|
|
839
|
+
type: outputType,
|
|
840
|
+
markdown_path: markdownPath,
|
|
841
|
+
images_dir: imagesDir,
|
|
842
|
+
output_root: outputRoot
|
|
843
|
+
},
|
|
844
|
+
preview
|
|
845
|
+
};
|
|
846
|
+
return jsonResult(output);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// src/tools/translate-pdf.ts
|
|
850
|
+
var import_node_fs4 = require("fs");
|
|
851
|
+
var import_promises6 = require("fs/promises");
|
|
852
|
+
var import_node_path5 = require("path");
|
|
853
|
+
var import_zod6 = require("zod");
|
|
854
|
+
var translatePdfName = "kolmopdf_translate_pdf";
|
|
855
|
+
var translatePdfDescription = "Translate a PDF while preserving its original layout via KolmoPDF. Produces a translated PDF (optionally side-by-side bilingual).";
|
|
856
|
+
var translatePdfInputSchema = import_zod6.z.object({
|
|
857
|
+
file_path: import_zod6.z.string(),
|
|
858
|
+
source_language: import_zod6.z.string().optional().default("en"),
|
|
859
|
+
target_language: import_zod6.z.string().optional().default("zh"),
|
|
860
|
+
layout_modes: import_zod6.z.array(import_zod6.z.enum(["translated_only", "side_by_side"])).optional().default(["translated_only"]),
|
|
861
|
+
enable_image_translation: import_zod6.z.boolean().optional().default(false),
|
|
862
|
+
enable_table_translation: import_zod6.z.boolean().optional().default(false),
|
|
863
|
+
output_subdir: import_zod6.z.string().optional()
|
|
864
|
+
});
|
|
865
|
+
async function translatePdfHandler(args, ctx) {
|
|
866
|
+
const client = ctx.getClient();
|
|
867
|
+
const filePath = (0, import_node_path5.resolve)(args.file_path);
|
|
868
|
+
const filename = (0, import_node_path5.basename)(filePath);
|
|
869
|
+
const fileSize = await readFileSize(filePath);
|
|
870
|
+
if (fileSize > MAX_FILE_BYTES) {
|
|
871
|
+
throw new KolmoPdfError("translate_pdf_file_too_large");
|
|
872
|
+
}
|
|
873
|
+
const pageCount = await readPageCount(filePath);
|
|
874
|
+
if (pageCount > MAX_PAGES) {
|
|
875
|
+
throw new KolmoPdfError("translate_pdf_page_limit_exceeded");
|
|
876
|
+
}
|
|
877
|
+
await ctx.progress?.report("[uploading] Sending PDF for translation...");
|
|
878
|
+
const fileBuffer = await (0, import_promises6.readFile)(filePath);
|
|
879
|
+
const submitResult = await client.translatePdf(
|
|
880
|
+
fileBuffer,
|
|
881
|
+
{
|
|
882
|
+
source_language: args.source_language,
|
|
883
|
+
target_language: args.target_language,
|
|
884
|
+
layout_modes: args.layout_modes,
|
|
885
|
+
enable_image_translation: args.enable_image_translation,
|
|
886
|
+
enable_table_translation: args.enable_table_translation
|
|
887
|
+
},
|
|
888
|
+
filename
|
|
889
|
+
);
|
|
890
|
+
const taskId = submitResult.task_id;
|
|
891
|
+
await ctx.progress?.report(`[submitted] Task ${taskId} created`);
|
|
892
|
+
await pollUntilComplete({
|
|
893
|
+
client,
|
|
894
|
+
taskId,
|
|
895
|
+
options: {
|
|
896
|
+
pollIntervalMs: ctx.config.pollIntervalMs,
|
|
897
|
+
maxPollMinutes: ctx.config.maxPollMinutes
|
|
898
|
+
},
|
|
899
|
+
progress: ctx.progress
|
|
900
|
+
});
|
|
901
|
+
await ctx.progress?.report("[downloading] Fetching translated PDF...");
|
|
902
|
+
const subdir = args.output_subdir || taskId;
|
|
903
|
+
const outputRoot = (0, import_node_path5.resolve)(ctx.config.outputDir, subdir);
|
|
904
|
+
(0, import_node_fs4.mkdirSync)(outputRoot, { recursive: true });
|
|
905
|
+
const pdfPath = (0, import_node_path5.join)(outputRoot, "translated.pdf");
|
|
906
|
+
const ws = (0, import_node_fs4.createWriteStream)(pdfPath);
|
|
907
|
+
await client.download(taskId, ws);
|
|
908
|
+
const pagesTranslated = Math.round(submitResult.points_deducted / 2);
|
|
909
|
+
const output = {
|
|
910
|
+
task_id: taskId,
|
|
911
|
+
pages_translated: pagesTranslated,
|
|
912
|
+
points_deducted: submitResult.points_deducted,
|
|
913
|
+
remaining_points: submitResult.remaining_points,
|
|
914
|
+
output: {
|
|
915
|
+
translated_pdf_path: pdfPath
|
|
916
|
+
}
|
|
917
|
+
};
|
|
918
|
+
return jsonResult(output);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// src/index.ts
|
|
922
|
+
var VERSION = "1.0.0";
|
|
923
|
+
function buildContext() {
|
|
924
|
+
const config = loadConfig();
|
|
925
|
+
return {
|
|
926
|
+
config,
|
|
927
|
+
getClient() {
|
|
928
|
+
if (!config.apiKey) {
|
|
929
|
+
throw new KolmoPdfError("invalid_api_key");
|
|
930
|
+
}
|
|
931
|
+
return new KolmoPdfClient({
|
|
932
|
+
apiKey: config.apiKey,
|
|
933
|
+
baseUrl: config.baseUrl,
|
|
934
|
+
httpTimeoutMs: config.httpTimeoutMs,
|
|
935
|
+
uploadTimeoutMs: config.uploadTimeoutMs
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
function guard(handler) {
|
|
941
|
+
return async (args) => {
|
|
942
|
+
try {
|
|
943
|
+
return await handler(args, buildContext());
|
|
944
|
+
} catch (err) {
|
|
945
|
+
return toMcpErrorResult(err);
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
function createServer() {
|
|
950
|
+
const server = new import_mcp.McpServer({ name: "kolmopdf", version: VERSION });
|
|
951
|
+
server.registerTool(
|
|
952
|
+
parsePdfName,
|
|
953
|
+
{ description: parsePdfDescription, inputSchema: parsePdfInputSchema.shape },
|
|
954
|
+
guard(parsePdfHandler)
|
|
955
|
+
);
|
|
956
|
+
server.registerTool(
|
|
957
|
+
translatePdfName,
|
|
958
|
+
{ description: translatePdfDescription, inputSchema: translatePdfInputSchema.shape },
|
|
959
|
+
guard(translatePdfHandler)
|
|
960
|
+
);
|
|
961
|
+
server.registerTool(
|
|
962
|
+
convertName,
|
|
963
|
+
{ description: convertDescription, inputSchema: convertInputSchema.shape },
|
|
964
|
+
guard(convertHandler)
|
|
965
|
+
);
|
|
966
|
+
server.registerTool(
|
|
967
|
+
estimateCostName,
|
|
968
|
+
{ description: estimateCostDescription, inputSchema: estimateCostInputSchema.shape },
|
|
969
|
+
guard(estimateCostHandler)
|
|
970
|
+
);
|
|
971
|
+
server.registerTool(
|
|
972
|
+
checkBalanceName,
|
|
973
|
+
{ description: checkBalanceDescription, inputSchema: checkBalanceInputSchema.shape },
|
|
974
|
+
guard(checkBalanceHandler)
|
|
975
|
+
);
|
|
976
|
+
server.registerTool(
|
|
977
|
+
getTaskStatusName,
|
|
978
|
+
{ description: getTaskStatusDescription, inputSchema: getTaskStatusInputSchema.shape },
|
|
979
|
+
guard(getTaskStatusHandler)
|
|
980
|
+
);
|
|
981
|
+
return server;
|
|
982
|
+
}
|
|
983
|
+
async function main() {
|
|
984
|
+
if (process.argv.includes("--version") || process.argv.includes("-v")) {
|
|
985
|
+
process.stdout.write(`${VERSION}
|
|
986
|
+
`);
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
const server = createServer();
|
|
990
|
+
const transport = new import_stdio.StdioServerTransport();
|
|
991
|
+
await server.connect(transport);
|
|
992
|
+
}
|
|
993
|
+
main().catch((err) => {
|
|
994
|
+
const detail = err instanceof Error ? err.stack : String(err);
|
|
995
|
+
process.stderr.write(`[kolmopdf-mcp] fatal: ${detail}
|
|
996
|
+
`);
|
|
997
|
+
process.exit(1);
|
|
998
|
+
});
|
|
999
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1000
|
+
0 && (module.exports = {
|
|
1001
|
+
createServer,
|
|
1002
|
+
jsonResult
|
|
1003
|
+
});
|
|
1004
|
+
//# sourceMappingURL=index.cjs.map
|