@kolmopdf/mcp-server 1.0.0 → 1.0.1
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,271 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unified KolmoPDF error model and MCP error-result formatting.
|
|
3
|
-
*
|
|
4
|
-
* Implements the error-code mapping table in DEVELOPMENT.md §8 and the
|
|
5
|
-
* MCP tool error envelope in §5.13.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export type ErrorSource = "api" | "client";
|
|
9
|
-
|
|
10
|
-
export interface ErrorSpec {
|
|
11
|
-
/** Default human-readable message. */
|
|
12
|
-
message: string;
|
|
13
|
-
/** Actionable remediation hint surfaced to the LLM / user. */
|
|
14
|
-
remediation: string;
|
|
15
|
-
/** Typical HTTP status; null for client-side codes. */
|
|
16
|
-
httpStatus: number | null;
|
|
17
|
-
source: ErrorSource;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Canonical mapping of every error_code we may surface (DEVELOPMENT.md §8). */
|
|
21
|
-
export const ERROR_SPECS: Record<string, ErrorSpec> = {
|
|
22
|
-
// --- API codes ---
|
|
23
|
-
invalid_api_key: {
|
|
24
|
-
message: "API key is missing or invalid.",
|
|
25
|
-
remediation: "Create a key at https://www.kolmopdf.com/api-keys (requires Plus/Pro).",
|
|
26
|
-
httpStatus: 401,
|
|
27
|
-
source: "api",
|
|
28
|
-
},
|
|
29
|
-
insufficient_points: {
|
|
30
|
-
message: "Not enough credits.",
|
|
31
|
-
remediation: "Top up at https://www.kolmopdf.com/subscription.",
|
|
32
|
-
httpStatus: 402,
|
|
33
|
-
source: "api",
|
|
34
|
-
},
|
|
35
|
-
points_deduction_failed: {
|
|
36
|
-
message: "Credit deduction failed.",
|
|
37
|
-
remediation: "Retry; if it persists contact support.",
|
|
38
|
-
httpStatus: 402,
|
|
39
|
-
source: "api",
|
|
40
|
-
},
|
|
41
|
-
no_file_found: {
|
|
42
|
-
message: "Request missing file field.",
|
|
43
|
-
remediation: "(internal) MCP server bug, please report.",
|
|
44
|
-
httpStatus: 400,
|
|
45
|
-
source: "api",
|
|
46
|
-
},
|
|
47
|
-
parse_file_too_large: {
|
|
48
|
-
message: "PDF exceeds 300MB.",
|
|
49
|
-
remediation: "Split the PDF locally.",
|
|
50
|
-
httpStatus: 400,
|
|
51
|
-
source: "api",
|
|
52
|
-
},
|
|
53
|
-
parse_page_limit_exceeded: {
|
|
54
|
-
message: "PDF exceeds 800 pages.",
|
|
55
|
-
remediation: "Split the PDF locally.",
|
|
56
|
-
httpStatus: 400,
|
|
57
|
-
source: "api",
|
|
58
|
-
},
|
|
59
|
-
parse_file_not_pdf: {
|
|
60
|
-
message: "File is not a valid PDF.",
|
|
61
|
-
remediation: "Upload a .pdf file.",
|
|
62
|
-
httpStatus: 400,
|
|
63
|
-
source: "api",
|
|
64
|
-
},
|
|
65
|
-
translate_pdf_file_too_large: {
|
|
66
|
-
message: "PDF exceeds 300MB.",
|
|
67
|
-
remediation: "Split the PDF locally.",
|
|
68
|
-
httpStatus: 400,
|
|
69
|
-
source: "api",
|
|
70
|
-
},
|
|
71
|
-
translate_pdf_file_not_pdf: {
|
|
72
|
-
message: "File is not a valid PDF.",
|
|
73
|
-
remediation: "Upload a .pdf file.",
|
|
74
|
-
httpStatus: 400,
|
|
75
|
-
source: "api",
|
|
76
|
-
},
|
|
77
|
-
translate_pdf_page_limit_exceeded: {
|
|
78
|
-
message: "PDF exceeds 800 pages.",
|
|
79
|
-
remediation: "Split the PDF locally.",
|
|
80
|
-
httpStatus: 400,
|
|
81
|
-
source: "api",
|
|
82
|
-
},
|
|
83
|
-
convert_file_too_large: {
|
|
84
|
-
message: "File exceeds 300MB.",
|
|
85
|
-
remediation: "Reduce file size.",
|
|
86
|
-
httpStatus: 400,
|
|
87
|
-
source: "api",
|
|
88
|
-
},
|
|
89
|
-
convert_file_type_unsupported: {
|
|
90
|
-
message: "File must be .md / .markdown / .zip.",
|
|
91
|
-
remediation: "Convert source to markdown first.",
|
|
92
|
-
httpStatus: 400,
|
|
93
|
-
source: "api",
|
|
94
|
-
},
|
|
95
|
-
convert_target_format_unsupported: {
|
|
96
|
-
message: "Target format unsupported.",
|
|
97
|
-
remediation: "Use word/docx/html/pdf/latex/tex.",
|
|
98
|
-
httpStatus: 400,
|
|
99
|
-
source: "api",
|
|
100
|
-
},
|
|
101
|
-
file_upload_failed: {
|
|
102
|
-
message: "Upload to storage failed.",
|
|
103
|
-
remediation: "Check network and retry.",
|
|
104
|
-
httpStatus: 500,
|
|
105
|
-
source: "api",
|
|
106
|
-
},
|
|
107
|
-
task_creation_failed: {
|
|
108
|
-
message: "Task creation failed.",
|
|
109
|
-
remediation: "Retry.",
|
|
110
|
-
httpStatus: 500,
|
|
111
|
-
source: "api",
|
|
112
|
-
},
|
|
113
|
-
parse_error: {
|
|
114
|
-
message: "Parsing failed.",
|
|
115
|
-
remediation: "Retry; if it persists, split and try again.",
|
|
116
|
-
httpStatus: 500,
|
|
117
|
-
source: "api",
|
|
118
|
-
},
|
|
119
|
-
parse_file_invalid: {
|
|
120
|
-
message: "PDF is malformed.",
|
|
121
|
-
remediation: "Re-export the PDF.",
|
|
122
|
-
httpStatus: 500,
|
|
123
|
-
source: "api",
|
|
124
|
-
},
|
|
125
|
-
parse_timeout: {
|
|
126
|
-
message: "Server-side timeout.",
|
|
127
|
-
remediation: "Split into smaller PDFs.",
|
|
128
|
-
httpStatus: 500,
|
|
129
|
-
source: "api",
|
|
130
|
-
},
|
|
131
|
-
api_task_error: {
|
|
132
|
-
message: "Generic task error.",
|
|
133
|
-
remediation: "Retry; if it persists contact support.",
|
|
134
|
-
httpStatus: 500,
|
|
135
|
-
source: "api",
|
|
136
|
-
},
|
|
137
|
-
// --- client codes ---
|
|
138
|
-
client_polling_timeout: {
|
|
139
|
-
message: "Local polling exceeded KOLMOPDF_MAX_POLL_MINUTES.",
|
|
140
|
-
remediation: "Task may still be running. Use kolmopdf_get_task_status with task_id.",
|
|
141
|
-
httpStatus: null,
|
|
142
|
-
source: "client",
|
|
143
|
-
},
|
|
144
|
-
client_network_error: {
|
|
145
|
-
message: "Network error after retries.",
|
|
146
|
-
remediation: "Check network.",
|
|
147
|
-
httpStatus: null,
|
|
148
|
-
source: "client",
|
|
149
|
-
},
|
|
150
|
-
client_local_validation: {
|
|
151
|
-
message: "Local pre-check failed (page count / file size).",
|
|
152
|
-
remediation: "See message for the specific limit that was exceeded.",
|
|
153
|
-
httpStatus: null,
|
|
154
|
-
source: "client",
|
|
155
|
-
},
|
|
156
|
-
client_extract_failed: {
|
|
157
|
-
message: "ZIP extraction failed.",
|
|
158
|
-
remediation: "Check disk permissions on output dir.",
|
|
159
|
-
httpStatus: null,
|
|
160
|
-
source: "client",
|
|
161
|
-
},
|
|
162
|
-
} as const;
|
|
163
|
-
|
|
164
|
-
const UNKNOWN_SPEC: ErrorSpec = {
|
|
165
|
-
message: "Unknown error.",
|
|
166
|
-
remediation: "Retry; if it persists contact https://www.kolmopdf.com/contact.",
|
|
167
|
-
httpStatus: null,
|
|
168
|
-
source: "client",
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
export interface KolmoPdfErrorOptions {
|
|
172
|
-
/** Override the default message from the spec. */
|
|
173
|
-
message?: string;
|
|
174
|
-
/** Override the default HTTP status from the spec. */
|
|
175
|
-
httpStatus?: number | null;
|
|
176
|
-
pointsRequired?: number;
|
|
177
|
-
currentPoints?: number;
|
|
178
|
-
/** Override the default remediation hint. */
|
|
179
|
-
remediation?: string;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/** Structured error thrown across the MCP server; carries a stable error_code. */
|
|
183
|
-
export class KolmoPdfError extends Error {
|
|
184
|
-
readonly errorCode: string;
|
|
185
|
-
readonly httpStatus: number | null;
|
|
186
|
-
readonly remediation: string;
|
|
187
|
-
readonly pointsRequired: number | undefined;
|
|
188
|
-
readonly currentPoints: number | undefined;
|
|
189
|
-
readonly source: ErrorSource;
|
|
190
|
-
|
|
191
|
-
constructor(errorCode: string, opts: KolmoPdfErrorOptions = {}) {
|
|
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 !== undefined ? 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
|
-
|
|
204
|
-
/** Shape of the JSON payload embedded in an MCP error result (DEVELOPMENT.md §5.13). */
|
|
205
|
-
export interface McpErrorPayload {
|
|
206
|
-
error_code: string;
|
|
207
|
-
message: string;
|
|
208
|
-
http_status: number | null;
|
|
209
|
-
points_required?: number;
|
|
210
|
-
current_points?: number;
|
|
211
|
-
remediation: string;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/** MCP tool result envelope for an error (matches MCP SDK `CallToolResult`). */
|
|
215
|
-
export interface McpErrorResult {
|
|
216
|
-
isError: true;
|
|
217
|
-
content: Array<{ type: "text"; text: string }>;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/** Convert a KolmoPdfError (or any error) into the MCP error result envelope. */
|
|
221
|
-
export function toMcpErrorResult(err: unknown): McpErrorResult {
|
|
222
|
-
const kerr =
|
|
223
|
-
err instanceof KolmoPdfError
|
|
224
|
-
? err
|
|
225
|
-
: new KolmoPdfError("api_task_error", {
|
|
226
|
-
message: err instanceof Error ? err.message : String(err),
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
const payload: McpErrorPayload = {
|
|
230
|
-
error_code: kerr.errorCode,
|
|
231
|
-
message: kerr.message,
|
|
232
|
-
http_status: kerr.httpStatus,
|
|
233
|
-
remediation: kerr.remediation,
|
|
234
|
-
};
|
|
235
|
-
if (kerr.pointsRequired !== undefined) payload.points_required = kerr.pointsRequired;
|
|
236
|
-
if (kerr.currentPoints !== undefined) payload.current_points = kerr.currentPoints;
|
|
237
|
-
|
|
238
|
-
return {
|
|
239
|
-
isError: true,
|
|
240
|
-
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/** Whether an API failure with this code is auto-refunded server-side (§8). */
|
|
245
|
-
export function isAutoRefunded(errorCode: string): boolean {
|
|
246
|
-
return (
|
|
247
|
-
errorCode === "task_creation_failed" ||
|
|
248
|
-
errorCode === "parse_error" ||
|
|
249
|
-
errorCode === "parse_file_invalid" ||
|
|
250
|
-
errorCode === "parse_timeout"
|
|
251
|
-
);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/** Map a raw API JSON failure body to a KolmoPdfError. */
|
|
255
|
-
export function errorFromApiBody(
|
|
256
|
-
body: {
|
|
257
|
-
error_code?: string;
|
|
258
|
-
message?: string;
|
|
259
|
-
points_required?: number;
|
|
260
|
-
current_points?: number;
|
|
261
|
-
},
|
|
262
|
-
httpStatus?: number,
|
|
263
|
-
): KolmoPdfError {
|
|
264
|
-
const code = body.error_code ?? "api_task_error";
|
|
265
|
-
return new KolmoPdfError(code, {
|
|
266
|
-
message: body.message,
|
|
267
|
-
httpStatus: httpStatus ?? null,
|
|
268
|
-
pointsRequired: body.points_required,
|
|
269
|
-
currentPoints: body.current_points,
|
|
270
|
-
});
|
|
271
|
-
}
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { createWriteStream, mkdirSync } from "node:fs";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
|
-
import { pipeline } from "node:stream/promises";
|
|
4
|
-
import { type Entry, type ZipFile, open as yauzlOpen } from "yauzl";
|
|
5
|
-
|
|
6
|
-
export interface ExtractResult {
|
|
7
|
-
markdownPath: string | null;
|
|
8
|
-
imagesDir: string | null;
|
|
9
|
-
outputRoot: string;
|
|
10
|
-
files: string[];
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export async function extractZip(zipPath: string, destDir: string): Promise<ExtractResult> {
|
|
14
|
-
mkdirSync(destDir, { recursive: true });
|
|
15
|
-
|
|
16
|
-
const zipFile = await openZip(zipPath);
|
|
17
|
-
const files: string[] = [];
|
|
18
|
-
let markdownPath: string | null = null;
|
|
19
|
-
let imagesDir: string | null = null;
|
|
20
|
-
|
|
21
|
-
for await (const entry of iterEntries(zipFile)) {
|
|
22
|
-
const entryPath = join(destDir, entry.fileName);
|
|
23
|
-
|
|
24
|
-
if (entry.fileName.endsWith("/")) {
|
|
25
|
-
mkdirSync(entryPath, { recursive: true });
|
|
26
|
-
if (entry.fileName.includes("images")) {
|
|
27
|
-
imagesDir = entryPath;
|
|
28
|
-
}
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
mkdirSync(dirname(entryPath), { recursive: true });
|
|
33
|
-
const readStream = await openReadStream(zipFile, entry);
|
|
34
|
-
const writeStream = createWriteStream(entryPath);
|
|
35
|
-
await pipeline(readStream, writeStream);
|
|
36
|
-
files.push(entryPath);
|
|
37
|
-
|
|
38
|
-
if (!markdownPath && /\.md$/i.test(entry.fileName)) {
|
|
39
|
-
markdownPath = entryPath;
|
|
40
|
-
}
|
|
41
|
-
if (!imagesDir && /images\//i.test(entry.fileName)) {
|
|
42
|
-
const prefix = entry.fileName.split("images/")[0] ?? "";
|
|
43
|
-
imagesDir = join(destDir, prefix, "images");
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return { markdownPath, imagesDir, outputRoot: destDir, files };
|
|
48
|
-
}
|
|
49
|
-
function openZip(path: string): Promise<ZipFile> {
|
|
50
|
-
return new Promise((resolve, reject) => {
|
|
51
|
-
yauzlOpen(path, { lazyEntries: true }, (err, zf) => {
|
|
52
|
-
if (err || !zf) return reject(err ?? new Error("Failed to open zip"));
|
|
53
|
-
resolve(zf);
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async function* iterEntries(zipFile: ZipFile): AsyncGenerator<Entry> {
|
|
59
|
-
let resolve: ((entry: Entry | null) => void) | null = null;
|
|
60
|
-
const queue: (Entry | null)[] = [];
|
|
61
|
-
|
|
62
|
-
zipFile.on("entry", (entry: Entry) => {
|
|
63
|
-
if (resolve) {
|
|
64
|
-
const r = resolve;
|
|
65
|
-
resolve = null;
|
|
66
|
-
r(entry);
|
|
67
|
-
} else {
|
|
68
|
-
queue.push(entry);
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
zipFile.on("end", () => {
|
|
72
|
-
if (resolve) {
|
|
73
|
-
const r = resolve;
|
|
74
|
-
resolve = null;
|
|
75
|
-
r(null);
|
|
76
|
-
} else {
|
|
77
|
-
queue.push(null);
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
zipFile.readEntry();
|
|
82
|
-
while (true) {
|
|
83
|
-
const entry =
|
|
84
|
-
queue.length > 0
|
|
85
|
-
? (queue.shift() as Entry | null)
|
|
86
|
-
: await new Promise<Entry | null>((r) => {
|
|
87
|
-
resolve = r;
|
|
88
|
-
});
|
|
89
|
-
if (entry === null) break;
|
|
90
|
-
yield entry;
|
|
91
|
-
zipFile.readEntry();
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function openReadStream(zipFile: ZipFile, entry: Entry): Promise<NodeJS.ReadableStream> {
|
|
96
|
-
return new Promise((resolve, reject) => {
|
|
97
|
-
zipFile.openReadStream(entry, (err, stream) => {
|
|
98
|
-
if (err || !stream) return reject(err ?? new Error("Failed to open entry stream"));
|
|
99
|
-
resolve(stream);
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
}
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @kolmopdf/mcp-server — stdio MCP server bootstrap (DEVELOPMENT.md §5.2).
|
|
3
|
-
*
|
|
4
|
-
* - Registers over the stdio transport.
|
|
5
|
-
* - Does NOT validate the API key at startup; the key is read lazily so the
|
|
6
|
-
* server boots even with no network / no key. The first authenticated tool
|
|
7
|
-
* call surfaces a missing key as an MCP error (invalid_api_key).
|
|
8
|
-
*/
|
|
9
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
|
-
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
12
|
-
import { KolmoPdfClient } from "./client.js";
|
|
13
|
-
import { loadConfig } from "./config.js";
|
|
14
|
-
import { type McpSuccessResult, type ToolContext, jsonResult } from "./context.js";
|
|
15
|
-
import { KolmoPdfError, toMcpErrorResult } from "./errors.js";
|
|
16
|
-
import {
|
|
17
|
-
checkBalanceDescription,
|
|
18
|
-
checkBalanceHandler,
|
|
19
|
-
checkBalanceInputSchema,
|
|
20
|
-
checkBalanceName,
|
|
21
|
-
} from "./tools/check-balance.js";
|
|
22
|
-
import {
|
|
23
|
-
convertDescription,
|
|
24
|
-
convertHandler,
|
|
25
|
-
convertInputSchema,
|
|
26
|
-
convertName,
|
|
27
|
-
} from "./tools/convert.js";
|
|
28
|
-
import {
|
|
29
|
-
estimateCostDescription,
|
|
30
|
-
estimateCostHandler,
|
|
31
|
-
estimateCostInputSchema,
|
|
32
|
-
estimateCostName,
|
|
33
|
-
} from "./tools/estimate-cost.js";
|
|
34
|
-
import {
|
|
35
|
-
getTaskStatusDescription,
|
|
36
|
-
getTaskStatusHandler,
|
|
37
|
-
getTaskStatusInputSchema,
|
|
38
|
-
getTaskStatusName,
|
|
39
|
-
} from "./tools/get-task-status.js";
|
|
40
|
-
import {
|
|
41
|
-
parsePdfDescription,
|
|
42
|
-
parsePdfHandler,
|
|
43
|
-
parsePdfInputSchema,
|
|
44
|
-
parsePdfName,
|
|
45
|
-
} from "./tools/parse-pdf.js";
|
|
46
|
-
import {
|
|
47
|
-
translatePdfDescription,
|
|
48
|
-
translatePdfHandler,
|
|
49
|
-
translatePdfInputSchema,
|
|
50
|
-
translatePdfName,
|
|
51
|
-
} from "./tools/translate-pdf.js";
|
|
52
|
-
|
|
53
|
-
const VERSION = "1.0.0";
|
|
54
|
-
|
|
55
|
-
/** Build the per-call tool context with a lazily-constructed API client. */
|
|
56
|
-
function buildContext(): ToolContext {
|
|
57
|
-
const config = loadConfig();
|
|
58
|
-
return {
|
|
59
|
-
config,
|
|
60
|
-
getClient(): KolmoPdfClient {
|
|
61
|
-
if (!config.apiKey) {
|
|
62
|
-
throw new KolmoPdfError("invalid_api_key");
|
|
63
|
-
}
|
|
64
|
-
return new KolmoPdfClient({
|
|
65
|
-
apiKey: config.apiKey,
|
|
66
|
-
baseUrl: config.baseUrl,
|
|
67
|
-
httpTimeoutMs: config.httpTimeoutMs,
|
|
68
|
-
uploadTimeoutMs: config.uploadTimeoutMs,
|
|
69
|
-
});
|
|
70
|
-
},
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** Wrap a typed handler so all thrown errors become MCP error results (§5.13). */
|
|
75
|
-
function guard<A>(
|
|
76
|
-
handler: (args: A, ctx: ToolContext) => Promise<McpSuccessResult>,
|
|
77
|
-
): (args: unknown) => Promise<CallToolResult> {
|
|
78
|
-
return async (args: unknown) => {
|
|
79
|
-
try {
|
|
80
|
-
return (await handler(args as A, buildContext())) as CallToolResult;
|
|
81
|
-
} catch (err) {
|
|
82
|
-
return toMcpErrorResult(err) as CallToolResult;
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export function createServer(): McpServer {
|
|
88
|
-
const server = new McpServer({ name: "kolmopdf", version: VERSION });
|
|
89
|
-
|
|
90
|
-
server.registerTool(
|
|
91
|
-
parsePdfName,
|
|
92
|
-
{ description: parsePdfDescription, inputSchema: parsePdfInputSchema.shape },
|
|
93
|
-
guard(parsePdfHandler),
|
|
94
|
-
);
|
|
95
|
-
server.registerTool(
|
|
96
|
-
translatePdfName,
|
|
97
|
-
{ description: translatePdfDescription, inputSchema: translatePdfInputSchema.shape },
|
|
98
|
-
guard(translatePdfHandler),
|
|
99
|
-
);
|
|
100
|
-
server.registerTool(
|
|
101
|
-
convertName,
|
|
102
|
-
{ description: convertDescription, inputSchema: convertInputSchema.shape },
|
|
103
|
-
guard(convertHandler),
|
|
104
|
-
);
|
|
105
|
-
server.registerTool(
|
|
106
|
-
estimateCostName,
|
|
107
|
-
{ description: estimateCostDescription, inputSchema: estimateCostInputSchema.shape },
|
|
108
|
-
guard(estimateCostHandler),
|
|
109
|
-
);
|
|
110
|
-
server.registerTool(
|
|
111
|
-
checkBalanceName,
|
|
112
|
-
{ description: checkBalanceDescription, inputSchema: checkBalanceInputSchema.shape },
|
|
113
|
-
guard(checkBalanceHandler),
|
|
114
|
-
);
|
|
115
|
-
server.registerTool(
|
|
116
|
-
getTaskStatusName,
|
|
117
|
-
{ description: getTaskStatusDescription, inputSchema: getTaskStatusInputSchema.shape },
|
|
118
|
-
guard(getTaskStatusHandler),
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
return server;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async function main(): Promise<void> {
|
|
125
|
-
// Surface --version without booting the transport (TESTING_AND_USAGE.md §9).
|
|
126
|
-
if (process.argv.includes("--version") || process.argv.includes("-v")) {
|
|
127
|
-
process.stdout.write(`${VERSION}\n`);
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
const server = createServer();
|
|
131
|
-
const transport = new StdioServerTransport();
|
|
132
|
-
await server.connect(transport);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// `jsonResult` is part of the public surface used by tool handlers (M2+).
|
|
136
|
-
export { jsonResult };
|
|
137
|
-
|
|
138
|
-
main().catch((err) => {
|
|
139
|
-
const detail = err instanceof Error ? err.stack : String(err);
|
|
140
|
-
process.stderr.write(`[kolmopdf-mcp] fatal: ${detail}\n`);
|
|
141
|
-
process.exit(1);
|
|
142
|
-
});
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { readFile, stat } from "node:fs/promises";
|
|
2
|
-
import { PDFDocument } from "pdf-lib";
|
|
3
|
-
|
|
4
|
-
export const MAX_PAGES = 800;
|
|
5
|
-
export const MAX_FILE_BYTES = 300 * 1024 * 1024;
|
|
6
|
-
|
|
7
|
-
export async function readPageCount(filePath: string): Promise<number> {
|
|
8
|
-
const data = await readFile(filePath);
|
|
9
|
-
const doc = await PDFDocument.load(data, { ignoreEncryption: true });
|
|
10
|
-
return doc.getPageCount();
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export async function readFileSize(filePath: string): Promise<number> {
|
|
14
|
-
const s = await stat(filePath);
|
|
15
|
-
return s.size;
|
|
16
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import type { KolmoPdfClient, StatusResult } from "./client.js";
|
|
2
|
-
import { KolmoPdfError } from "./errors.js";
|
|
3
|
-
import { type ProgressReporter, humanizeStatus } from "./progress.js";
|
|
4
|
-
|
|
5
|
-
export interface PollOptions {
|
|
6
|
-
pollIntervalMs: number;
|
|
7
|
-
maxPollMinutes: number;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export const TERMINAL_OK = "completed";
|
|
11
|
-
export const TERMINAL_FAIL = "failed";
|
|
12
|
-
export const IN_FLIGHT_STATUSES = new Set(["pending", "waiting", "processing"]);
|
|
13
|
-
|
|
14
|
-
export const RETRY_POLICY = {
|
|
15
|
-
maxAttempts: 3,
|
|
16
|
-
baseDelayMs: 1000,
|
|
17
|
-
factor: 2,
|
|
18
|
-
} as const;
|
|
19
|
-
|
|
20
|
-
export function backoffDelayMs(attempt: number): number {
|
|
21
|
-
return RETRY_POLICY.baseDelayMs * RETRY_POLICY.factor ** (attempt - 1);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function isRetryable(err: { httpStatus?: number | null; code?: string }): boolean {
|
|
25
|
-
const transientCodes = ["ECONNRESET", "ETIMEDOUT", "ECONNREFUSED", "EAI_AGAIN"];
|
|
26
|
-
if (err.code && transientCodes.includes(err.code)) return true;
|
|
27
|
-
if (typeof err.httpStatus === "number" && err.httpStatus >= 500) return true;
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface PollContext {
|
|
32
|
-
client: KolmoPdfClient;
|
|
33
|
-
taskId: string;
|
|
34
|
-
options: PollOptions;
|
|
35
|
-
progress?: ProgressReporter;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function sleep(ms: number): Promise<void> {
|
|
39
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async function fetchStatusWithRetry(client: KolmoPdfClient, taskId: string): Promise<StatusResult> {
|
|
43
|
-
for (let attempt = 1; attempt <= RETRY_POLICY.maxAttempts; attempt++) {
|
|
44
|
-
try {
|
|
45
|
-
return await client.getStatus(taskId);
|
|
46
|
-
} catch (err) {
|
|
47
|
-
const retryable =
|
|
48
|
-
err instanceof KolmoPdfError
|
|
49
|
-
? isRetryable(err)
|
|
50
|
-
: isRetryable({ code: (err as NodeJS.ErrnoException).code });
|
|
51
|
-
if (!retryable || attempt === RETRY_POLICY.maxAttempts) throw err;
|
|
52
|
-
await sleep(backoffDelayMs(attempt));
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
throw new KolmoPdfError("client_network_error");
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export async function pollUntilComplete(ctx: PollContext): Promise<StatusResult> {
|
|
59
|
-
const { client, taskId, options, progress } = ctx;
|
|
60
|
-
const deadline = Date.now() + options.maxPollMinutes * 60_000;
|
|
61
|
-
|
|
62
|
-
while (true) {
|
|
63
|
-
if (Date.now() > deadline) {
|
|
64
|
-
throw new KolmoPdfError("client_polling_timeout");
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const result = await fetchStatusWithRetry(client, taskId);
|
|
68
|
-
|
|
69
|
-
if (result.status === TERMINAL_OK) {
|
|
70
|
-
await progress?.report(`[completed] Task ${taskId} done`);
|
|
71
|
-
return result;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (result.status === TERMINAL_FAIL) {
|
|
75
|
-
throw new KolmoPdfError(result.error_code || "api_task_error", {
|
|
76
|
-
message: result.message || "Task failed",
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const aheadTasks = result.queue_info?.ahead_tasks;
|
|
81
|
-
await progress?.report(humanizeStatus(result.status as string, aheadTasks));
|
|
82
|
-
await sleep(options.pollIntervalMs);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MCP progress-notification helper (DEVELOPMENT.md §5.11).
|
|
3
|
-
*
|
|
4
|
-
* `progress` must be monotonically increasing; `total` is omitted because the
|
|
5
|
-
* API does not provide a precise percentage.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/** Minimal shape of the MCP request context used to emit notifications. */
|
|
9
|
-
export interface ProgressSink {
|
|
10
|
-
/** Present only when the client supplied a progressToken in request `_meta`. */
|
|
11
|
-
progressToken?: string | number;
|
|
12
|
-
notify(notification: {
|
|
13
|
-
method: "notifications/progress";
|
|
14
|
-
params: {
|
|
15
|
-
progressToken: string | number;
|
|
16
|
-
progress: number;
|
|
17
|
-
message?: string;
|
|
18
|
-
};
|
|
19
|
-
}): Promise<void>;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Stateful emitter that guarantees a monotonically increasing counter. */
|
|
23
|
-
export class ProgressReporter {
|
|
24
|
-
private counter = 0;
|
|
25
|
-
|
|
26
|
-
constructor(private readonly sink: ProgressSink | undefined) {}
|
|
27
|
-
|
|
28
|
-
async report(message: string): Promise<void> {
|
|
29
|
-
if (!this.sink || this.sink.progressToken === undefined) return;
|
|
30
|
-
this.counter += 1;
|
|
31
|
-
await this.sink.notify({
|
|
32
|
-
method: "notifications/progress",
|
|
33
|
-
params: {
|
|
34
|
-
progressToken: this.sink.progressToken,
|
|
35
|
-
progress: this.counter,
|
|
36
|
-
message,
|
|
37
|
-
},
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Build a human-readable status line, e.g. "[waiting] 3 tasks ahead". */
|
|
43
|
-
export function humanizeStatus(status: string, aheadTasks?: number): string {
|
|
44
|
-
if (status === "waiting" && typeof aheadTasks === "number") {
|
|
45
|
-
return `[waiting] ${aheadTasks} tasks ahead`;
|
|
46
|
-
}
|
|
47
|
-
return `[${status}]`;
|
|
48
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { maskApiKey } from "../config.js";
|
|
3
|
-
import type { McpSuccessResult, ToolContext } from "../context.js";
|
|
4
|
-
import { jsonResult } from "../context.js";
|
|
5
|
-
|
|
6
|
-
export const checkBalanceName = "kolmopdf_check_balance";
|
|
7
|
-
|
|
8
|
-
export const checkBalanceDescription =
|
|
9
|
-
"Show the current KolmoPDF credit balance for the configured API key.";
|
|
10
|
-
|
|
11
|
-
export const checkBalanceInputSchema = z.object({});
|
|
12
|
-
|
|
13
|
-
export type CheckBalanceInput = z.infer<typeof checkBalanceInputSchema>;
|
|
14
|
-
|
|
15
|
-
export interface CheckBalanceOutput {
|
|
16
|
-
points: number;
|
|
17
|
-
api_key_masked: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export async function checkBalanceHandler(
|
|
21
|
-
_args: CheckBalanceInput,
|
|
22
|
-
ctx: ToolContext,
|
|
23
|
-
): Promise<McpSuccessResult> {
|
|
24
|
-
const client = ctx.getClient();
|
|
25
|
-
const balance = await client.getBalance();
|
|
26
|
-
|
|
27
|
-
const output: CheckBalanceOutput = {
|
|
28
|
-
points: balance.points,
|
|
29
|
-
api_key_masked: maskApiKey(ctx.config.apiKey || ""),
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
return jsonResult(output as unknown as Record<string, unknown>);
|
|
33
|
-
}
|