@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 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/client.ts","../src/errors.ts","../src/config.ts","../src/context.ts","../src/tools/check-balance.ts","../src/tools/convert.ts","../src/pages.ts","../src/progress.ts","../src/polling.ts","../src/tools/estimate-cost.ts","../src/tools/get-task-status.ts","../src/tools/parse-pdf.ts","../src/extract.ts","../src/tools/translate-pdf.ts"],"sourcesContent":["/**\r\n * @kolmopdf/mcp-server — stdio MCP server bootstrap (DEVELOPMENT.md §5.2).\r\n *\r\n * - Registers over the stdio transport.\r\n * - Does NOT validate the API key at startup; the key is read lazily so the\r\n * server boots even with no network / no key. The first authenticated tool\r\n * call surfaces a missing key as an MCP error (invalid_api_key).\r\n */\r\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\r\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\r\nimport type { CallToolResult } from \"@modelcontextprotocol/sdk/types.js\";\r\nimport { KolmoPdfClient } from \"./client.js\";\r\nimport { loadConfig } from \"./config.js\";\r\nimport { type McpSuccessResult, type ToolContext, jsonResult } from \"./context.js\";\r\nimport { KolmoPdfError, toMcpErrorResult } from \"./errors.js\";\r\nimport {\r\n checkBalanceDescription,\r\n checkBalanceHandler,\r\n checkBalanceInputSchema,\r\n checkBalanceName,\r\n} from \"./tools/check-balance.js\";\r\nimport {\r\n convertDescription,\r\n convertHandler,\r\n convertInputSchema,\r\n convertName,\r\n} from \"./tools/convert.js\";\r\nimport {\r\n estimateCostDescription,\r\n estimateCostHandler,\r\n estimateCostInputSchema,\r\n estimateCostName,\r\n} from \"./tools/estimate-cost.js\";\r\nimport {\r\n getTaskStatusDescription,\r\n getTaskStatusHandler,\r\n getTaskStatusInputSchema,\r\n getTaskStatusName,\r\n} from \"./tools/get-task-status.js\";\r\nimport {\r\n parsePdfDescription,\r\n parsePdfHandler,\r\n parsePdfInputSchema,\r\n parsePdfName,\r\n} from \"./tools/parse-pdf.js\";\r\nimport {\r\n translatePdfDescription,\r\n translatePdfHandler,\r\n translatePdfInputSchema,\r\n translatePdfName,\r\n} from \"./tools/translate-pdf.js\";\r\n\r\nconst VERSION = \"1.0.0\";\r\n\r\n/** Build the per-call tool context with a lazily-constructed API client. */\r\nfunction buildContext(): ToolContext {\r\n const config = loadConfig();\r\n return {\r\n config,\r\n getClient(): KolmoPdfClient {\r\n if (!config.apiKey) {\r\n throw new KolmoPdfError(\"invalid_api_key\");\r\n }\r\n return new KolmoPdfClient({\r\n apiKey: config.apiKey,\r\n baseUrl: config.baseUrl,\r\n httpTimeoutMs: config.httpTimeoutMs,\r\n uploadTimeoutMs: config.uploadTimeoutMs,\r\n });\r\n },\r\n };\r\n}\r\n\r\n/** Wrap a typed handler so all thrown errors become MCP error results (§5.13). */\r\nfunction guard<A>(\r\n handler: (args: A, ctx: ToolContext) => Promise<McpSuccessResult>,\r\n): (args: unknown) => Promise<CallToolResult> {\r\n return async (args: unknown) => {\r\n try {\r\n return (await handler(args as A, buildContext())) as CallToolResult;\r\n } catch (err) {\r\n return toMcpErrorResult(err) as CallToolResult;\r\n }\r\n };\r\n}\r\n\r\nexport function createServer(): McpServer {\r\n const server = new McpServer({ name: \"kolmopdf\", version: VERSION });\r\n\r\n server.registerTool(\r\n parsePdfName,\r\n { description: parsePdfDescription, inputSchema: parsePdfInputSchema.shape },\r\n guard(parsePdfHandler),\r\n );\r\n server.registerTool(\r\n translatePdfName,\r\n { description: translatePdfDescription, inputSchema: translatePdfInputSchema.shape },\r\n guard(translatePdfHandler),\r\n );\r\n server.registerTool(\r\n convertName,\r\n { description: convertDescription, inputSchema: convertInputSchema.shape },\r\n guard(convertHandler),\r\n );\r\n server.registerTool(\r\n estimateCostName,\r\n { description: estimateCostDescription, inputSchema: estimateCostInputSchema.shape },\r\n guard(estimateCostHandler),\r\n );\r\n server.registerTool(\r\n checkBalanceName,\r\n { description: checkBalanceDescription, inputSchema: checkBalanceInputSchema.shape },\r\n guard(checkBalanceHandler),\r\n );\r\n server.registerTool(\r\n getTaskStatusName,\r\n { description: getTaskStatusDescription, inputSchema: getTaskStatusInputSchema.shape },\r\n guard(getTaskStatusHandler),\r\n );\r\n\r\n return server;\r\n}\r\n\r\nasync function main(): Promise<void> {\r\n // Surface --version without booting the transport (TESTING_AND_USAGE.md §9).\r\n if (process.argv.includes(\"--version\") || process.argv.includes(\"-v\")) {\r\n process.stdout.write(`${VERSION}\\n`);\r\n return;\r\n }\r\n const server = createServer();\r\n const transport = new StdioServerTransport();\r\n await server.connect(transport);\r\n}\r\n\r\n// `jsonResult` is part of the public surface used by tool handlers (M2+).\r\nexport { jsonResult };\r\n\r\nmain().catch((err) => {\r\n const detail = err instanceof Error ? err.stack : String(err);\r\n process.stderr.write(`[kolmopdf-mcp] fatal: ${detail}\\n`);\r\n process.exit(1);\r\n});\r\n","import type { Writable } from \"node:stream\";\r\nimport { Readable } from \"node:stream\";\r\nimport { pipeline } from \"node:stream/promises\";\r\nimport { KolmoPdfError, errorFromApiBody } from \"./errors.js\";\r\n\r\nexport interface KolmoPdfClientOptions {\r\n apiKey: string;\r\n baseUrl: string;\r\n httpTimeoutMs: number;\r\n uploadTimeoutMs: number;\r\n}\r\n\r\nexport interface ParseForm {\r\n table_mode?: \"markdown\" | \"image\";\r\n formula_format?: \"dollar\" | \"bracket\";\r\n enable_translation?: boolean;\r\n target_language?: string;\r\n output_options?: string[];\r\n images_as_url?: boolean;\r\n skip_rotation_detection?: boolean;\r\n enable_cross_page_merge?: boolean;\r\n}\r\n\r\nexport interface TranslateForm {\r\n source_language?: string;\r\n target_language?: string;\r\n layout_modes?: Array<\"translated_only\" | \"side_by_side\">;\r\n enable_image_translation?: boolean;\r\n enable_table_translation?: boolean;\r\n}\r\n\r\nexport interface ConvertForm {\r\n target_format?: string;\r\n}\r\n\r\nexport type TaskStatus = \"pending\" | \"waiting\" | \"processing\" | \"completed\" | \"failed\";\r\n\r\nexport interface SubmitResult {\r\n task_id: string;\r\n status: TaskStatus | string;\r\n points_deducted: number;\r\n remaining_points: number;\r\n queue_info?: { position: number; ahead_tasks: number };\r\n}\r\n\r\nexport interface StatusResult {\r\n success: boolean;\r\n status: TaskStatus | string;\r\n message?: string;\r\n queue_info?: { position: number; ahead_tasks: number };\r\n error_code?: string;\r\n result?: { task_id: string; download_url: string };\r\n}\r\n\r\nexport interface DownloadMeta {\r\n contentType: string | null;\r\n isZip: boolean;\r\n bytesWritten: number;\r\n}\r\n\r\nexport interface BalanceResult {\r\n success: boolean;\r\n points: number;\r\n api_key: string;\r\n}\r\n\r\nexport type FileInput = Buffer | NodeJS.ReadableStream;\r\n\r\nexport class KolmoPdfClient {\r\n private readonly apiKey: string;\r\n private readonly baseUrl: string;\r\n private readonly httpTimeoutMs: number;\r\n private readonly uploadTimeoutMs: number;\r\n\r\n constructor(opts: KolmoPdfClientOptions) {\r\n this.apiKey = opts.apiKey;\r\n this.baseUrl = opts.baseUrl;\r\n this.httpTimeoutMs = opts.httpTimeoutMs;\r\n this.uploadTimeoutMs = opts.uploadTimeoutMs;\r\n }\r\n\r\n private get apiBase(): string {\r\n return `${this.baseUrl}/api/pdf-to-markdown-proxy`;\r\n }\r\n\r\n private headers(): Record<string, string> {\r\n return { \"X-API-Key\": this.apiKey };\r\n }\r\n\r\n private async jsonRequest(url: string, init: RequestInit): Promise<unknown> {\r\n const res = await fetch(url, init);\r\n const body = (await res.json()) as Record<string, unknown>;\r\n if (!res.ok || body.success === false) {\r\n throw errorFromApiBody(\r\n body as {\r\n error_code?: string;\r\n message?: string;\r\n points_required?: number;\r\n current_points?: number;\r\n },\r\n res.status,\r\n );\r\n }\r\n return body;\r\n }\r\n\r\n private async buildFileForm(file: FileInput, filename: string): Promise<FormData> {\r\n const form = new FormData();\r\n let blob: Blob;\r\n if (Buffer.isBuffer(file)) {\r\n blob = new Blob([file]);\r\n } else {\r\n const chunks: Buffer[] = [];\r\n for await (const chunk of file) {\r\n chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));\r\n }\r\n blob = new Blob([Buffer.concat(chunks)]);\r\n }\r\n form.append(\"file\", blob, filename);\r\n return form;\r\n }\r\n\r\n async parse(file: FileInput, form: ParseForm, filename: string): Promise<SubmitResult> {\r\n const fd = await this.buildFileForm(file, filename);\r\n if (form.table_mode) fd.append(\"table_mode\", form.table_mode);\r\n if (form.formula_format) fd.append(\"formula_format\", form.formula_format);\r\n if (form.enable_translation !== undefined)\r\n fd.append(\"enable_translation\", String(form.enable_translation));\r\n if (form.target_language) fd.append(\"target_language\", form.target_language);\r\n if (form.output_options?.length) fd.append(\"output_options\", form.output_options.join(\",\"));\r\n if (form.images_as_url !== undefined) fd.append(\"images_as_url\", String(form.images_as_url));\r\n if (form.skip_rotation_detection !== undefined)\r\n fd.append(\"skip_rotation_detection\", String(form.skip_rotation_detection));\r\n if (form.enable_cross_page_merge !== undefined)\r\n fd.append(\"enable_cross_page_merge\", String(form.enable_cross_page_merge));\r\n\r\n const body = await this.jsonRequest(`${this.apiBase}/parse`, {\r\n method: \"POST\",\r\n headers: this.headers(),\r\n body: fd,\r\n signal: AbortSignal.timeout(this.uploadTimeoutMs),\r\n });\r\n return body as unknown as SubmitResult;\r\n }\r\n\r\n async translatePdf(\r\n file: FileInput,\r\n form: TranslateForm,\r\n filename: string,\r\n ): Promise<SubmitResult> {\r\n const fd = await this.buildFileForm(file, filename);\r\n if (form.source_language) fd.append(\"sourceLanguage\", form.source_language);\r\n if (form.target_language) fd.append(\"targetLanguage\", form.target_language);\r\n if (form.layout_modes?.length) fd.append(\"layoutModes\", form.layout_modes.join(\",\"));\r\n if (form.enable_image_translation !== undefined)\r\n fd.append(\"enableImageTranslation\", String(form.enable_image_translation));\r\n if (form.enable_table_translation !== undefined)\r\n fd.append(\"enableTableTranslation\", String(form.enable_table_translation));\r\n\r\n const body = await this.jsonRequest(`${this.apiBase}/translate-pdf`, {\r\n method: \"POST\",\r\n headers: this.headers(),\r\n body: fd,\r\n signal: AbortSignal.timeout(this.uploadTimeoutMs),\r\n });\r\n return body as unknown as SubmitResult;\r\n }\r\n\r\n async convert(file: FileInput, form: ConvertForm, filename: string): Promise<SubmitResult> {\r\n const fd = await this.buildFileForm(file, filename);\r\n if (form.target_format) fd.append(\"targetFormat\", form.target_format);\r\n\r\n const body = await this.jsonRequest(`${this.apiBase}/convert`, {\r\n method: \"POST\",\r\n headers: this.headers(),\r\n body: fd,\r\n signal: AbortSignal.timeout(this.uploadTimeoutMs),\r\n });\r\n return body as unknown as SubmitResult;\r\n }\r\n\r\n async getStatus(taskId: string): Promise<StatusResult> {\r\n const body = await this.jsonRequest(`${this.apiBase}/status/${taskId}`, {\r\n method: \"GET\",\r\n headers: this.headers(),\r\n signal: AbortSignal.timeout(this.httpTimeoutMs),\r\n });\r\n return body as unknown as StatusResult;\r\n }\r\n\r\n async download(taskId: string, dest: Writable): Promise<DownloadMeta> {\r\n const res = await fetch(`${this.apiBase}/download/${taskId}`, {\r\n method: \"GET\",\r\n headers: this.headers(),\r\n signal: AbortSignal.timeout(this.uploadTimeoutMs),\r\n });\r\n if (!res.ok) {\r\n throw new KolmoPdfError(\"api_task_error\", {\r\n message: `Download failed with HTTP ${res.status}`,\r\n httpStatus: res.status,\r\n });\r\n }\r\n const contentType = res.headers.get(\"content-type\");\r\n const isZip = contentType?.includes(\"zip\") || contentType?.includes(\"octet-stream\") || false;\r\n const body = res.body;\r\n if (!body) {\r\n throw new KolmoPdfError(\"api_task_error\", { message: \"Empty download response body\" });\r\n }\r\n\r\n const reader = body.getReader();\r\n let bytesWritten = 0;\r\n\r\n async function* generate() {\r\n while (true) {\r\n const { done, value } = await reader.read();\r\n if (done) break;\r\n bytesWritten += value.byteLength;\r\n yield Buffer.from(value);\r\n }\r\n }\r\n\r\n const readable = Readable.from(generate());\r\n await pipeline(readable, dest);\r\n return { contentType, isZip, bytesWritten };\r\n }\r\n\r\n async getBalance(): Promise<BalanceResult> {\r\n const body = await this.jsonRequest(`${this.apiBase}/balance`, {\r\n method: \"GET\",\r\n headers: this.headers(),\r\n signal: AbortSignal.timeout(this.httpTimeoutMs),\r\n });\r\n return body as unknown as BalanceResult;\r\n }\r\n}\r\n","/**\r\n * Unified KolmoPDF error model and MCP error-result formatting.\r\n *\r\n * Implements the error-code mapping table in DEVELOPMENT.md §8 and the\r\n * MCP tool error envelope in §5.13.\r\n */\r\n\r\nexport type ErrorSource = \"api\" | \"client\";\r\n\r\nexport interface ErrorSpec {\r\n /** Default human-readable message. */\r\n message: string;\r\n /** Actionable remediation hint surfaced to the LLM / user. */\r\n remediation: string;\r\n /** Typical HTTP status; null for client-side codes. */\r\n httpStatus: number | null;\r\n source: ErrorSource;\r\n}\r\n\r\n/** Canonical mapping of every error_code we may surface (DEVELOPMENT.md §8). */\r\nexport const ERROR_SPECS: Record<string, ErrorSpec> = {\r\n // --- API codes ---\r\n invalid_api_key: {\r\n message: \"API key is missing or invalid.\",\r\n remediation: \"Create a key at https://www.kolmopdf.com/api-keys (requires Plus/Pro).\",\r\n httpStatus: 401,\r\n source: \"api\",\r\n },\r\n insufficient_points: {\r\n message: \"Not enough credits.\",\r\n remediation: \"Top up at https://www.kolmopdf.com/subscription.\",\r\n httpStatus: 402,\r\n source: \"api\",\r\n },\r\n points_deduction_failed: {\r\n message: \"Credit deduction failed.\",\r\n remediation: \"Retry; if it persists contact support.\",\r\n httpStatus: 402,\r\n source: \"api\",\r\n },\r\n no_file_found: {\r\n message: \"Request missing file field.\",\r\n remediation: \"(internal) MCP server bug, please report.\",\r\n httpStatus: 400,\r\n source: \"api\",\r\n },\r\n parse_file_too_large: {\r\n message: \"PDF exceeds 300MB.\",\r\n remediation: \"Split the PDF locally.\",\r\n httpStatus: 400,\r\n source: \"api\",\r\n },\r\n parse_page_limit_exceeded: {\r\n message: \"PDF exceeds 800 pages.\",\r\n remediation: \"Split the PDF locally.\",\r\n httpStatus: 400,\r\n source: \"api\",\r\n },\r\n parse_file_not_pdf: {\r\n message: \"File is not a valid PDF.\",\r\n remediation: \"Upload a .pdf file.\",\r\n httpStatus: 400,\r\n source: \"api\",\r\n },\r\n translate_pdf_file_too_large: {\r\n message: \"PDF exceeds 300MB.\",\r\n remediation: \"Split the PDF locally.\",\r\n httpStatus: 400,\r\n source: \"api\",\r\n },\r\n translate_pdf_file_not_pdf: {\r\n message: \"File is not a valid PDF.\",\r\n remediation: \"Upload a .pdf file.\",\r\n httpStatus: 400,\r\n source: \"api\",\r\n },\r\n translate_pdf_page_limit_exceeded: {\r\n message: \"PDF exceeds 800 pages.\",\r\n remediation: \"Split the PDF locally.\",\r\n httpStatus: 400,\r\n source: \"api\",\r\n },\r\n convert_file_too_large: {\r\n message: \"File exceeds 300MB.\",\r\n remediation: \"Reduce file size.\",\r\n httpStatus: 400,\r\n source: \"api\",\r\n },\r\n convert_file_type_unsupported: {\r\n message: \"File must be .md / .markdown / .zip.\",\r\n remediation: \"Convert source to markdown first.\",\r\n httpStatus: 400,\r\n source: \"api\",\r\n },\r\n convert_target_format_unsupported: {\r\n message: \"Target format unsupported.\",\r\n remediation: \"Use word/docx/html/pdf/latex/tex.\",\r\n httpStatus: 400,\r\n source: \"api\",\r\n },\r\n file_upload_failed: {\r\n message: \"Upload to storage failed.\",\r\n remediation: \"Check network and retry.\",\r\n httpStatus: 500,\r\n source: \"api\",\r\n },\r\n task_creation_failed: {\r\n message: \"Task creation failed.\",\r\n remediation: \"Retry.\",\r\n httpStatus: 500,\r\n source: \"api\",\r\n },\r\n parse_error: {\r\n message: \"Parsing failed.\",\r\n remediation: \"Retry; if it persists, split and try again.\",\r\n httpStatus: 500,\r\n source: \"api\",\r\n },\r\n parse_file_invalid: {\r\n message: \"PDF is malformed.\",\r\n remediation: \"Re-export the PDF.\",\r\n httpStatus: 500,\r\n source: \"api\",\r\n },\r\n parse_timeout: {\r\n message: \"Server-side timeout.\",\r\n remediation: \"Split into smaller PDFs.\",\r\n httpStatus: 500,\r\n source: \"api\",\r\n },\r\n api_task_error: {\r\n message: \"Generic task error.\",\r\n remediation: \"Retry; if it persists contact support.\",\r\n httpStatus: 500,\r\n source: \"api\",\r\n },\r\n // --- client codes ---\r\n client_polling_timeout: {\r\n message: \"Local polling exceeded KOLMOPDF_MAX_POLL_MINUTES.\",\r\n remediation: \"Task may still be running. Use kolmopdf_get_task_status with task_id.\",\r\n httpStatus: null,\r\n source: \"client\",\r\n },\r\n client_network_error: {\r\n message: \"Network error after retries.\",\r\n remediation: \"Check network.\",\r\n httpStatus: null,\r\n source: \"client\",\r\n },\r\n client_local_validation: {\r\n message: \"Local pre-check failed (page count / file size).\",\r\n remediation: \"See message for the specific limit that was exceeded.\",\r\n httpStatus: null,\r\n source: \"client\",\r\n },\r\n client_extract_failed: {\r\n message: \"ZIP extraction failed.\",\r\n remediation: \"Check disk permissions on output dir.\",\r\n httpStatus: null,\r\n source: \"client\",\r\n },\r\n} as const;\r\n\r\nconst UNKNOWN_SPEC: ErrorSpec = {\r\n message: \"Unknown error.\",\r\n remediation: \"Retry; if it persists contact https://www.kolmopdf.com/contact.\",\r\n httpStatus: null,\r\n source: \"client\",\r\n};\r\n\r\nexport interface KolmoPdfErrorOptions {\r\n /** Override the default message from the spec. */\r\n message?: string;\r\n /** Override the default HTTP status from the spec. */\r\n httpStatus?: number | null;\r\n pointsRequired?: number;\r\n currentPoints?: number;\r\n /** Override the default remediation hint. */\r\n remediation?: string;\r\n}\r\n\r\n/** Structured error thrown across the MCP server; carries a stable error_code. */\r\nexport class KolmoPdfError extends Error {\r\n readonly errorCode: string;\r\n readonly httpStatus: number | null;\r\n readonly remediation: string;\r\n readonly pointsRequired: number | undefined;\r\n readonly currentPoints: number | undefined;\r\n readonly source: ErrorSource;\r\n\r\n constructor(errorCode: string, opts: KolmoPdfErrorOptions = {}) {\r\n const spec = ERROR_SPECS[errorCode] ?? UNKNOWN_SPEC;\r\n super(opts.message ?? spec.message);\r\n this.name = \"KolmoPdfError\";\r\n this.errorCode = errorCode;\r\n this.httpStatus = opts.httpStatus !== undefined ? opts.httpStatus : spec.httpStatus;\r\n this.remediation = opts.remediation ?? spec.remediation;\r\n this.pointsRequired = opts.pointsRequired;\r\n this.currentPoints = opts.currentPoints;\r\n this.source = spec.source;\r\n }\r\n}\r\n\r\n/** Shape of the JSON payload embedded in an MCP error result (DEVELOPMENT.md §5.13). */\r\nexport interface McpErrorPayload {\r\n error_code: string;\r\n message: string;\r\n http_status: number | null;\r\n points_required?: number;\r\n current_points?: number;\r\n remediation: string;\r\n}\r\n\r\n/** MCP tool result envelope for an error (matches MCP SDK `CallToolResult`). */\r\nexport interface McpErrorResult {\r\n isError: true;\r\n content: Array<{ type: \"text\"; text: string }>;\r\n}\r\n\r\n/** Convert a KolmoPdfError (or any error) into the MCP error result envelope. */\r\nexport function toMcpErrorResult(err: unknown): McpErrorResult {\r\n const kerr =\r\n err instanceof KolmoPdfError\r\n ? err\r\n : new KolmoPdfError(\"api_task_error\", {\r\n message: err instanceof Error ? err.message : String(err),\r\n });\r\n\r\n const payload: McpErrorPayload = {\r\n error_code: kerr.errorCode,\r\n message: kerr.message,\r\n http_status: kerr.httpStatus,\r\n remediation: kerr.remediation,\r\n };\r\n if (kerr.pointsRequired !== undefined) payload.points_required = kerr.pointsRequired;\r\n if (kerr.currentPoints !== undefined) payload.current_points = kerr.currentPoints;\r\n\r\n return {\r\n isError: true,\r\n content: [{ type: \"text\", text: JSON.stringify(payload) }],\r\n };\r\n}\r\n\r\n/** Whether an API failure with this code is auto-refunded server-side (§8). */\r\nexport function isAutoRefunded(errorCode: string): boolean {\r\n return (\r\n errorCode === \"task_creation_failed\" ||\r\n errorCode === \"parse_error\" ||\r\n errorCode === \"parse_file_invalid\" ||\r\n errorCode === \"parse_timeout\"\r\n );\r\n}\r\n\r\n/** Map a raw API JSON failure body to a KolmoPdfError. */\r\nexport function errorFromApiBody(\r\n body: {\r\n error_code?: string;\r\n message?: string;\r\n points_required?: number;\r\n current_points?: number;\r\n },\r\n httpStatus?: number,\r\n): KolmoPdfError {\r\n const code = body.error_code ?? \"api_task_error\";\r\n return new KolmoPdfError(code, {\r\n message: body.message,\r\n httpStatus: httpStatus ?? null,\r\n pointsRequired: body.points_required,\r\n currentPoints: body.current_points,\r\n });\r\n}\r\n","/**\r\n * Environment-variable loader for the KolmoPDF MCP server.\r\n *\r\n * Per DEVELOPMENT.md §4 and §5.2, the API key is NOT validated at startup —\r\n * it is read lazily so the server can boot in offline / no-key environments.\r\n * The first authenticated tool call surfaces a missing key as an MCP error.\r\n */\r\n\r\nexport interface KolmoPdfConfig {\r\n /** Resolved at call time; may be undefined until the user sets it. */\r\n apiKey: string | undefined;\r\n baseUrl: string;\r\n outputDir: string;\r\n pollIntervalMs: number;\r\n maxPollMinutes: number;\r\n httpTimeoutMs: number;\r\n uploadTimeoutMs: number;\r\n}\r\n\r\nconst DEFAULTS = {\r\n baseUrl: \"https://www.kolmopdf.com\",\r\n outputDir: \"./kolmopdf-output\",\r\n pollIntervalMs: 2000,\r\n maxPollMinutes: 30,\r\n httpTimeoutMs: 60_000,\r\n uploadTimeoutMs: 600_000,\r\n} as const;\r\n\r\nfunction intFromEnv(value: string | undefined, fallback: number): number {\r\n if (value === undefined || value.trim() === \"\") return fallback;\r\n const parsed = Number.parseInt(value, 10);\r\n return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;\r\n}\r\n\r\nfunction trimTrailingSlash(url: string): string {\r\n return url.replace(/\\/+$/, \"\");\r\n}\r\n\r\n/**\r\n * Build a config object from the current process environment.\r\n * Reads on every call so live env changes are picked up.\r\n */\r\nexport function loadConfig(env: NodeJS.ProcessEnv = process.env): KolmoPdfConfig {\r\n const apiKeyRaw = env.KOLMOPDF_API_KEY?.trim();\r\n return {\r\n apiKey: apiKeyRaw && apiKeyRaw.length > 0 ? apiKeyRaw : undefined,\r\n baseUrl: trimTrailingSlash(env.KOLMOPDF_BASE_URL?.trim() || DEFAULTS.baseUrl),\r\n outputDir: env.KOLMOPDF_OUTPUT_DIR?.trim() || DEFAULTS.outputDir,\r\n pollIntervalMs: intFromEnv(env.KOLMOPDF_POLL_INTERVAL_MS, DEFAULTS.pollIntervalMs),\r\n maxPollMinutes: intFromEnv(env.KOLMOPDF_MAX_POLL_MINUTES, DEFAULTS.maxPollMinutes),\r\n httpTimeoutMs: intFromEnv(env.KOLMOPDF_HTTP_TIMEOUT_MS, DEFAULTS.httpTimeoutMs),\r\n uploadTimeoutMs: intFromEnv(env.KOLMOPDF_UPLOAD_TIMEOUT_MS, DEFAULTS.uploadTimeoutMs),\r\n };\r\n}\r\n\r\n/** Mask an API key for display: first 6 + \"***\" + last 4 (DEVELOPMENT.md §5.8). */\r\nexport function maskApiKey(apiKey: string): string {\r\n if (apiKey.length <= 10) return \"***\";\r\n return `${apiKey.slice(0, 6)}***${apiKey.slice(-4)}`;\r\n}\r\n\r\nexport const configDefaults = DEFAULTS;\r\n","/**\r\n * Shared per-call context handed to every tool handler.\r\n */\r\nimport type { KolmoPdfClient } from \"./client.js\";\r\nimport type { KolmoPdfConfig } from \"./config.js\";\r\nimport type { ProgressReporter } from \"./progress.js\";\r\n\r\nexport interface ToolContext {\r\n config: KolmoPdfConfig;\r\n /** Lazily constructed; throws invalid_api_key when the key is absent. */\r\n getClient(): KolmoPdfClient;\r\n progress?: ProgressReporter;\r\n}\r\n\r\n/** Standard MCP success result envelope (subset of the SDK `CallToolResult`). */\r\nexport interface McpSuccessResult {\r\n content: Array<{ type: \"text\"; text: string }>;\r\n structuredContent?: Record<string, unknown>;\r\n}\r\n\r\n/** Wrap a JSON-serializable object into the MCP success envelope. */\r\nexport function jsonResult(data: Record<string, unknown>): McpSuccessResult {\r\n return {\r\n content: [{ type: \"text\", text: JSON.stringify(data, null, 2) }],\r\n structuredContent: data,\r\n };\r\n}\r\n","import { z } from \"zod\";\r\nimport { maskApiKey } from \"../config.js\";\r\nimport type { McpSuccessResult, ToolContext } from \"../context.js\";\r\nimport { jsonResult } from \"../context.js\";\r\n\r\nexport const checkBalanceName = \"kolmopdf_check_balance\";\r\n\r\nexport const checkBalanceDescription =\r\n \"Show the current KolmoPDF credit balance for the configured API key.\";\r\n\r\nexport const checkBalanceInputSchema = z.object({});\r\n\r\nexport type CheckBalanceInput = z.infer<typeof checkBalanceInputSchema>;\r\n\r\nexport interface CheckBalanceOutput {\r\n points: number;\r\n api_key_masked: string;\r\n}\r\n\r\nexport async function checkBalanceHandler(\r\n _args: CheckBalanceInput,\r\n ctx: ToolContext,\r\n): Promise<McpSuccessResult> {\r\n const client = ctx.getClient();\r\n const balance = await client.getBalance();\r\n\r\n const output: CheckBalanceOutput = {\r\n points: balance.points,\r\n api_key_masked: maskApiKey(ctx.config.apiKey || \"\"),\r\n };\r\n\r\n return jsonResult(output as unknown as Record<string, unknown>);\r\n}\r\n","import { createWriteStream, mkdirSync } from \"node:fs\";\r\nimport { readFile } from \"node:fs/promises\";\r\nimport { basename, join, resolve } from \"node:path\";\r\nimport { z } from \"zod\";\r\nimport type { McpSuccessResult, ToolContext } from \"../context.js\";\r\nimport { jsonResult } from \"../context.js\";\r\nimport { KolmoPdfError } from \"../errors.js\";\r\nimport { MAX_FILE_BYTES, readFileSize } from \"../pages.js\";\r\nimport { pollUntilComplete } from \"../polling.js\";\r\n\r\nexport const convertName = \"kolmopdf_convert_markdown\";\r\n\r\nexport const convertDescription =\r\n \"Convert a Markdown file (or a ZIP of markdown + images) to DOCX, HTML, PDF, \" +\r\n \"or LaTeX via KolmoPDF.\";\r\n\r\nexport const convertInputSchema = z.object({\r\n file_path: z\r\n .string()\r\n .describe(\"Path to a .md/.markdown file or .zip containing markdown + images.\"),\r\n target_format: z.enum([\"word\", \"docx\", \"html\", \"pdf\", \"latex\", \"tex\"]).optional().default(\"word\"),\r\n output_subdir: z.string().optional(),\r\n});\r\n\r\nexport type ConvertInput = z.infer<typeof convertInputSchema>;\r\n\r\nexport interface ConvertOutput {\r\n task_id: string;\r\n points_deducted: number;\r\n remaining_points: number;\r\n output: {\r\n output_path: string;\r\n target_format: string;\r\n };\r\n}\r\n\r\nexport function formatToExtension(targetFormat: string): string {\r\n switch (targetFormat) {\r\n case \"word\":\r\n case \"docx\":\r\n return \".docx\";\r\n case \"html\":\r\n return \".html\";\r\n case \"pdf\":\r\n return \".pdf\";\r\n case \"latex\":\r\n case \"tex\":\r\n return \".tex\";\r\n default:\r\n return \".out\";\r\n }\r\n}\r\n\r\nexport function normalizeFormat(targetFormat: string): string {\r\n switch (targetFormat) {\r\n case \"word\":\r\n case \"docx\":\r\n return \"docx\";\r\n case \"latex\":\r\n case \"tex\":\r\n return \"tex\";\r\n default:\r\n return targetFormat;\r\n }\r\n}\r\nexport async function convertHandler(\r\n args: ConvertInput,\r\n ctx: ToolContext,\r\n): Promise<McpSuccessResult> {\r\n const client = ctx.getClient();\r\n const filePath = resolve(args.file_path);\r\n const filename = basename(filePath);\r\n\r\n const fileSize = await readFileSize(filePath);\r\n if (fileSize > MAX_FILE_BYTES) {\r\n throw new KolmoPdfError(\"convert_file_too_large\");\r\n }\r\n\r\n const ext = filePath.toLowerCase();\r\n if (!ext.endsWith(\".md\") && !ext.endsWith(\".markdown\") && !ext.endsWith(\".zip\")) {\r\n throw new KolmoPdfError(\"convert_file_type_unsupported\");\r\n }\r\n\r\n await ctx.progress?.report(\"[uploading] Sending file for conversion...\");\r\n\r\n const fileBuffer = await readFile(filePath);\r\n const submitResult = await client.convert(\r\n fileBuffer,\r\n {\r\n target_format: args.target_format,\r\n },\r\n filename,\r\n );\r\n\r\n const taskId = submitResult.task_id;\r\n await ctx.progress?.report(`[submitted] Task ${taskId} created`);\r\n\r\n await pollUntilComplete({\r\n client,\r\n taskId,\r\n options: {\r\n pollIntervalMs: ctx.config.pollIntervalMs,\r\n maxPollMinutes: ctx.config.maxPollMinutes,\r\n },\r\n progress: ctx.progress,\r\n });\r\n\r\n await ctx.progress?.report(\"[downloading] Fetching converted file...\");\r\n\r\n const subdir = args.output_subdir || taskId;\r\n const outputRoot = resolve(ctx.config.outputDir, subdir);\r\n mkdirSync(outputRoot, { recursive: true });\r\n\r\n const outExt = formatToExtension(args.target_format);\r\n const outputPath = join(outputRoot, `result${outExt}`);\r\n const ws = createWriteStream(outputPath);\r\n await client.download(taskId, ws);\r\n\r\n const output: ConvertOutput = {\r\n task_id: taskId,\r\n points_deducted: submitResult.points_deducted,\r\n remaining_points: submitResult.remaining_points,\r\n output: {\r\n output_path: outputPath,\r\n target_format: normalizeFormat(args.target_format),\r\n },\r\n };\r\n\r\n return jsonResult(output as unknown as Record<string, unknown>);\r\n}\r\n","import { readFile, stat } from \"node:fs/promises\";\r\nimport { PDFDocument } from \"pdf-lib\";\r\n\r\nexport const MAX_PAGES = 800;\r\nexport const MAX_FILE_BYTES = 300 * 1024 * 1024;\r\n\r\nexport async function readPageCount(filePath: string): Promise<number> {\r\n const data = await readFile(filePath);\r\n const doc = await PDFDocument.load(data, { ignoreEncryption: true });\r\n return doc.getPageCount();\r\n}\r\n\r\nexport async function readFileSize(filePath: string): Promise<number> {\r\n const s = await stat(filePath);\r\n return s.size;\r\n}\r\n","/**\r\n * MCP progress-notification helper (DEVELOPMENT.md §5.11).\r\n *\r\n * `progress` must be monotonically increasing; `total` is omitted because the\r\n * API does not provide a precise percentage.\r\n */\r\n\r\n/** Minimal shape of the MCP request context used to emit notifications. */\r\nexport interface ProgressSink {\r\n /** Present only when the client supplied a progressToken in request `_meta`. */\r\n progressToken?: string | number;\r\n notify(notification: {\r\n method: \"notifications/progress\";\r\n params: {\r\n progressToken: string | number;\r\n progress: number;\r\n message?: string;\r\n };\r\n }): Promise<void>;\r\n}\r\n\r\n/** Stateful emitter that guarantees a monotonically increasing counter. */\r\nexport class ProgressReporter {\r\n private counter = 0;\r\n\r\n constructor(private readonly sink: ProgressSink | undefined) {}\r\n\r\n async report(message: string): Promise<void> {\r\n if (!this.sink || this.sink.progressToken === undefined) return;\r\n this.counter += 1;\r\n await this.sink.notify({\r\n method: \"notifications/progress\",\r\n params: {\r\n progressToken: this.sink.progressToken,\r\n progress: this.counter,\r\n message,\r\n },\r\n });\r\n }\r\n}\r\n\r\n/** Build a human-readable status line, e.g. \"[waiting] 3 tasks ahead\". */\r\nexport function humanizeStatus(status: string, aheadTasks?: number): string {\r\n if (status === \"waiting\" && typeof aheadTasks === \"number\") {\r\n return `[waiting] ${aheadTasks} tasks ahead`;\r\n }\r\n return `[${status}]`;\r\n}\r\n","import type { KolmoPdfClient, StatusResult } from \"./client.js\";\r\nimport { KolmoPdfError } from \"./errors.js\";\r\nimport { type ProgressReporter, humanizeStatus } from \"./progress.js\";\r\n\r\nexport interface PollOptions {\r\n pollIntervalMs: number;\r\n maxPollMinutes: number;\r\n}\r\n\r\nexport const TERMINAL_OK = \"completed\";\r\nexport const TERMINAL_FAIL = \"failed\";\r\nexport const IN_FLIGHT_STATUSES = new Set([\"pending\", \"waiting\", \"processing\"]);\r\n\r\nexport const RETRY_POLICY = {\r\n maxAttempts: 3,\r\n baseDelayMs: 1000,\r\n factor: 2,\r\n} as const;\r\n\r\nexport function backoffDelayMs(attempt: number): number {\r\n return RETRY_POLICY.baseDelayMs * RETRY_POLICY.factor ** (attempt - 1);\r\n}\r\n\r\nexport function isRetryable(err: { httpStatus?: number | null; code?: string }): boolean {\r\n const transientCodes = [\"ECONNRESET\", \"ETIMEDOUT\", \"ECONNREFUSED\", \"EAI_AGAIN\"];\r\n if (err.code && transientCodes.includes(err.code)) return true;\r\n if (typeof err.httpStatus === \"number\" && err.httpStatus >= 500) return true;\r\n return false;\r\n}\r\n\r\nexport interface PollContext {\r\n client: KolmoPdfClient;\r\n taskId: string;\r\n options: PollOptions;\r\n progress?: ProgressReporter;\r\n}\r\n\r\nfunction sleep(ms: number): Promise<void> {\r\n return new Promise((resolve) => setTimeout(resolve, ms));\r\n}\r\n\r\nasync function fetchStatusWithRetry(client: KolmoPdfClient, taskId: string): Promise<StatusResult> {\r\n for (let attempt = 1; attempt <= RETRY_POLICY.maxAttempts; attempt++) {\r\n try {\r\n return await client.getStatus(taskId);\r\n } catch (err) {\r\n const retryable =\r\n err instanceof KolmoPdfError\r\n ? isRetryable(err)\r\n : isRetryable({ code: (err as NodeJS.ErrnoException).code });\r\n if (!retryable || attempt === RETRY_POLICY.maxAttempts) throw err;\r\n await sleep(backoffDelayMs(attempt));\r\n }\r\n }\r\n throw new KolmoPdfError(\"client_network_error\");\r\n}\r\n\r\nexport async function pollUntilComplete(ctx: PollContext): Promise<StatusResult> {\r\n const { client, taskId, options, progress } = ctx;\r\n const deadline = Date.now() + options.maxPollMinutes * 60_000;\r\n\r\n while (true) {\r\n if (Date.now() > deadline) {\r\n throw new KolmoPdfError(\"client_polling_timeout\");\r\n }\r\n\r\n const result = await fetchStatusWithRetry(client, taskId);\r\n\r\n if (result.status === TERMINAL_OK) {\r\n await progress?.report(`[completed] Task ${taskId} done`);\r\n return result;\r\n }\r\n\r\n if (result.status === TERMINAL_FAIL) {\r\n throw new KolmoPdfError(result.error_code || \"api_task_error\", {\r\n message: result.message || \"Task failed\",\r\n });\r\n }\r\n\r\n const aheadTasks = result.queue_info?.ahead_tasks;\r\n await progress?.report(humanizeStatus(result.status as string, aheadTasks));\r\n await sleep(options.pollIntervalMs);\r\n }\r\n}\r\n","import { resolve } from \"node:path\";\r\nimport { z } from \"zod\";\r\nimport type { McpSuccessResult, ToolContext } from \"../context.js\";\r\nimport { jsonResult } from \"../context.js\";\r\nimport { readPageCount } from \"../pages.js\";\r\n\r\nexport const estimateCostName = \"kolmopdf_estimate_cost\";\r\n\r\nexport const estimateCostDescription =\r\n \"Estimate the credit cost of a KolmoPDF operation before running it. \" +\r\n \"Reads page count locally and checks the current balance. Does not spend credits.\";\r\n\r\nexport const estimateCostInputSchema = z.object({\r\n file_path: z.string(),\r\n operation: z.enum([\"parse\", \"parse_translate\", \"translate\", \"convert\"]),\r\n options: z\r\n .object({\r\n images_as_url: z.boolean().optional(),\r\n })\r\n .optional(),\r\n});\r\n\r\nexport type EstimateCostInput = z.infer<typeof estimateCostInputSchema>;\r\n\r\nexport type Operation = EstimateCostInput[\"operation\"];\r\n\r\nexport interface EstimateCostOutput {\r\n pages: number | null;\r\n estimated_credits: number;\r\n current_balance: number;\r\n sufficient: boolean;\r\n shortfall: number;\r\n recommendation: string;\r\n}\r\n\r\nexport function estimateCredits(operation: Operation, pages: number): number {\r\n switch (operation) {\r\n case \"parse\":\r\n return pages * 2;\r\n case \"parse_translate\":\r\n return pages * 3;\r\n case \"translate\":\r\n return pages * 2;\r\n case \"convert\":\r\n return 1;\r\n }\r\n}\r\n\r\nexport function buildRecommendation(shortfall: number): string {\r\n return shortfall > 0\r\n ? `Need top-up at https://www.kolmopdf.com/subscription (short by ${shortfall} credits).`\r\n : \"Sufficient\";\r\n}\r\n\r\nexport async function estimateCostHandler(\r\n args: EstimateCostInput,\r\n ctx: ToolContext,\r\n): Promise<McpSuccessResult> {\r\n const client = ctx.getClient();\r\n\r\n let pages: number | null = null;\r\n if (args.operation !== \"convert\") {\r\n const filePath = resolve(args.file_path);\r\n pages = await readPageCount(filePath);\r\n }\r\n\r\n const estimatedCredits = estimateCredits(args.operation, pages ?? 1);\r\n const balance = await client.getBalance();\r\n const currentBalance = balance.points;\r\n const shortfall = Math.max(0, estimatedCredits - currentBalance);\r\n\r\n const output: EstimateCostOutput = {\r\n pages,\r\n estimated_credits: estimatedCredits,\r\n current_balance: currentBalance,\r\n sufficient: shortfall === 0,\r\n shortfall,\r\n recommendation: buildRecommendation(shortfall),\r\n };\r\n\r\n return jsonResult(output as unknown as Record<string, unknown>);\r\n}\r\n","import { z } from \"zod\";\r\nimport type { McpSuccessResult, ToolContext } from \"../context.js\";\r\nimport { jsonResult } from \"../context.js\";\r\n\r\nexport const getTaskStatusName = \"kolmopdf_get_task_status\";\r\n\r\nexport const getTaskStatusDescription =\r\n \"Advanced/debug tool. Returns raw status for a KolmoPDF task. Use only when \" +\r\n \"explicitly asked to inspect a task by ID, or when troubleshooting a stuck task.\";\r\n\r\nexport const getTaskStatusInputSchema = z.object({\r\n task_id: z.string(),\r\n});\r\n\r\nexport type GetTaskStatusInput = z.infer<typeof getTaskStatusInputSchema>;\r\n\r\nexport async function getTaskStatusHandler(\r\n args: GetTaskStatusInput,\r\n ctx: ToolContext,\r\n): Promise<McpSuccessResult> {\r\n const client = ctx.getClient();\r\n const status = await client.getStatus(args.task_id);\r\n return jsonResult(status as unknown as Record<string, unknown>);\r\n}\r\n","import { createWriteStream, mkdirSync } from \"node:fs\";\r\nimport { readFile } from \"node:fs/promises\";\r\nimport { basename, join, resolve } from \"node:path\";\r\nimport { z } from \"zod\";\r\nimport type { McpSuccessResult, ToolContext } from \"../context.js\";\r\nimport { jsonResult } from \"../context.js\";\r\nimport { KolmoPdfError } from \"../errors.js\";\r\nimport { extractZip } from \"../extract.js\";\r\nimport { MAX_FILE_BYTES, MAX_PAGES, readFileSize, readPageCount } from \"../pages.js\";\r\nimport { pollUntilComplete } from \"../polling.js\";\r\n\r\nexport const parsePdfName = \"kolmopdf_parse_pdf\";\r\n\r\nexport const parsePdfDescription =\r\n \"Parse a local PDF into Markdown via KolmoPDF. Handles formulas, tables, \" +\r\n \"multi-column layouts, and code blocks. Optionally translates while parsing.\";\r\n\r\nexport const parsePdfInputSchema = z.object({\r\n file_path: z.string().describe(\"Absolute or cwd-relative path to a local PDF file.\"),\r\n table_mode: z.enum([\"markdown\", \"image\"]).optional(),\r\n formula_format: z.enum([\"dollar\", \"bracket\"]).optional(),\r\n enable_translation: z.boolean().optional(),\r\n target_language: z.enum([\"zh\", \"en\", \"ja\", \"ko\", \"fr\", \"de\", \"es\", \"ru\"]).optional(),\r\n output_options: z.array(z.enum([\"original\", \"translated\", \"bilingual\"])).optional(),\r\n images_as_url: z.boolean().optional(),\r\n skip_rotation_detection: z.boolean().optional(),\r\n enable_cross_page_merge: z.boolean().optional(),\r\n output_subdir: z\r\n .string()\r\n .optional()\r\n .describe(\"Subdirectory name under KOLMOPDF_OUTPUT_DIR. Defaults to <task_id>.\"),\r\n});\r\n\r\nexport type ParsePdfInput = z.infer<typeof parsePdfInputSchema>;\r\n\r\nexport interface ParsePdfOutput {\r\n task_id: string;\r\n pages_parsed: number;\r\n points_deducted: number;\r\n remaining_points: number;\r\n output: {\r\n type: \"zip_extracted\" | \"markdown_file\";\r\n markdown_path: string;\r\n images_dir: string | null;\r\n output_root: string;\r\n };\r\n preview: string;\r\n}\r\n\r\nexport async function parsePdfHandler(\r\n args: ParsePdfInput,\r\n ctx: ToolContext,\r\n): Promise<McpSuccessResult> {\r\n const client = ctx.getClient();\r\n const filePath = resolve(args.file_path);\r\n const filename = basename(filePath);\r\n\r\n const fileSize = await readFileSize(filePath);\r\n if (fileSize > MAX_FILE_BYTES) {\r\n throw new KolmoPdfError(\"parse_file_too_large\");\r\n }\r\n\r\n const pageCount = await readPageCount(filePath);\r\n if (pageCount > MAX_PAGES) {\r\n throw new KolmoPdfError(\"parse_page_limit_exceeded\");\r\n }\r\n\r\n await ctx.progress?.report(\"[uploading] Sending PDF to KolmoPDF...\");\r\n\r\n const fileBuffer = await readFile(filePath);\r\n const submitResult = await client.parse(\r\n fileBuffer,\r\n {\r\n table_mode: args.table_mode,\r\n formula_format: args.formula_format,\r\n enable_translation: args.enable_translation,\r\n target_language: args.target_language,\r\n output_options: args.output_options,\r\n images_as_url: args.images_as_url,\r\n skip_rotation_detection: args.skip_rotation_detection,\r\n enable_cross_page_merge: args.enable_cross_page_merge,\r\n },\r\n filename,\r\n );\r\n\r\n const taskId = submitResult.task_id;\r\n await ctx.progress?.report(`[submitted] Task ${taskId} created`);\r\n\r\n await pollUntilComplete({\r\n client,\r\n taskId,\r\n options: {\r\n pollIntervalMs: ctx.config.pollIntervalMs,\r\n maxPollMinutes: ctx.config.maxPollMinutes,\r\n },\r\n progress: ctx.progress,\r\n });\r\n\r\n await ctx.progress?.report(\"[downloading] Fetching result...\");\r\n\r\n const subdir = args.output_subdir || taskId;\r\n const outputRoot = resolve(ctx.config.outputDir, subdir);\r\n mkdirSync(outputRoot, { recursive: true });\r\n\r\n const isUrlMode = args.images_as_url === true;\r\n\r\n let markdownPath: string;\r\n let imagesDir: string | null = null;\r\n let outputType: \"zip_extracted\" | \"markdown_file\";\r\n\r\n if (isUrlMode) {\r\n markdownPath = join(outputRoot, \"result.md\");\r\n const ws = createWriteStream(markdownPath);\r\n await client.download(taskId, ws);\r\n outputType = \"markdown_file\";\r\n } else {\r\n const zipPath = join(outputRoot, \"result.zip\");\r\n const ws = createWriteStream(zipPath);\r\n await client.download(taskId, ws);\r\n const extracted = await extractZip(zipPath, outputRoot);\r\n markdownPath = extracted.markdownPath || join(outputRoot, \"result.md\");\r\n imagesDir = extracted.imagesDir;\r\n outputType = \"zip_extracted\";\r\n }\r\n\r\n const mdContent = await readFile(markdownPath, \"utf-8\").catch(() => \"\");\r\n const preview = mdContent.slice(0, 500);\r\n\r\n const ptsPerPage = args.enable_translation ? 3 : 2;\r\n const pagesParsed = Math.round(submitResult.points_deducted / ptsPerPage);\r\n\r\n const output: ParsePdfOutput = {\r\n task_id: taskId,\r\n pages_parsed: pagesParsed,\r\n points_deducted: submitResult.points_deducted,\r\n remaining_points: submitResult.remaining_points,\r\n output: {\r\n type: outputType,\r\n markdown_path: markdownPath,\r\n images_dir: imagesDir,\r\n output_root: outputRoot,\r\n },\r\n preview,\r\n };\r\n\r\n return jsonResult(output as unknown as Record<string, unknown>);\r\n}\r\n","import { createWriteStream, mkdirSync } from \"node:fs\";\r\nimport { dirname, join } from \"node:path\";\r\nimport { pipeline } from \"node:stream/promises\";\r\nimport { type Entry, type ZipFile, open as yauzlOpen } from \"yauzl\";\r\n\r\nexport interface ExtractResult {\r\n markdownPath: string | null;\r\n imagesDir: string | null;\r\n outputRoot: string;\r\n files: string[];\r\n}\r\n\r\nexport async function extractZip(zipPath: string, destDir: string): Promise<ExtractResult> {\r\n mkdirSync(destDir, { recursive: true });\r\n\r\n const zipFile = await openZip(zipPath);\r\n const files: string[] = [];\r\n let markdownPath: string | null = null;\r\n let imagesDir: string | null = null;\r\n\r\n for await (const entry of iterEntries(zipFile)) {\r\n const entryPath = join(destDir, entry.fileName);\r\n\r\n if (entry.fileName.endsWith(\"/\")) {\r\n mkdirSync(entryPath, { recursive: true });\r\n if (entry.fileName.includes(\"images\")) {\r\n imagesDir = entryPath;\r\n }\r\n continue;\r\n }\r\n\r\n mkdirSync(dirname(entryPath), { recursive: true });\r\n const readStream = await openReadStream(zipFile, entry);\r\n const writeStream = createWriteStream(entryPath);\r\n await pipeline(readStream, writeStream);\r\n files.push(entryPath);\r\n\r\n if (!markdownPath && /\\.md$/i.test(entry.fileName)) {\r\n markdownPath = entryPath;\r\n }\r\n if (!imagesDir && /images\\//i.test(entry.fileName)) {\r\n const prefix = entry.fileName.split(\"images/\")[0] ?? \"\";\r\n imagesDir = join(destDir, prefix, \"images\");\r\n }\r\n }\r\n\r\n return { markdownPath, imagesDir, outputRoot: destDir, files };\r\n}\r\nfunction openZip(path: string): Promise<ZipFile> {\r\n return new Promise((resolve, reject) => {\r\n yauzlOpen(path, { lazyEntries: true }, (err, zf) => {\r\n if (err || !zf) return reject(err ?? new Error(\"Failed to open zip\"));\r\n resolve(zf);\r\n });\r\n });\r\n}\r\n\r\nasync function* iterEntries(zipFile: ZipFile): AsyncGenerator<Entry> {\r\n let resolve: ((entry: Entry | null) => void) | null = null;\r\n const queue: (Entry | null)[] = [];\r\n\r\n zipFile.on(\"entry\", (entry: Entry) => {\r\n if (resolve) {\r\n const r = resolve;\r\n resolve = null;\r\n r(entry);\r\n } else {\r\n queue.push(entry);\r\n }\r\n });\r\n zipFile.on(\"end\", () => {\r\n if (resolve) {\r\n const r = resolve;\r\n resolve = null;\r\n r(null);\r\n } else {\r\n queue.push(null);\r\n }\r\n });\r\n\r\n zipFile.readEntry();\r\n while (true) {\r\n const entry =\r\n queue.length > 0\r\n ? (queue.shift() as Entry | null)\r\n : await new Promise<Entry | null>((r) => {\r\n resolve = r;\r\n });\r\n if (entry === null) break;\r\n yield entry;\r\n zipFile.readEntry();\r\n }\r\n}\r\n\r\nfunction openReadStream(zipFile: ZipFile, entry: Entry): Promise<NodeJS.ReadableStream> {\r\n return new Promise((resolve, reject) => {\r\n zipFile.openReadStream(entry, (err, stream) => {\r\n if (err || !stream) return reject(err ?? new Error(\"Failed to open entry stream\"));\r\n resolve(stream);\r\n });\r\n });\r\n}\r\n","import { createWriteStream, mkdirSync } from \"node:fs\";\r\nimport { readFile } from \"node:fs/promises\";\r\nimport { basename, join, resolve } from \"node:path\";\r\nimport { z } from \"zod\";\r\nimport type { McpSuccessResult, ToolContext } from \"../context.js\";\r\nimport { jsonResult } from \"../context.js\";\r\nimport { KolmoPdfError } from \"../errors.js\";\r\nimport { MAX_FILE_BYTES, MAX_PAGES, readFileSize, readPageCount } from \"../pages.js\";\r\nimport { pollUntilComplete } from \"../polling.js\";\r\n\r\nexport const translatePdfName = \"kolmopdf_translate_pdf\";\r\n\r\nexport const translatePdfDescription =\r\n \"Translate a PDF while preserving its original layout via KolmoPDF. \" +\r\n \"Produces a translated PDF (optionally side-by-side bilingual).\";\r\n\r\nexport const translatePdfInputSchema = z.object({\r\n file_path: z.string(),\r\n source_language: z.string().optional().default(\"en\"),\r\n target_language: z.string().optional().default(\"zh\"),\r\n layout_modes: z\r\n .array(z.enum([\"translated_only\", \"side_by_side\"]))\r\n .optional()\r\n .default([\"translated_only\"]),\r\n enable_image_translation: z.boolean().optional().default(false),\r\n enable_table_translation: z.boolean().optional().default(false),\r\n output_subdir: z.string().optional(),\r\n});\r\n\r\nexport type TranslatePdfInput = z.infer<typeof translatePdfInputSchema>;\r\n\r\nexport interface TranslatePdfOutput {\r\n task_id: string;\r\n pages_translated: number;\r\n points_deducted: number;\r\n remaining_points: number;\r\n output: {\r\n translated_pdf_path: string;\r\n };\r\n}\r\n\r\nexport async function translatePdfHandler(\r\n args: TranslatePdfInput,\r\n ctx: ToolContext,\r\n): Promise<McpSuccessResult> {\r\n const client = ctx.getClient();\r\n const filePath = resolve(args.file_path);\r\n const filename = basename(filePath);\r\n const fileSize = await readFileSize(filePath);\r\n if (fileSize > MAX_FILE_BYTES) {\r\n throw new KolmoPdfError(\"translate_pdf_file_too_large\");\r\n }\r\n\r\n const pageCount = await readPageCount(filePath);\r\n if (pageCount > MAX_PAGES) {\r\n throw new KolmoPdfError(\"translate_pdf_page_limit_exceeded\");\r\n }\r\n\r\n await ctx.progress?.report(\"[uploading] Sending PDF for translation...\");\r\n\r\n const fileBuffer = await readFile(filePath);\r\n const submitResult = await client.translatePdf(\r\n fileBuffer,\r\n {\r\n source_language: args.source_language,\r\n target_language: args.target_language,\r\n layout_modes: args.layout_modes,\r\n enable_image_translation: args.enable_image_translation,\r\n enable_table_translation: args.enable_table_translation,\r\n },\r\n filename,\r\n );\r\n\r\n const taskId = submitResult.task_id;\r\n await ctx.progress?.report(`[submitted] Task ${taskId} created`);\r\n\r\n await pollUntilComplete({\r\n client,\r\n taskId,\r\n options: {\r\n pollIntervalMs: ctx.config.pollIntervalMs,\r\n maxPollMinutes: ctx.config.maxPollMinutes,\r\n },\r\n progress: ctx.progress,\r\n });\r\n\r\n await ctx.progress?.report(\"[downloading] Fetching translated PDF...\");\r\n\r\n const subdir = args.output_subdir || taskId;\r\n const outputRoot = resolve(ctx.config.outputDir, subdir);\r\n mkdirSync(outputRoot, { recursive: true });\r\n\r\n const pdfPath = join(outputRoot, \"translated.pdf\");\r\n const ws = createWriteStream(pdfPath);\r\n await client.download(taskId, ws);\r\n\r\n const pagesTranslated = Math.round(submitResult.points_deducted / 2);\r\n\r\n const output: TranslatePdfOutput = {\r\n task_id: taskId,\r\n pages_translated: pagesTranslated,\r\n points_deducted: submitResult.points_deducted,\r\n remaining_points: submitResult.remaining_points,\r\n output: {\r\n translated_pdf_path: pdfPath,\r\n },\r\n };\r\n\r\n return jsonResult(output as unknown as Record<string, unknown>);\r\n}\r\n"],"mappings":"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","names":["readFile","z","resolve","z","readFile","resolve","z","z","resolve","z","z","createWriteStream","mkdirSync","readFile","basename","join","resolve","z","createWriteStream","mkdirSync","join","pipeline","resolve","z","resolve","basename","readFile","mkdirSync","join","createWriteStream","createWriteStream","mkdirSync","readFile","basename","join","resolve","z","z","resolve","basename","readFile","mkdirSync","join","createWriteStream"]}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@kolmopdf/mcp-server",
|
|
3
|
-
"version": "1.0.0",
|
|
4
|
-
"description": "MCP server for KolmoPDF — PDF parsing, layout-preserving translation, and Markdown format conversion.",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"bin": {
|
|
8
|
-
"kolmopdf-mcp": "./dist/index.js"
|
|
9
|
-
},
|
|
10
|
-
"engines": {
|
|
11
|
-
"node": ">=20"
|
|
12
|
-
},
|
|
13
|
-
"files": ["dist", "README.md", "LICENSE"],
|
|
14
|
-
"keywords": [
|
|
15
|
-
"mcp",
|
|
16
|
-
"model-context-protocol",
|
|
17
|
-
"claude",
|
|
18
|
-
"claude-code",
|
|
19
|
-
"codex",
|
|
20
|
-
"pdf",
|
|
21
|
-
"markdown",
|
|
22
|
-
"pdf-to-markdown",
|
|
23
|
-
"translation",
|
|
24
|
-
"kolmopdf"
|
|
25
|
-
],
|
|
26
|
-
"scripts": {
|
|
27
|
-
"build": "tsup",
|
|
28
|
-
"dev": "tsup --watch",
|
|
29
|
-
"typecheck": "tsc --noEmit",
|
|
30
|
-
"test": "vitest run",
|
|
31
|
-
"test:unit": "vitest run tests/unit",
|
|
32
|
-
"test:integration": "vitest run tests/integration",
|
|
33
|
-
"test:watch": "vitest"
|
|
34
|
-
},
|
|
35
|
-
"dependencies": {
|
|
36
|
-
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
37
|
-
"pdf-lib": "^1.17.1",
|
|
38
|
-
"undici": "^6.19.8",
|
|
39
|
-
"yauzl": "^3.1.3",
|
|
40
|
-
"zod": "^3.25.0"
|
|
41
|
-
},
|
|
42
|
-
"devDependencies": {
|
|
43
|
-
"@types/node": "^20.14.0",
|
|
44
|
-
"@types/yauzl": "^2.10.3",
|
|
45
|
-
"tsup": "^8.2.4",
|
|
46
|
-
"typescript": "^5.4.5",
|
|
47
|
-
"vitest": "^2.0.5"
|
|
48
|
-
},
|
|
49
|
-
"repository": {
|
|
50
|
-
"type": "git",
|
|
51
|
-
"url": "https://github.com/kolmopdf/claude-plugin.git",
|
|
52
|
-
"directory": "packages/mcp-server"
|
|
53
|
-
}
|
|
54
|
-
}
|
|
@@ -1,235 +0,0 @@
|
|
|
1
|
-
import type { Writable } from "node:stream";
|
|
2
|
-
import { Readable } from "node:stream";
|
|
3
|
-
import { pipeline } from "node:stream/promises";
|
|
4
|
-
import { KolmoPdfError, errorFromApiBody } from "./errors.js";
|
|
5
|
-
|
|
6
|
-
export interface KolmoPdfClientOptions {
|
|
7
|
-
apiKey: string;
|
|
8
|
-
baseUrl: string;
|
|
9
|
-
httpTimeoutMs: number;
|
|
10
|
-
uploadTimeoutMs: number;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface ParseForm {
|
|
14
|
-
table_mode?: "markdown" | "image";
|
|
15
|
-
formula_format?: "dollar" | "bracket";
|
|
16
|
-
enable_translation?: boolean;
|
|
17
|
-
target_language?: string;
|
|
18
|
-
output_options?: string[];
|
|
19
|
-
images_as_url?: boolean;
|
|
20
|
-
skip_rotation_detection?: boolean;
|
|
21
|
-
enable_cross_page_merge?: boolean;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface TranslateForm {
|
|
25
|
-
source_language?: string;
|
|
26
|
-
target_language?: string;
|
|
27
|
-
layout_modes?: Array<"translated_only" | "side_by_side">;
|
|
28
|
-
enable_image_translation?: boolean;
|
|
29
|
-
enable_table_translation?: boolean;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface ConvertForm {
|
|
33
|
-
target_format?: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export type TaskStatus = "pending" | "waiting" | "processing" | "completed" | "failed";
|
|
37
|
-
|
|
38
|
-
export interface SubmitResult {
|
|
39
|
-
task_id: string;
|
|
40
|
-
status: TaskStatus | string;
|
|
41
|
-
points_deducted: number;
|
|
42
|
-
remaining_points: number;
|
|
43
|
-
queue_info?: { position: number; ahead_tasks: number };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface StatusResult {
|
|
47
|
-
success: boolean;
|
|
48
|
-
status: TaskStatus | string;
|
|
49
|
-
message?: string;
|
|
50
|
-
queue_info?: { position: number; ahead_tasks: number };
|
|
51
|
-
error_code?: string;
|
|
52
|
-
result?: { task_id: string; download_url: string };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface DownloadMeta {
|
|
56
|
-
contentType: string | null;
|
|
57
|
-
isZip: boolean;
|
|
58
|
-
bytesWritten: number;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export interface BalanceResult {
|
|
62
|
-
success: boolean;
|
|
63
|
-
points: number;
|
|
64
|
-
api_key: string;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export type FileInput = Buffer | NodeJS.ReadableStream;
|
|
68
|
-
|
|
69
|
-
export class KolmoPdfClient {
|
|
70
|
-
private readonly apiKey: string;
|
|
71
|
-
private readonly baseUrl: string;
|
|
72
|
-
private readonly httpTimeoutMs: number;
|
|
73
|
-
private readonly uploadTimeoutMs: number;
|
|
74
|
-
|
|
75
|
-
constructor(opts: KolmoPdfClientOptions) {
|
|
76
|
-
this.apiKey = opts.apiKey;
|
|
77
|
-
this.baseUrl = opts.baseUrl;
|
|
78
|
-
this.httpTimeoutMs = opts.httpTimeoutMs;
|
|
79
|
-
this.uploadTimeoutMs = opts.uploadTimeoutMs;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
private get apiBase(): string {
|
|
83
|
-
return `${this.baseUrl}/api/pdf-to-markdown-proxy`;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
private headers(): Record<string, string> {
|
|
87
|
-
return { "X-API-Key": this.apiKey };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
private async jsonRequest(url: string, init: RequestInit): Promise<unknown> {
|
|
91
|
-
const res = await fetch(url, init);
|
|
92
|
-
const body = (await res.json()) as Record<string, unknown>;
|
|
93
|
-
if (!res.ok || body.success === false) {
|
|
94
|
-
throw errorFromApiBody(
|
|
95
|
-
body as {
|
|
96
|
-
error_code?: string;
|
|
97
|
-
message?: string;
|
|
98
|
-
points_required?: number;
|
|
99
|
-
current_points?: number;
|
|
100
|
-
},
|
|
101
|
-
res.status,
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
return body;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
private async buildFileForm(file: FileInput, filename: string): Promise<FormData> {
|
|
108
|
-
const form = new FormData();
|
|
109
|
-
let blob: Blob;
|
|
110
|
-
if (Buffer.isBuffer(file)) {
|
|
111
|
-
blob = new Blob([file]);
|
|
112
|
-
} else {
|
|
113
|
-
const chunks: Buffer[] = [];
|
|
114
|
-
for await (const chunk of file) {
|
|
115
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
116
|
-
}
|
|
117
|
-
blob = new Blob([Buffer.concat(chunks)]);
|
|
118
|
-
}
|
|
119
|
-
form.append("file", blob, filename);
|
|
120
|
-
return form;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
async parse(file: FileInput, form: ParseForm, filename: string): Promise<SubmitResult> {
|
|
124
|
-
const fd = await this.buildFileForm(file, filename);
|
|
125
|
-
if (form.table_mode) fd.append("table_mode", form.table_mode);
|
|
126
|
-
if (form.formula_format) fd.append("formula_format", form.formula_format);
|
|
127
|
-
if (form.enable_translation !== undefined)
|
|
128
|
-
fd.append("enable_translation", String(form.enable_translation));
|
|
129
|
-
if (form.target_language) fd.append("target_language", form.target_language);
|
|
130
|
-
if (form.output_options?.length) fd.append("output_options", form.output_options.join(","));
|
|
131
|
-
if (form.images_as_url !== undefined) fd.append("images_as_url", String(form.images_as_url));
|
|
132
|
-
if (form.skip_rotation_detection !== undefined)
|
|
133
|
-
fd.append("skip_rotation_detection", String(form.skip_rotation_detection));
|
|
134
|
-
if (form.enable_cross_page_merge !== undefined)
|
|
135
|
-
fd.append("enable_cross_page_merge", String(form.enable_cross_page_merge));
|
|
136
|
-
|
|
137
|
-
const body = await this.jsonRequest(`${this.apiBase}/parse`, {
|
|
138
|
-
method: "POST",
|
|
139
|
-
headers: this.headers(),
|
|
140
|
-
body: fd,
|
|
141
|
-
signal: AbortSignal.timeout(this.uploadTimeoutMs),
|
|
142
|
-
});
|
|
143
|
-
return body as unknown as SubmitResult;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
async translatePdf(
|
|
147
|
-
file: FileInput,
|
|
148
|
-
form: TranslateForm,
|
|
149
|
-
filename: string,
|
|
150
|
-
): Promise<SubmitResult> {
|
|
151
|
-
const fd = await this.buildFileForm(file, filename);
|
|
152
|
-
if (form.source_language) fd.append("sourceLanguage", form.source_language);
|
|
153
|
-
if (form.target_language) fd.append("targetLanguage", form.target_language);
|
|
154
|
-
if (form.layout_modes?.length) fd.append("layoutModes", form.layout_modes.join(","));
|
|
155
|
-
if (form.enable_image_translation !== undefined)
|
|
156
|
-
fd.append("enableImageTranslation", String(form.enable_image_translation));
|
|
157
|
-
if (form.enable_table_translation !== undefined)
|
|
158
|
-
fd.append("enableTableTranslation", String(form.enable_table_translation));
|
|
159
|
-
|
|
160
|
-
const body = await this.jsonRequest(`${this.apiBase}/translate-pdf`, {
|
|
161
|
-
method: "POST",
|
|
162
|
-
headers: this.headers(),
|
|
163
|
-
body: fd,
|
|
164
|
-
signal: AbortSignal.timeout(this.uploadTimeoutMs),
|
|
165
|
-
});
|
|
166
|
-
return body as unknown as SubmitResult;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
async convert(file: FileInput, form: ConvertForm, filename: string): Promise<SubmitResult> {
|
|
170
|
-
const fd = await this.buildFileForm(file, filename);
|
|
171
|
-
if (form.target_format) fd.append("targetFormat", form.target_format);
|
|
172
|
-
|
|
173
|
-
const body = await this.jsonRequest(`${this.apiBase}/convert`, {
|
|
174
|
-
method: "POST",
|
|
175
|
-
headers: this.headers(),
|
|
176
|
-
body: fd,
|
|
177
|
-
signal: AbortSignal.timeout(this.uploadTimeoutMs),
|
|
178
|
-
});
|
|
179
|
-
return body as unknown as SubmitResult;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
async getStatus(taskId: string): Promise<StatusResult> {
|
|
183
|
-
const body = await this.jsonRequest(`${this.apiBase}/status/${taskId}`, {
|
|
184
|
-
method: "GET",
|
|
185
|
-
headers: this.headers(),
|
|
186
|
-
signal: AbortSignal.timeout(this.httpTimeoutMs),
|
|
187
|
-
});
|
|
188
|
-
return body as unknown as StatusResult;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
async download(taskId: string, dest: Writable): Promise<DownloadMeta> {
|
|
192
|
-
const res = await fetch(`${this.apiBase}/download/${taskId}`, {
|
|
193
|
-
method: "GET",
|
|
194
|
-
headers: this.headers(),
|
|
195
|
-
signal: AbortSignal.timeout(this.uploadTimeoutMs),
|
|
196
|
-
});
|
|
197
|
-
if (!res.ok) {
|
|
198
|
-
throw new KolmoPdfError("api_task_error", {
|
|
199
|
-
message: `Download failed with HTTP ${res.status}`,
|
|
200
|
-
httpStatus: res.status,
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
const contentType = res.headers.get("content-type");
|
|
204
|
-
const isZip = contentType?.includes("zip") || contentType?.includes("octet-stream") || false;
|
|
205
|
-
const body = res.body;
|
|
206
|
-
if (!body) {
|
|
207
|
-
throw new KolmoPdfError("api_task_error", { message: "Empty download response body" });
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const reader = body.getReader();
|
|
211
|
-
let bytesWritten = 0;
|
|
212
|
-
|
|
213
|
-
async function* generate() {
|
|
214
|
-
while (true) {
|
|
215
|
-
const { done, value } = await reader.read();
|
|
216
|
-
if (done) break;
|
|
217
|
-
bytesWritten += value.byteLength;
|
|
218
|
-
yield Buffer.from(value);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const readable = Readable.from(generate());
|
|
223
|
-
await pipeline(readable, dest);
|
|
224
|
-
return { contentType, isZip, bytesWritten };
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
async getBalance(): Promise<BalanceResult> {
|
|
228
|
-
const body = await this.jsonRequest(`${this.apiBase}/balance`, {
|
|
229
|
-
method: "GET",
|
|
230
|
-
headers: this.headers(),
|
|
231
|
-
signal: AbortSignal.timeout(this.httpTimeoutMs),
|
|
232
|
-
});
|
|
233
|
-
return body as unknown as BalanceResult;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Environment-variable loader for the KolmoPDF MCP server.
|
|
3
|
-
*
|
|
4
|
-
* Per DEVELOPMENT.md §4 and §5.2, the API key is NOT validated at startup —
|
|
5
|
-
* it is read lazily so the server can boot in offline / no-key environments.
|
|
6
|
-
* The first authenticated tool call surfaces a missing key as an MCP error.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
export interface KolmoPdfConfig {
|
|
10
|
-
/** Resolved at call time; may be undefined until the user sets it. */
|
|
11
|
-
apiKey: string | undefined;
|
|
12
|
-
baseUrl: string;
|
|
13
|
-
outputDir: string;
|
|
14
|
-
pollIntervalMs: number;
|
|
15
|
-
maxPollMinutes: number;
|
|
16
|
-
httpTimeoutMs: number;
|
|
17
|
-
uploadTimeoutMs: number;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const DEFAULTS = {
|
|
21
|
-
baseUrl: "https://www.kolmopdf.com",
|
|
22
|
-
outputDir: "./kolmopdf-output",
|
|
23
|
-
pollIntervalMs: 2000,
|
|
24
|
-
maxPollMinutes: 30,
|
|
25
|
-
httpTimeoutMs: 60_000,
|
|
26
|
-
uploadTimeoutMs: 600_000,
|
|
27
|
-
} as const;
|
|
28
|
-
|
|
29
|
-
function intFromEnv(value: string | undefined, fallback: number): number {
|
|
30
|
-
if (value === undefined || value.trim() === "") return fallback;
|
|
31
|
-
const parsed = Number.parseInt(value, 10);
|
|
32
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function trimTrailingSlash(url: string): string {
|
|
36
|
-
return url.replace(/\/+$/, "");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Build a config object from the current process environment.
|
|
41
|
-
* Reads on every call so live env changes are picked up.
|
|
42
|
-
*/
|
|
43
|
-
export function loadConfig(env: NodeJS.ProcessEnv = process.env): KolmoPdfConfig {
|
|
44
|
-
const apiKeyRaw = env.KOLMOPDF_API_KEY?.trim();
|
|
45
|
-
return {
|
|
46
|
-
apiKey: apiKeyRaw && apiKeyRaw.length > 0 ? apiKeyRaw : undefined,
|
|
47
|
-
baseUrl: trimTrailingSlash(env.KOLMOPDF_BASE_URL?.trim() || DEFAULTS.baseUrl),
|
|
48
|
-
outputDir: env.KOLMOPDF_OUTPUT_DIR?.trim() || DEFAULTS.outputDir,
|
|
49
|
-
pollIntervalMs: intFromEnv(env.KOLMOPDF_POLL_INTERVAL_MS, DEFAULTS.pollIntervalMs),
|
|
50
|
-
maxPollMinutes: intFromEnv(env.KOLMOPDF_MAX_POLL_MINUTES, DEFAULTS.maxPollMinutes),
|
|
51
|
-
httpTimeoutMs: intFromEnv(env.KOLMOPDF_HTTP_TIMEOUT_MS, DEFAULTS.httpTimeoutMs),
|
|
52
|
-
uploadTimeoutMs: intFromEnv(env.KOLMOPDF_UPLOAD_TIMEOUT_MS, DEFAULTS.uploadTimeoutMs),
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Mask an API key for display: first 6 + "***" + last 4 (DEVELOPMENT.md §5.8). */
|
|
57
|
-
export function maskApiKey(apiKey: string): string {
|
|
58
|
-
if (apiKey.length <= 10) return "***";
|
|
59
|
-
return `${apiKey.slice(0, 6)}***${apiKey.slice(-4)}`;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export const configDefaults = DEFAULTS;
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared per-call context handed to every tool handler.
|
|
3
|
-
*/
|
|
4
|
-
import type { KolmoPdfClient } from "./client.js";
|
|
5
|
-
import type { KolmoPdfConfig } from "./config.js";
|
|
6
|
-
import type { ProgressReporter } from "./progress.js";
|
|
7
|
-
|
|
8
|
-
export interface ToolContext {
|
|
9
|
-
config: KolmoPdfConfig;
|
|
10
|
-
/** Lazily constructed; throws invalid_api_key when the key is absent. */
|
|
11
|
-
getClient(): KolmoPdfClient;
|
|
12
|
-
progress?: ProgressReporter;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** Standard MCP success result envelope (subset of the SDK `CallToolResult`). */
|
|
16
|
-
export interface McpSuccessResult {
|
|
17
|
-
content: Array<{ type: "text"; text: string }>;
|
|
18
|
-
structuredContent?: Record<string, unknown>;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** Wrap a JSON-serializable object into the MCP success envelope. */
|
|
22
|
-
export function jsonResult(data: Record<string, unknown>): McpSuccessResult {
|
|
23
|
-
return {
|
|
24
|
-
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
25
|
-
structuredContent: data,
|
|
26
|
-
};
|
|
27
|
-
}
|