@kolmopdf/mcp-server 1.0.0 → 1.0.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/LICENSE +21 -21
- package/README.md +37 -63
- package/dist/index.cjs.map +1 -0
- package/{packages/mcp-server/dist → dist}/index.js +0 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -30
- package/.claude-plugin/marketplace.json +0 -25
- package/.github/ISSUE_TEMPLATE/bug-report.yml +0 -75
- package/.github/workflows/ci.yml +0 -98
- package/.github/workflows/release.yml +0 -52
- package/CHANGELOG.md +0 -12
- package/biome.json +0 -33
- package/codex-skill/kolmopdf/SKILL.md +0 -108
- package/codex-skill/kolmopdf/references/chain-recipes.md +0 -35
- package/codex-skill/kolmopdf/references/parameter-glossary.md +0 -72
- package/doc/apidocs/Format_Conversion_API_Guide.md +0 -117
- package/doc/apidocs/PDF_Layout_Translation_API_Guide.md +0 -138
- package/doc/apidocs/PDF_Parsing_API_Guide.md +0 -364
- package/doc/plan/DEVELOPMENT.md +0 -896
- package/doc/plan/DISTRIBUTION.md +0 -377
- package/doc/plan/TESTING_AND_USAGE.md +0 -370
- package/packages/mcp-server/LICENSE +0 -21
- package/packages/mcp-server/README.md +0 -37
- package/packages/mcp-server/dist/index.cjs.map +0 -1
- package/packages/mcp-server/dist/index.js.map +0 -1
- package/packages/mcp-server/package.json +0 -54
- package/packages/mcp-server/src/client.ts +0 -235
- package/packages/mcp-server/src/config.ts +0 -62
- package/packages/mcp-server/src/context.ts +0 -27
- package/packages/mcp-server/src/errors.ts +0 -271
- package/packages/mcp-server/src/extract.ts +0 -102
- package/packages/mcp-server/src/index.ts +0 -142
- package/packages/mcp-server/src/pages.ts +0 -16
- package/packages/mcp-server/src/polling.ts +0 -84
- package/packages/mcp-server/src/progress.ts +0 -48
- package/packages/mcp-server/src/tools/check-balance.ts +0 -33
- package/packages/mcp-server/src/tools/convert.ts +0 -130
- package/packages/mcp-server/src/tools/estimate-cost.ts +0 -82
- package/packages/mcp-server/src/tools/get-task-status.ts +0 -24
- package/packages/mcp-server/src/tools/parse-pdf.ts +0 -147
- package/packages/mcp-server/src/tools/translate-pdf.ts +0 -110
- package/packages/mcp-server/tests/integration/smoke.test.ts +0 -33
- package/packages/mcp-server/tests/unit/config.test.ts +0 -49
- package/packages/mcp-server/tests/unit/convert.test.ts +0 -28
- package/packages/mcp-server/tests/unit/errors.test.ts +0 -112
- package/packages/mcp-server/tests/unit/estimate-cost.test.ts +0 -28
- package/packages/mcp-server/tests/unit/polling.test.ts +0 -24
- package/packages/mcp-server/tsconfig.json +0 -9
- package/packages/mcp-server/tsup.config.ts +0 -13
- package/packages/mcp-server/vitest.config.ts +0 -13
- package/plugins/kolmopdf/.claude-plugin/plugin.json +0 -16
- package/plugins/kolmopdf/.mcp.json +0 -11
- package/plugins/kolmopdf/README.md +0 -28
- package/plugins/kolmopdf/commands/balance.md +0 -6
- package/plugins/kolmopdf/commands/convert.md +0 -14
- package/plugins/kolmopdf/commands/parse.md +0 -14
- package/plugins/kolmopdf/commands/translate.md +0 -14
- package/plugins/kolmopdf/skills/kolmopdf/SKILL.md +0 -108
- package/plugins/kolmopdf/skills/kolmopdf/references/chain-recipes.md +0 -35
- package/plugins/kolmopdf/skills/kolmopdf/references/parameter-glossary.md +0 -72
- package/pnpm-workspace.yaml +0 -2
- package/smithery.yaml +0 -21
- package/tsconfig.base.json +0 -21
- /package/{packages/mcp-server/dist → dist}/index.cjs +0 -0
- /package/{packages/mcp-server/dist → dist}/index.d.cts +0 -0
- /package/{packages/mcp-server/dist → dist}/index.d.ts +0 -0
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { createWriteStream, mkdirSync } from "node:fs";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
-
import { basename, join, resolve } from "node:path";
|
|
4
|
-
import { z } from "zod";
|
|
5
|
-
import type { McpSuccessResult, ToolContext } from "../context.js";
|
|
6
|
-
import { jsonResult } from "../context.js";
|
|
7
|
-
import { KolmoPdfError } from "../errors.js";
|
|
8
|
-
import { MAX_FILE_BYTES, readFileSize } from "../pages.js";
|
|
9
|
-
import { pollUntilComplete } from "../polling.js";
|
|
10
|
-
|
|
11
|
-
export const convertName = "kolmopdf_convert_markdown";
|
|
12
|
-
|
|
13
|
-
export const convertDescription =
|
|
14
|
-
"Convert a Markdown file (or a ZIP of markdown + images) to DOCX, HTML, PDF, " +
|
|
15
|
-
"or LaTeX via KolmoPDF.";
|
|
16
|
-
|
|
17
|
-
export const convertInputSchema = z.object({
|
|
18
|
-
file_path: z
|
|
19
|
-
.string()
|
|
20
|
-
.describe("Path to a .md/.markdown file or .zip containing markdown + images."),
|
|
21
|
-
target_format: z.enum(["word", "docx", "html", "pdf", "latex", "tex"]).optional().default("word"),
|
|
22
|
-
output_subdir: z.string().optional(),
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
export type ConvertInput = z.infer<typeof convertInputSchema>;
|
|
26
|
-
|
|
27
|
-
export interface ConvertOutput {
|
|
28
|
-
task_id: string;
|
|
29
|
-
points_deducted: number;
|
|
30
|
-
remaining_points: number;
|
|
31
|
-
output: {
|
|
32
|
-
output_path: string;
|
|
33
|
-
target_format: string;
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function formatToExtension(targetFormat: string): string {
|
|
38
|
-
switch (targetFormat) {
|
|
39
|
-
case "word":
|
|
40
|
-
case "docx":
|
|
41
|
-
return ".docx";
|
|
42
|
-
case "html":
|
|
43
|
-
return ".html";
|
|
44
|
-
case "pdf":
|
|
45
|
-
return ".pdf";
|
|
46
|
-
case "latex":
|
|
47
|
-
case "tex":
|
|
48
|
-
return ".tex";
|
|
49
|
-
default:
|
|
50
|
-
return ".out";
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function normalizeFormat(targetFormat: string): string {
|
|
55
|
-
switch (targetFormat) {
|
|
56
|
-
case "word":
|
|
57
|
-
case "docx":
|
|
58
|
-
return "docx";
|
|
59
|
-
case "latex":
|
|
60
|
-
case "tex":
|
|
61
|
-
return "tex";
|
|
62
|
-
default:
|
|
63
|
-
return targetFormat;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
export async function convertHandler(
|
|
67
|
-
args: ConvertInput,
|
|
68
|
-
ctx: ToolContext,
|
|
69
|
-
): Promise<McpSuccessResult> {
|
|
70
|
-
const client = ctx.getClient();
|
|
71
|
-
const filePath = resolve(args.file_path);
|
|
72
|
-
const filename = basename(filePath);
|
|
73
|
-
|
|
74
|
-
const fileSize = await readFileSize(filePath);
|
|
75
|
-
if (fileSize > MAX_FILE_BYTES) {
|
|
76
|
-
throw new KolmoPdfError("convert_file_too_large");
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const ext = filePath.toLowerCase();
|
|
80
|
-
if (!ext.endsWith(".md") && !ext.endsWith(".markdown") && !ext.endsWith(".zip")) {
|
|
81
|
-
throw new KolmoPdfError("convert_file_type_unsupported");
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
await ctx.progress?.report("[uploading] Sending file for conversion...");
|
|
85
|
-
|
|
86
|
-
const fileBuffer = await readFile(filePath);
|
|
87
|
-
const submitResult = await client.convert(
|
|
88
|
-
fileBuffer,
|
|
89
|
-
{
|
|
90
|
-
target_format: args.target_format,
|
|
91
|
-
},
|
|
92
|
-
filename,
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
const taskId = submitResult.task_id;
|
|
96
|
-
await ctx.progress?.report(`[submitted] Task ${taskId} created`);
|
|
97
|
-
|
|
98
|
-
await pollUntilComplete({
|
|
99
|
-
client,
|
|
100
|
-
taskId,
|
|
101
|
-
options: {
|
|
102
|
-
pollIntervalMs: ctx.config.pollIntervalMs,
|
|
103
|
-
maxPollMinutes: ctx.config.maxPollMinutes,
|
|
104
|
-
},
|
|
105
|
-
progress: ctx.progress,
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
await ctx.progress?.report("[downloading] Fetching converted file...");
|
|
109
|
-
|
|
110
|
-
const subdir = args.output_subdir || taskId;
|
|
111
|
-
const outputRoot = resolve(ctx.config.outputDir, subdir);
|
|
112
|
-
mkdirSync(outputRoot, { recursive: true });
|
|
113
|
-
|
|
114
|
-
const outExt = formatToExtension(args.target_format);
|
|
115
|
-
const outputPath = join(outputRoot, `result${outExt}`);
|
|
116
|
-
const ws = createWriteStream(outputPath);
|
|
117
|
-
await client.download(taskId, ws);
|
|
118
|
-
|
|
119
|
-
const output: ConvertOutput = {
|
|
120
|
-
task_id: taskId,
|
|
121
|
-
points_deducted: submitResult.points_deducted,
|
|
122
|
-
remaining_points: submitResult.remaining_points,
|
|
123
|
-
output: {
|
|
124
|
-
output_path: outputPath,
|
|
125
|
-
target_format: normalizeFormat(args.target_format),
|
|
126
|
-
},
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
return jsonResult(output as unknown as Record<string, unknown>);
|
|
130
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import type { McpSuccessResult, ToolContext } from "../context.js";
|
|
4
|
-
import { jsonResult } from "../context.js";
|
|
5
|
-
import { readPageCount } from "../pages.js";
|
|
6
|
-
|
|
7
|
-
export const estimateCostName = "kolmopdf_estimate_cost";
|
|
8
|
-
|
|
9
|
-
export const estimateCostDescription =
|
|
10
|
-
"Estimate the credit cost of a KolmoPDF operation before running it. " +
|
|
11
|
-
"Reads page count locally and checks the current balance. Does not spend credits.";
|
|
12
|
-
|
|
13
|
-
export const estimateCostInputSchema = z.object({
|
|
14
|
-
file_path: z.string(),
|
|
15
|
-
operation: z.enum(["parse", "parse_translate", "translate", "convert"]),
|
|
16
|
-
options: z
|
|
17
|
-
.object({
|
|
18
|
-
images_as_url: z.boolean().optional(),
|
|
19
|
-
})
|
|
20
|
-
.optional(),
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
export type EstimateCostInput = z.infer<typeof estimateCostInputSchema>;
|
|
24
|
-
|
|
25
|
-
export type Operation = EstimateCostInput["operation"];
|
|
26
|
-
|
|
27
|
-
export interface EstimateCostOutput {
|
|
28
|
-
pages: number | null;
|
|
29
|
-
estimated_credits: number;
|
|
30
|
-
current_balance: number;
|
|
31
|
-
sufficient: boolean;
|
|
32
|
-
shortfall: number;
|
|
33
|
-
recommendation: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function estimateCredits(operation: Operation, pages: number): number {
|
|
37
|
-
switch (operation) {
|
|
38
|
-
case "parse":
|
|
39
|
-
return pages * 2;
|
|
40
|
-
case "parse_translate":
|
|
41
|
-
return pages * 3;
|
|
42
|
-
case "translate":
|
|
43
|
-
return pages * 2;
|
|
44
|
-
case "convert":
|
|
45
|
-
return 1;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function buildRecommendation(shortfall: number): string {
|
|
50
|
-
return shortfall > 0
|
|
51
|
-
? `Need top-up at https://www.kolmopdf.com/subscription (short by ${shortfall} credits).`
|
|
52
|
-
: "Sufficient";
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export async function estimateCostHandler(
|
|
56
|
-
args: EstimateCostInput,
|
|
57
|
-
ctx: ToolContext,
|
|
58
|
-
): Promise<McpSuccessResult> {
|
|
59
|
-
const client = ctx.getClient();
|
|
60
|
-
|
|
61
|
-
let pages: number | null = null;
|
|
62
|
-
if (args.operation !== "convert") {
|
|
63
|
-
const filePath = resolve(args.file_path);
|
|
64
|
-
pages = await readPageCount(filePath);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const estimatedCredits = estimateCredits(args.operation, pages ?? 1);
|
|
68
|
-
const balance = await client.getBalance();
|
|
69
|
-
const currentBalance = balance.points;
|
|
70
|
-
const shortfall = Math.max(0, estimatedCredits - currentBalance);
|
|
71
|
-
|
|
72
|
-
const output: EstimateCostOutput = {
|
|
73
|
-
pages,
|
|
74
|
-
estimated_credits: estimatedCredits,
|
|
75
|
-
current_balance: currentBalance,
|
|
76
|
-
sufficient: shortfall === 0,
|
|
77
|
-
shortfall,
|
|
78
|
-
recommendation: buildRecommendation(shortfall),
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
return jsonResult(output as unknown as Record<string, unknown>);
|
|
82
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import type { McpSuccessResult, ToolContext } from "../context.js";
|
|
3
|
-
import { jsonResult } from "../context.js";
|
|
4
|
-
|
|
5
|
-
export const getTaskStatusName = "kolmopdf_get_task_status";
|
|
6
|
-
|
|
7
|
-
export const getTaskStatusDescription =
|
|
8
|
-
"Advanced/debug tool. Returns raw status for a KolmoPDF task. Use only when " +
|
|
9
|
-
"explicitly asked to inspect a task by ID, or when troubleshooting a stuck task.";
|
|
10
|
-
|
|
11
|
-
export const getTaskStatusInputSchema = z.object({
|
|
12
|
-
task_id: z.string(),
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
export type GetTaskStatusInput = z.infer<typeof getTaskStatusInputSchema>;
|
|
16
|
-
|
|
17
|
-
export async function getTaskStatusHandler(
|
|
18
|
-
args: GetTaskStatusInput,
|
|
19
|
-
ctx: ToolContext,
|
|
20
|
-
): Promise<McpSuccessResult> {
|
|
21
|
-
const client = ctx.getClient();
|
|
22
|
-
const status = await client.getStatus(args.task_id);
|
|
23
|
-
return jsonResult(status as unknown as Record<string, unknown>);
|
|
24
|
-
}
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import { createWriteStream, mkdirSync } from "node:fs";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
-
import { basename, join, resolve } from "node:path";
|
|
4
|
-
import { z } from "zod";
|
|
5
|
-
import type { McpSuccessResult, ToolContext } from "../context.js";
|
|
6
|
-
import { jsonResult } from "../context.js";
|
|
7
|
-
import { KolmoPdfError } from "../errors.js";
|
|
8
|
-
import { extractZip } from "../extract.js";
|
|
9
|
-
import { MAX_FILE_BYTES, MAX_PAGES, readFileSize, readPageCount } from "../pages.js";
|
|
10
|
-
import { pollUntilComplete } from "../polling.js";
|
|
11
|
-
|
|
12
|
-
export const parsePdfName = "kolmopdf_parse_pdf";
|
|
13
|
-
|
|
14
|
-
export const parsePdfDescription =
|
|
15
|
-
"Parse a local PDF into Markdown via KolmoPDF. Handles formulas, tables, " +
|
|
16
|
-
"multi-column layouts, and code blocks. Optionally translates while parsing.";
|
|
17
|
-
|
|
18
|
-
export const parsePdfInputSchema = z.object({
|
|
19
|
-
file_path: z.string().describe("Absolute or cwd-relative path to a local PDF file."),
|
|
20
|
-
table_mode: z.enum(["markdown", "image"]).optional(),
|
|
21
|
-
formula_format: z.enum(["dollar", "bracket"]).optional(),
|
|
22
|
-
enable_translation: z.boolean().optional(),
|
|
23
|
-
target_language: z.enum(["zh", "en", "ja", "ko", "fr", "de", "es", "ru"]).optional(),
|
|
24
|
-
output_options: z.array(z.enum(["original", "translated", "bilingual"])).optional(),
|
|
25
|
-
images_as_url: z.boolean().optional(),
|
|
26
|
-
skip_rotation_detection: z.boolean().optional(),
|
|
27
|
-
enable_cross_page_merge: z.boolean().optional(),
|
|
28
|
-
output_subdir: z
|
|
29
|
-
.string()
|
|
30
|
-
.optional()
|
|
31
|
-
.describe("Subdirectory name under KOLMOPDF_OUTPUT_DIR. Defaults to <task_id>."),
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
export type ParsePdfInput = z.infer<typeof parsePdfInputSchema>;
|
|
35
|
-
|
|
36
|
-
export interface ParsePdfOutput {
|
|
37
|
-
task_id: string;
|
|
38
|
-
pages_parsed: number;
|
|
39
|
-
points_deducted: number;
|
|
40
|
-
remaining_points: number;
|
|
41
|
-
output: {
|
|
42
|
-
type: "zip_extracted" | "markdown_file";
|
|
43
|
-
markdown_path: string;
|
|
44
|
-
images_dir: string | null;
|
|
45
|
-
output_root: string;
|
|
46
|
-
};
|
|
47
|
-
preview: string;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export async function parsePdfHandler(
|
|
51
|
-
args: ParsePdfInput,
|
|
52
|
-
ctx: ToolContext,
|
|
53
|
-
): Promise<McpSuccessResult> {
|
|
54
|
-
const client = ctx.getClient();
|
|
55
|
-
const filePath = resolve(args.file_path);
|
|
56
|
-
const filename = basename(filePath);
|
|
57
|
-
|
|
58
|
-
const fileSize = await readFileSize(filePath);
|
|
59
|
-
if (fileSize > MAX_FILE_BYTES) {
|
|
60
|
-
throw new KolmoPdfError("parse_file_too_large");
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const pageCount = await readPageCount(filePath);
|
|
64
|
-
if (pageCount > MAX_PAGES) {
|
|
65
|
-
throw new KolmoPdfError("parse_page_limit_exceeded");
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
await ctx.progress?.report("[uploading] Sending PDF to KolmoPDF...");
|
|
69
|
-
|
|
70
|
-
const fileBuffer = await readFile(filePath);
|
|
71
|
-
const submitResult = await client.parse(
|
|
72
|
-
fileBuffer,
|
|
73
|
-
{
|
|
74
|
-
table_mode: args.table_mode,
|
|
75
|
-
formula_format: args.formula_format,
|
|
76
|
-
enable_translation: args.enable_translation,
|
|
77
|
-
target_language: args.target_language,
|
|
78
|
-
output_options: args.output_options,
|
|
79
|
-
images_as_url: args.images_as_url,
|
|
80
|
-
skip_rotation_detection: args.skip_rotation_detection,
|
|
81
|
-
enable_cross_page_merge: args.enable_cross_page_merge,
|
|
82
|
-
},
|
|
83
|
-
filename,
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
const taskId = submitResult.task_id;
|
|
87
|
-
await ctx.progress?.report(`[submitted] Task ${taskId} created`);
|
|
88
|
-
|
|
89
|
-
await pollUntilComplete({
|
|
90
|
-
client,
|
|
91
|
-
taskId,
|
|
92
|
-
options: {
|
|
93
|
-
pollIntervalMs: ctx.config.pollIntervalMs,
|
|
94
|
-
maxPollMinutes: ctx.config.maxPollMinutes,
|
|
95
|
-
},
|
|
96
|
-
progress: ctx.progress,
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
await ctx.progress?.report("[downloading] Fetching result...");
|
|
100
|
-
|
|
101
|
-
const subdir = args.output_subdir || taskId;
|
|
102
|
-
const outputRoot = resolve(ctx.config.outputDir, subdir);
|
|
103
|
-
mkdirSync(outputRoot, { recursive: true });
|
|
104
|
-
|
|
105
|
-
const isUrlMode = args.images_as_url === true;
|
|
106
|
-
|
|
107
|
-
let markdownPath: string;
|
|
108
|
-
let imagesDir: string | null = null;
|
|
109
|
-
let outputType: "zip_extracted" | "markdown_file";
|
|
110
|
-
|
|
111
|
-
if (isUrlMode) {
|
|
112
|
-
markdownPath = join(outputRoot, "result.md");
|
|
113
|
-
const ws = createWriteStream(markdownPath);
|
|
114
|
-
await client.download(taskId, ws);
|
|
115
|
-
outputType = "markdown_file";
|
|
116
|
-
} else {
|
|
117
|
-
const zipPath = join(outputRoot, "result.zip");
|
|
118
|
-
const ws = createWriteStream(zipPath);
|
|
119
|
-
await client.download(taskId, ws);
|
|
120
|
-
const extracted = await extractZip(zipPath, outputRoot);
|
|
121
|
-
markdownPath = extracted.markdownPath || join(outputRoot, "result.md");
|
|
122
|
-
imagesDir = extracted.imagesDir;
|
|
123
|
-
outputType = "zip_extracted";
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const mdContent = await readFile(markdownPath, "utf-8").catch(() => "");
|
|
127
|
-
const preview = mdContent.slice(0, 500);
|
|
128
|
-
|
|
129
|
-
const ptsPerPage = args.enable_translation ? 3 : 2;
|
|
130
|
-
const pagesParsed = Math.round(submitResult.points_deducted / ptsPerPage);
|
|
131
|
-
|
|
132
|
-
const output: ParsePdfOutput = {
|
|
133
|
-
task_id: taskId,
|
|
134
|
-
pages_parsed: pagesParsed,
|
|
135
|
-
points_deducted: submitResult.points_deducted,
|
|
136
|
-
remaining_points: submitResult.remaining_points,
|
|
137
|
-
output: {
|
|
138
|
-
type: outputType,
|
|
139
|
-
markdown_path: markdownPath,
|
|
140
|
-
images_dir: imagesDir,
|
|
141
|
-
output_root: outputRoot,
|
|
142
|
-
},
|
|
143
|
-
preview,
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
return jsonResult(output as unknown as Record<string, unknown>);
|
|
147
|
-
}
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import { createWriteStream, mkdirSync } from "node:fs";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
-
import { basename, join, resolve } from "node:path";
|
|
4
|
-
import { z } from "zod";
|
|
5
|
-
import type { McpSuccessResult, ToolContext } from "../context.js";
|
|
6
|
-
import { jsonResult } from "../context.js";
|
|
7
|
-
import { KolmoPdfError } from "../errors.js";
|
|
8
|
-
import { MAX_FILE_BYTES, MAX_PAGES, readFileSize, readPageCount } from "../pages.js";
|
|
9
|
-
import { pollUntilComplete } from "../polling.js";
|
|
10
|
-
|
|
11
|
-
export const translatePdfName = "kolmopdf_translate_pdf";
|
|
12
|
-
|
|
13
|
-
export const translatePdfDescription =
|
|
14
|
-
"Translate a PDF while preserving its original layout via KolmoPDF. " +
|
|
15
|
-
"Produces a translated PDF (optionally side-by-side bilingual).";
|
|
16
|
-
|
|
17
|
-
export const translatePdfInputSchema = z.object({
|
|
18
|
-
file_path: z.string(),
|
|
19
|
-
source_language: z.string().optional().default("en"),
|
|
20
|
-
target_language: z.string().optional().default("zh"),
|
|
21
|
-
layout_modes: z
|
|
22
|
-
.array(z.enum(["translated_only", "side_by_side"]))
|
|
23
|
-
.optional()
|
|
24
|
-
.default(["translated_only"]),
|
|
25
|
-
enable_image_translation: z.boolean().optional().default(false),
|
|
26
|
-
enable_table_translation: z.boolean().optional().default(false),
|
|
27
|
-
output_subdir: z.string().optional(),
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
export type TranslatePdfInput = z.infer<typeof translatePdfInputSchema>;
|
|
31
|
-
|
|
32
|
-
export interface TranslatePdfOutput {
|
|
33
|
-
task_id: string;
|
|
34
|
-
pages_translated: number;
|
|
35
|
-
points_deducted: number;
|
|
36
|
-
remaining_points: number;
|
|
37
|
-
output: {
|
|
38
|
-
translated_pdf_path: string;
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export async function translatePdfHandler(
|
|
43
|
-
args: TranslatePdfInput,
|
|
44
|
-
ctx: ToolContext,
|
|
45
|
-
): Promise<McpSuccessResult> {
|
|
46
|
-
const client = ctx.getClient();
|
|
47
|
-
const filePath = resolve(args.file_path);
|
|
48
|
-
const filename = basename(filePath);
|
|
49
|
-
const fileSize = await readFileSize(filePath);
|
|
50
|
-
if (fileSize > MAX_FILE_BYTES) {
|
|
51
|
-
throw new KolmoPdfError("translate_pdf_file_too_large");
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const pageCount = await readPageCount(filePath);
|
|
55
|
-
if (pageCount > MAX_PAGES) {
|
|
56
|
-
throw new KolmoPdfError("translate_pdf_page_limit_exceeded");
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
await ctx.progress?.report("[uploading] Sending PDF for translation...");
|
|
60
|
-
|
|
61
|
-
const fileBuffer = await readFile(filePath);
|
|
62
|
-
const submitResult = await client.translatePdf(
|
|
63
|
-
fileBuffer,
|
|
64
|
-
{
|
|
65
|
-
source_language: args.source_language,
|
|
66
|
-
target_language: args.target_language,
|
|
67
|
-
layout_modes: args.layout_modes,
|
|
68
|
-
enable_image_translation: args.enable_image_translation,
|
|
69
|
-
enable_table_translation: args.enable_table_translation,
|
|
70
|
-
},
|
|
71
|
-
filename,
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
const taskId = submitResult.task_id;
|
|
75
|
-
await ctx.progress?.report(`[submitted] Task ${taskId} created`);
|
|
76
|
-
|
|
77
|
-
await pollUntilComplete({
|
|
78
|
-
client,
|
|
79
|
-
taskId,
|
|
80
|
-
options: {
|
|
81
|
-
pollIntervalMs: ctx.config.pollIntervalMs,
|
|
82
|
-
maxPollMinutes: ctx.config.maxPollMinutes,
|
|
83
|
-
},
|
|
84
|
-
progress: ctx.progress,
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
await ctx.progress?.report("[downloading] Fetching translated PDF...");
|
|
88
|
-
|
|
89
|
-
const subdir = args.output_subdir || taskId;
|
|
90
|
-
const outputRoot = resolve(ctx.config.outputDir, subdir);
|
|
91
|
-
mkdirSync(outputRoot, { recursive: true });
|
|
92
|
-
|
|
93
|
-
const pdfPath = join(outputRoot, "translated.pdf");
|
|
94
|
-
const ws = createWriteStream(pdfPath);
|
|
95
|
-
await client.download(taskId, ws);
|
|
96
|
-
|
|
97
|
-
const pagesTranslated = Math.round(submitResult.points_deducted / 2);
|
|
98
|
-
|
|
99
|
-
const output: TranslatePdfOutput = {
|
|
100
|
-
task_id: taskId,
|
|
101
|
-
pages_translated: pagesTranslated,
|
|
102
|
-
points_deducted: submitResult.points_deducted,
|
|
103
|
-
remaining_points: submitResult.remaining_points,
|
|
104
|
-
output: {
|
|
105
|
-
translated_pdf_path: pdfPath,
|
|
106
|
-
},
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
return jsonResult(output as unknown as Record<string, unknown>);
|
|
110
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { KolmoPdfClient } from "../../src/client.js";
|
|
3
|
-
import { loadConfig } from "../../src/config.js";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Live smoke tests (TESTING_AND_USAGE.md §7). These hit the real API and only
|
|
7
|
-
* run when KOLMOPDF_API_KEY is present (CI nightly job / manual). Skipped
|
|
8
|
-
* otherwise so the default `pnpm test` stays offline and deterministic.
|
|
9
|
-
*/
|
|
10
|
-
const hasKey = Boolean(process.env.KOLMOPDF_API_KEY);
|
|
11
|
-
const maybe = hasKey ? describe : describe.skip;
|
|
12
|
-
|
|
13
|
-
maybe("smoke-balance", () => {
|
|
14
|
-
it("returns a non-negative balance for a valid key", async () => {
|
|
15
|
-
const cfg = loadConfig();
|
|
16
|
-
const client = new KolmoPdfClient({
|
|
17
|
-
apiKey: cfg.apiKey as string,
|
|
18
|
-
baseUrl: cfg.baseUrl,
|
|
19
|
-
httpTimeoutMs: cfg.httpTimeoutMs,
|
|
20
|
-
uploadTimeoutMs: cfg.uploadTimeoutMs,
|
|
21
|
-
});
|
|
22
|
-
const balance = await client.getBalance();
|
|
23
|
-
expect(balance.success).toBe(true);
|
|
24
|
-
expect(balance.points).toBeGreaterThanOrEqual(0);
|
|
25
|
-
});
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
describe("smoke harness", () => {
|
|
29
|
-
it("is configured to skip when no API key is set", () => {
|
|
30
|
-
// Guards against accidentally enabling live calls in offline CI.
|
|
31
|
-
expect(typeof hasKey).toBe("boolean");
|
|
32
|
-
});
|
|
33
|
-
});
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { configDefaults, loadConfig, maskApiKey } from "../../src/config.js";
|
|
3
|
-
|
|
4
|
-
describe("loadConfig", () => {
|
|
5
|
-
it("applies defaults when env is empty", () => {
|
|
6
|
-
const cfg = loadConfig({});
|
|
7
|
-
expect(cfg.apiKey).toBeUndefined();
|
|
8
|
-
expect(cfg.baseUrl).toBe(configDefaults.baseUrl);
|
|
9
|
-
expect(cfg.outputDir).toBe(configDefaults.outputDir);
|
|
10
|
-
expect(cfg.pollIntervalMs).toBe(configDefaults.pollIntervalMs);
|
|
11
|
-
expect(cfg.maxPollMinutes).toBe(configDefaults.maxPollMinutes);
|
|
12
|
-
expect(cfg.httpTimeoutMs).toBe(configDefaults.httpTimeoutMs);
|
|
13
|
-
expect(cfg.uploadTimeoutMs).toBe(configDefaults.uploadTimeoutMs);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it("reads and trims the API key", () => {
|
|
17
|
-
expect(loadConfig({ KOLMOPDF_API_KEY: " sk-abc " }).apiKey).toBe("sk-abc");
|
|
18
|
-
expect(loadConfig({ KOLMOPDF_API_KEY: " " }).apiKey).toBeUndefined();
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it("strips trailing slashes from the base URL", () => {
|
|
22
|
-
expect(loadConfig({ KOLMOPDF_BASE_URL: "https://example.com/" }).baseUrl).toBe(
|
|
23
|
-
"https://example.com",
|
|
24
|
-
);
|
|
25
|
-
expect(loadConfig({ KOLMOPDF_BASE_URL: "https://example.com///" }).baseUrl).toBe(
|
|
26
|
-
"https://example.com",
|
|
27
|
-
);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("parses numeric envs and falls back on invalid values", () => {
|
|
31
|
-
expect(loadConfig({ KOLMOPDF_POLL_INTERVAL_MS: "500" }).pollIntervalMs).toBe(500);
|
|
32
|
-
expect(loadConfig({ KOLMOPDF_POLL_INTERVAL_MS: "abc" }).pollIntervalMs).toBe(
|
|
33
|
-
configDefaults.pollIntervalMs,
|
|
34
|
-
);
|
|
35
|
-
expect(loadConfig({ KOLMOPDF_MAX_POLL_MINUTES: "-3" }).maxPollMinutes).toBe(
|
|
36
|
-
configDefaults.maxPollMinutes,
|
|
37
|
-
);
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
describe("maskApiKey", () => {
|
|
42
|
-
it("shows first 6 + last 4 for long keys", () => {
|
|
43
|
-
expect(maskApiKey("sk-1234567890abcdef")).toBe("sk-123***cdef");
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("fully masks short keys", () => {
|
|
47
|
-
expect(maskApiKey("short")).toBe("***");
|
|
48
|
-
});
|
|
49
|
-
});
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { formatToExtension, normalizeFormat } from "../../src/tools/convert.js";
|
|
3
|
-
|
|
4
|
-
describe("formatToExtension", () => {
|
|
5
|
-
it.each([
|
|
6
|
-
["word", ".docx"],
|
|
7
|
-
["docx", ".docx"],
|
|
8
|
-
["html", ".html"],
|
|
9
|
-
["pdf", ".pdf"],
|
|
10
|
-
["latex", ".tex"],
|
|
11
|
-
["tex", ".tex"],
|
|
12
|
-
])("%s → %s", (input, expected) => {
|
|
13
|
-
expect(formatToExtension(input)).toBe(expected);
|
|
14
|
-
});
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
describe("normalizeFormat", () => {
|
|
18
|
-
it.each([
|
|
19
|
-
["word", "docx"],
|
|
20
|
-
["docx", "docx"],
|
|
21
|
-
["latex", "tex"],
|
|
22
|
-
["tex", "tex"],
|
|
23
|
-
["html", "html"],
|
|
24
|
-
["pdf", "pdf"],
|
|
25
|
-
])("%s → %s", (input, expected) => {
|
|
26
|
-
expect(normalizeFormat(input)).toBe(expected);
|
|
27
|
-
});
|
|
28
|
-
});
|