mineru-mcp 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,27 +1,55 @@
1
1
  # mineru-mcp
2
2
 
3
- MCP server for [MinerU](https://mineru.net) document parsing API - optimized for Claude Code.
3
+ MCP server for [MinerU](https://mineru.net) document parsing API extract text, tables, and formulas from PDFs, DOCs, and images.
4
4
 
5
5
  ## Features
6
6
 
7
- - **4 optimized tools** with concise descriptions (~73% token reduction vs alternatives)
8
- - **VLM model support** (90%+ accuracy) and pipeline mode (faster)
9
- - **Page range selection** - parse specific pages only
10
- - **Batch processing** - up to 200 documents at once
11
- - **Pagination** - efficient handling of large batch results
7
+ - **VLM model** 90%+ accuracy for complex documents
8
+ - **Pipeline model** Fast processing for simple documents
9
+ - **Local file upload** Upload files from disk for batch parsing
10
+ - **Batch processing** Parse up to 200 documents at once
11
+ - **Download & rename** Extract markdown with original filenames
12
+ - **Page ranges** — Extract specific pages only
12
13
  - **109 language OCR** support
14
+ - **Optimized for Claude Code** — 73% token reduction vs alternatives
15
+
16
+ ## Tools
17
+
18
+ | Tool | Description |
19
+ |------|-------------|
20
+ | `mineru_parse` | Parse a document URL |
21
+ | `mineru_status` | Check task progress, get download URL |
22
+ | `mineru_batch` | Parse multiple URLs (max 200) |
23
+ | `mineru_batch_status` | Get batch results with pagination |
24
+ | `mineru_upload_batch` | Upload local files for batch parsing |
25
+ | `mineru_download_results` | Download results as named markdown files |
13
26
 
14
27
  ## Installation
15
28
 
16
- ### Claude Code
29
+ Requires [Node.js](https://nodejs.org/) 18+ and a [MinerU API key](https://mineru.net).
30
+
31
+ ### CLI Install (one-liner)
17
32
 
18
33
  ```bash
19
- claude mcp add mineru-mcp -e MINERU_API_KEY=your-api-key -- npx mineru-mcp
34
+ # Claude Code
35
+ claude mcp add mineru-mcp -e MINERU_API_KEY=your-api-key -- npx -y mineru-mcp
36
+
37
+ # Codex CLI (OpenAI)
38
+ codex mcp add mineru --env MINERU_API_KEY=your-api-key -- npx -y mineru-mcp
39
+
40
+ # Gemini CLI (Google)
41
+ gemini mcp add -e MINERU_API_KEY=your-api-key mineru npx -y mineru-mcp
20
42
  ```
21
43
 
22
44
  ### Claude Desktop
23
45
 
24
- Add to your Claude Desktop config:
46
+ Add to your `claude_desktop_config.json`:
47
+
48
+ | OS | Config path |
49
+ |----|-------------|
50
+ | macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |
51
+ | Windows | `%APPDATA%\Claude\claude_desktop_config.json` |
52
+ | Linux | `~/.config/Claude/claude_desktop_config.json` |
25
53
 
26
54
  ```json
27
55
  {
@@ -37,6 +65,158 @@ Add to your Claude Desktop config:
37
65
  }
38
66
  ```
39
67
 
68
+ ### VS Code
69
+
70
+ Add to `.vscode/mcp.json` (workspace) or open Command Palette > `MCP: Open User Configuration` (global):
71
+
72
+ ```json
73
+ {
74
+ "servers": {
75
+ "mineru": {
76
+ "command": "npx",
77
+ "args": ["-y", "mineru-mcp"],
78
+ "env": {
79
+ "MINERU_API_KEY": "your-api-key"
80
+ }
81
+ }
82
+ }
83
+ }
84
+ ```
85
+
86
+ > **Note**: VS Code uses `"servers"` as the top-level key, not `"mcpServers"`. Other VS Code forks (Trae, Void, PearAI, etc.) typically use this same format.
87
+
88
+ ### Cursor
89
+
90
+ Add to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project):
91
+
92
+ ```json
93
+ {
94
+ "mcpServers": {
95
+ "mineru": {
96
+ "command": "npx",
97
+ "args": ["-y", "mineru-mcp"],
98
+ "env": {
99
+ "MINERU_API_KEY": "your-api-key"
100
+ }
101
+ }
102
+ }
103
+ }
104
+ ```
105
+
106
+ ### Windsurf
107
+
108
+ Add to `~/.codeium/windsurf/mcp_config.json` (Windows: `%USERPROFILE%\.codeium\windsurf\mcp_config.json`):
109
+
110
+ ```json
111
+ {
112
+ "mcpServers": {
113
+ "mineru": {
114
+ "command": "npx",
115
+ "args": ["-y", "mineru-mcp"],
116
+ "env": {
117
+ "MINERU_API_KEY": "your-api-key"
118
+ }
119
+ }
120
+ }
121
+ }
122
+ ```
123
+
124
+ ### Cline
125
+
126
+ Open MCP Servers icon in Cline panel > Configure > Advanced MCP Settings, then add:
127
+
128
+ ```json
129
+ {
130
+ "mcpServers": {
131
+ "mineru": {
132
+ "command": "npx",
133
+ "args": ["-y", "mineru-mcp"],
134
+ "env": {
135
+ "MINERU_API_KEY": "your-api-key"
136
+ }
137
+ }
138
+ }
139
+ }
140
+ ```
141
+
142
+ ### Cherry Studio
143
+
144
+ In Settings > MCP Servers > Add Server, set Type to `STDIO`, Command to `npx`, Args to `-y mineru-mcp`, and add environment variable `MINERU_API_KEY`. Or paste in JSON/Code mode:
145
+
146
+ ```json
147
+ {
148
+ "mineru": {
149
+ "name": "MinerU",
150
+ "command": "npx",
151
+ "args": ["-y", "mineru-mcp"],
152
+ "env": {
153
+ "MINERU_API_KEY": "your-api-key"
154
+ },
155
+ "isActive": true
156
+ }
157
+ }
158
+ ```
159
+
160
+ ### Witsy
161
+
162
+ In Settings > MCP Servers, add a new server with Type: `stdio`, Command: `npx`, Args: `-y mineru-mcp`, and set environment variable `MINERU_API_KEY` to your API key.
163
+
164
+ ### Codex CLI (TOML config)
165
+
166
+ Alternatively, edit `~/.codex/config.toml` directly:
167
+
168
+ ```toml
169
+ [mcp_servers.mineru]
170
+ command = "npx"
171
+ args = ["-y", "mineru-mcp"]
172
+
173
+ [mcp_servers.mineru.env]
174
+ MINERU_API_KEY = "your-api-key"
175
+ ```
176
+
177
+ ### Gemini CLI (JSON config)
178
+
179
+ Alternatively, edit `~/.gemini/settings.json` directly:
180
+
181
+ ```json
182
+ {
183
+ "mcpServers": {
184
+ "mineru": {
185
+ "command": "npx",
186
+ "args": ["-y", "mineru-mcp"],
187
+ "env": {
188
+ "MINERU_API_KEY": "your-api-key"
189
+ }
190
+ }
191
+ }
192
+ }
193
+ ```
194
+
195
+ ### Windows
196
+
197
+ On Windows, `npx` requires a shell wrapper. Replace `"command": "npx"` with:
198
+
199
+ ```json
200
+ {
201
+ "command": "cmd",
202
+ "args": ["/c", "npx", "-y", "mineru-mcp"],
203
+ "env": {
204
+ "MINERU_API_KEY": "your-api-key"
205
+ }
206
+ }
207
+ ```
208
+
209
+ For CLI tools on Windows:
210
+
211
+ ```bash
212
+ claude mcp add mineru-mcp -e MINERU_API_KEY=your-api-key -- cmd /c npx -y mineru-mcp
213
+ codex mcp add mineru --env MINERU_API_KEY=your-api-key -- cmd /c npx -y mineru-mcp
214
+ ```
215
+
216
+ ### ChatGPT
217
+
218
+ ChatGPT only supports remote MCP servers over HTTPS — local stdio servers like this one are not directly supported. You would need to deploy behind a public URL with HTTP transport.
219
+
40
220
  ## Configuration
41
221
 
42
222
  | Environment Variable | Default | Description |
@@ -47,11 +227,9 @@ Add to your Claude Desktop config:
47
227
 
48
228
  Get your API key at [mineru.net](https://mineru.net)
49
229
 
50
- ## Tools
51
-
52
- ### `mineru_parse`
230
+ ## Usage
53
231
 
54
- Parse a single document URL.
232
+ ### Parse a single URL
55
233
 
56
234
  ```typescript
57
235
  mineru_parse({
@@ -66,9 +244,7 @@ mineru_parse({
66
244
  })
67
245
  ```
68
246
 
69
- ### `mineru_status`
70
-
71
- Check task progress and get download URL.
247
+ ### Check task progress
72
248
 
73
249
  ```typescript
74
250
  mineru_status({
@@ -79,9 +255,7 @@ mineru_status({
79
255
 
80
256
  **Concise output**: `done | abc-123 | https://cdn-mineru.../result.zip`
81
257
 
82
- ### `mineru_batch`
83
-
84
- Parse multiple documents in one batch (max 200).
258
+ ### Batch parse URLs
85
259
 
86
260
  ```typescript
87
261
  mineru_batch({
@@ -90,9 +264,7 @@ mineru_batch({
90
264
  })
91
265
  ```
92
266
 
93
- ### `mineru_batch_status`
94
-
95
- Get batch results with pagination.
267
+ ### Check batch progress
96
268
 
97
269
  ```typescript
98
270
  mineru_batch_status({
@@ -103,6 +275,41 @@ mineru_batch_status({
103
275
  })
104
276
  ```
105
277
 
278
+ ### Upload local files
279
+
280
+ ```typescript
281
+ mineru_upload_batch({
282
+ directory: "/path/to/pdfs", // scan directory for supported files
283
+ // OR
284
+ files: ["/path/to/doc1.pdf", "/path/to/doc2.pdf"], // explicit file list
285
+ model: "vlm", // optional
286
+ formula: true, // optional
287
+ table: true, // optional
288
+ language: "en", // optional
289
+ formats: ["html"] // optional
290
+ })
291
+ ```
292
+
293
+ Returns `batch_id` for tracking. Each file's original name is preserved via `data_id` (spaces become underscores).
294
+
295
+ ### Download results as markdown
296
+
297
+ ```typescript
298
+ mineru_download_results({
299
+ batch_id: "batch-123", // from mineru_upload_batch or mineru_batch
300
+ output_dir: "/path/to/output",
301
+ overwrite: false // optional: overwrite existing files
302
+ })
303
+ ```
304
+
305
+ Output filenames are derived from `data_id` (e.g., `my_paper_title.md`). Spaces in original filenames become underscores.
306
+
307
+ ### Typical local file workflow
308
+
309
+ ```
310
+ mineru_upload_batch → mineru_batch_status (poll) → mineru_download_results
311
+ ```
312
+
106
313
  ## Supported Formats
107
314
 
108
315
  - PDF, DOC, DOCX, PPT, PPTX
@@ -112,7 +319,7 @@ mineru_batch_status({
112
319
 
113
320
  - Single file: 200MB max, 600 pages max
114
321
  - Daily quota: 2000 pages at high priority
115
- - Batch: max 200 URLs per request
322
+ - Batch: max 200 files per request
116
323
 
117
324
  ## License
118
325
 
@@ -120,6 +327,6 @@ MIT
120
327
 
121
328
  ## Links
122
329
 
123
- - [MinerU](https://mineru.net) - Document parsing service
124
- - [MinerU GitHub](https://github.com/opendatalab/MinerU) - Open source version
125
- - [MCP Specification](https://modelcontextprotocol.io) - Model Context Protocol
330
+ - [MinerU](https://mineru.net) Document parsing service
331
+ - [MinerU GitHub](https://github.com/opendatalab/MinerU) Open source version
332
+ - [MCP Specification](https://modelcontextprotocol.io) Model Context Protocol
package/dist/index.d.ts CHANGED
@@ -56,4 +56,44 @@ export default function createServer({ config }: {
56
56
  } | undefined;
57
57
  } | undefined;
58
58
  }>;
59
+ export declare function createSandboxServer(): import("@modelcontextprotocol/sdk/server").Server<{
60
+ method: string;
61
+ params?: {
62
+ [x: string]: unknown;
63
+ task?: {
64
+ [x: string]: unknown;
65
+ ttl?: number | null | undefined;
66
+ pollInterval?: number | undefined;
67
+ } | undefined;
68
+ _meta?: {
69
+ [x: string]: unknown;
70
+ progressToken?: string | number | undefined;
71
+ "io.modelcontextprotocol/related-task"?: {
72
+ [x: string]: unknown;
73
+ taskId: string;
74
+ } | undefined;
75
+ } | undefined;
76
+ } | undefined;
77
+ }, {
78
+ method: string;
79
+ params?: {
80
+ [x: string]: unknown;
81
+ _meta?: {
82
+ [x: string]: unknown;
83
+ "io.modelcontextprotocol/related-task"?: {
84
+ [x: string]: unknown;
85
+ taskId: string;
86
+ } | undefined;
87
+ } | undefined;
88
+ } | undefined;
89
+ }, {
90
+ [x: string]: unknown;
91
+ _meta?: {
92
+ [x: string]: unknown;
93
+ "io.modelcontextprotocol/related-task"?: {
94
+ [x: string]: unknown;
95
+ taskId: string;
96
+ } | undefined;
97
+ } | undefined;
98
+ }>;
59
99
  export {};
package/dist/index.js CHANGED
@@ -3,6 +3,12 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
5
  import axios, { AxiosError } from "axios";
6
+ import { readFileSync, createWriteStream, readdirSync, statSync, mkdirSync, writeFileSync, existsSync, unlinkSync, rmSync, realpathSync } from "node:fs";
7
+ import { join, basename, extname } from "node:path";
8
+ import { execFileSync } from "node:child_process";
9
+ import { tmpdir } from "node:os";
10
+ import { randomBytes } from "node:crypto";
11
+ import { pipeline } from "node:stream/promises";
6
12
  // Configuration schema for Smithery
7
13
  export const configSchema = z.object({
8
14
  mineruApiKey: z.string().describe("MinerU API key from mineru.net"),
@@ -110,7 +116,7 @@ export default function createServer({ config }) {
110
116
  // Create MCP server
111
117
  const server = new McpServer({
112
118
  name: "mineru",
113
- version: "1.0.1",
119
+ version: "1.0.2",
114
120
  });
115
121
  // Tool 1: mineru_parse
116
122
  server.tool("mineru_parse", "Parse a document URL. Returns task_id to check status.", {
@@ -234,9 +240,262 @@ export default function createServer({ config }) {
234
240
  content: [{ type: "text", text }],
235
241
  };
236
242
  });
243
+ // Tool 5: mineru_upload_batch
244
+ server.tool("mineru_upload_batch", "Upload local files from a directory for batch parsing. Handles file upload to MinerU servers. Returns batch_id to track with mineru_batch_status. Use mineru_download_results to get named markdown files.", {
245
+ directory: z.string().optional().describe("Directory path containing PDF/DOC/PPT files"),
246
+ files: z.array(z.string()).optional().describe("Array of absolute file paths (alternative to directory)"),
247
+ model: z
248
+ .enum(["pipeline", "vlm"])
249
+ .optional()
250
+ .describe("pipeline=fast, vlm=90% accuracy"),
251
+ formula: z.boolean().optional().describe("Formula recognition"),
252
+ table: z.boolean().optional().describe("Table recognition"),
253
+ language: z.string().optional().describe("Language code: ch, en, etc"),
254
+ formats: z
255
+ .array(z.enum(["docx", "html", "latex"]))
256
+ .optional()
257
+ .describe("Extra export formats"),
258
+ }, async (params) => {
259
+ const supportedExts = new Set([".pdf", ".doc", ".docx", ".ppt", ".pptx", ".png", ".jpg", ".jpeg"]);
260
+ // Collect files
261
+ let filePaths = [];
262
+ if (params.files?.length) {
263
+ filePaths = params.files;
264
+ }
265
+ else if (params.directory) {
266
+ const dir = params.directory;
267
+ if (!existsSync(dir)) {
268
+ throw new Error(`Directory not found: ${dir}`);
269
+ }
270
+ const entries = readdirSync(dir);
271
+ filePaths = entries
272
+ .filter((f) => supportedExts.has(extname(f).toLowerCase()))
273
+ .map((f) => join(dir, f));
274
+ }
275
+ else {
276
+ throw new Error("Provide either 'directory' or 'files' parameter.");
277
+ }
278
+ if (filePaths.length === 0) {
279
+ throw new Error("No supported files found.");
280
+ }
281
+ if (filePaths.length > 200) {
282
+ throw new Error(`Found ${filePaths.length} files. Max 200 per batch. Filter or split.`);
283
+ }
284
+ // Validate files exist and build request with collision-safe data_ids
285
+ const fileEntries = [];
286
+ const usedDataIds = new Set();
287
+ for (const fp of filePaths) {
288
+ if (!existsSync(fp)) {
289
+ throw new Error(`File not found: ${fp}`);
290
+ }
291
+ const stats = statSync(fp);
292
+ if (stats.size > 200 * 1024 * 1024) {
293
+ throw new Error(`File too large (${(stats.size / 1024 / 1024).toFixed(0)}MB): ${basename(fp)}. Max 200MB.`);
294
+ }
295
+ const name = basename(fp);
296
+ let stem = name.replace(extname(name), "").replace(/[^a-zA-Z0-9_\-\.]/g, "_").slice(0, 128);
297
+ // Handle data_id collisions
298
+ let candidate = stem;
299
+ let counter = 1;
300
+ while (usedDataIds.has(candidate)) {
301
+ candidate = `${stem}_${counter++}`;
302
+ }
303
+ usedDataIds.add(candidate);
304
+ fileEntries.push({ name, data_id: candidate });
305
+ }
306
+ // Request upload URLs
307
+ const requestData = {
308
+ files: fileEntries,
309
+ model_version: params.model || defaultModel,
310
+ };
311
+ if (params.formula !== undefined)
312
+ requestData.enable_formula = params.formula;
313
+ if (params.table !== undefined)
314
+ requestData.enable_table = params.table;
315
+ if (params.language)
316
+ requestData.language = params.language;
317
+ if (params.formats?.length)
318
+ requestData.extra_formats = params.formats;
319
+ const result = await mineruRequest("/file-urls/batch", "POST", requestData);
320
+ if (result.file_urls.length !== filePaths.length) {
321
+ throw new Error(`Expected ${filePaths.length} upload URLs, got ${result.file_urls.length}`);
322
+ }
323
+ // Upload each file to presigned OSS URLs using native fetch
324
+ // Presigned URLs are signed WITHOUT Content-Type — axios force-adds it, so use fetch
325
+ const uploadResults = [];
326
+ for (let i = 0; i < filePaths.length; i++) {
327
+ const fp = filePaths[i];
328
+ const uploadUrl = result.file_urls[i];
329
+ const fileName = basename(fp);
330
+ try {
331
+ const fileData = readFileSync(fp);
332
+ const resp = await fetch(uploadUrl, {
333
+ method: "PUT",
334
+ body: fileData,
335
+ signal: AbortSignal.timeout(300_000),
336
+ });
337
+ if (!resp.ok) {
338
+ const body = await resp.text();
339
+ uploadResults.push(`FAIL: ${fileName} - HTTP ${resp.status}: ${body.slice(0, 200)}`);
340
+ }
341
+ else {
342
+ uploadResults.push(`OK: ${fileName}`);
343
+ }
344
+ }
345
+ catch (err) {
346
+ const msg = err instanceof Error ? err.message : String(err);
347
+ uploadResults.push(`FAIL: ${fileName} - ${msg}`);
348
+ }
349
+ }
350
+ const successCount = uploadResults.filter((r) => r.startsWith("OK")).length;
351
+ const failCount = uploadResults.filter((r) => r.startsWith("FAIL")).length;
352
+ let text = `Batch ${result.batch_id}: ${successCount} uploaded, ${failCount} failed.\n`;
353
+ text += `Parsing starts automatically. Use mineru_batch_status to track.\n`;
354
+ text += `Use mineru_download_results with this batch_id to get named .md files.\n`;
355
+ if (failCount > 0) {
356
+ text += `\nFailed uploads:\n${uploadResults.filter((r) => r.startsWith("FAIL")).join("\n")}`;
357
+ }
358
+ return {
359
+ content: [{ type: "text", text }],
360
+ };
361
+ });
362
+ // Tool 6: mineru_download_results
363
+ server.tool("mineru_download_results", "Download batch results and extract markdown files with original filenames. Downloads zips, extracts .md content, and saves as {original_name}.md in output directory.", {
364
+ batch_id: z.string().describe("Batch ID from mineru_upload_batch or mineru_batch"),
365
+ output_dir: z.string().describe("Directory to save markdown files"),
366
+ overwrite: z.boolean().optional().default(false).describe("Overwrite existing files"),
367
+ }, async (params) => {
368
+ // Check batch status
369
+ const batch = await mineruRequest(`/extract-results/batch/${params.batch_id}`);
370
+ const results = batch.extract_result;
371
+ const doneResults = results.filter((r) => r.state === "done" && r.full_zip_url);
372
+ const pendingResults = results.filter((r) => ["pending", "running", "converting"].includes(r.state));
373
+ const failedResults = results.filter((r) => r.state === "failed");
374
+ if (doneResults.length === 0 && pendingResults.length > 0) {
375
+ return {
376
+ content: [{
377
+ type: "text",
378
+ text: `Batch ${params.batch_id}: ${pendingResults.length} still processing, 0 done. Try again later.`,
379
+ }],
380
+ };
381
+ }
382
+ // Create output directory
383
+ mkdirSync(params.output_dir, { recursive: true });
384
+ const tmpBase = join(tmpdir(), `mineru-dl-${Date.now()}-${randomBytes(4).toString("hex")}`);
385
+ mkdirSync(tmpBase, { recursive: true });
386
+ const downloaded = [];
387
+ const errors = [];
388
+ // Depth-limited, symlink-safe .md file finder
389
+ const findMd = (dir, baseDir, depth = 0, maxDepth = 5) => {
390
+ if (depth > maxDepth)
391
+ return null;
392
+ const entries = readdirSync(dir, { withFileTypes: true });
393
+ for (const entry of entries) {
394
+ if (entry.isSymbolicLink())
395
+ continue; // skip symlinks (zip slip protection)
396
+ const fullPath = join(dir, entry.name);
397
+ if (entry.isFile() && entry.name.endsWith(".md")) {
398
+ // Verify resolved path stays within extraction dir
399
+ const realPath = realpathSync(fullPath);
400
+ if (!realPath.startsWith(realpathSync(baseDir)))
401
+ continue;
402
+ return fullPath;
403
+ }
404
+ if (entry.isDirectory()) {
405
+ const found = findMd(fullPath, baseDir, depth + 1, maxDepth);
406
+ if (found)
407
+ return found;
408
+ }
409
+ }
410
+ return null;
411
+ };
412
+ for (const r of doneResults) {
413
+ // Prefer data_id (set by us from original filename) over file_name (API-returned, can be stale)
414
+ const rawName = r.data_id || r.file_name || "unknown";
415
+ const safeName = basename(rawName).replace(/[^a-zA-Z0-9_\-\.\s]/g, "_");
416
+ const stem = safeName.replace(extname(safeName), "") || "unnamed";
417
+ const mdOutputPath = join(params.output_dir, `${stem}.md`);
418
+ if (!params.overwrite && existsSync(mdOutputPath)) {
419
+ downloaded.push(`SKIP: ${stem}.md (exists)`);
420
+ continue;
421
+ }
422
+ try {
423
+ // Download zip via streaming to avoid memory pressure
424
+ const zipPath = join(tmpBase, `${stem}.zip`);
425
+ const response = await axios.get(r.full_zip_url, {
426
+ responseType: "stream",
427
+ timeout: 120_000,
428
+ });
429
+ await pipeline(response.data, createWriteStream(zipPath));
430
+ // Extract zip using execFileSync (no shell injection)
431
+ const extractDir = join(tmpBase, stem);
432
+ mkdirSync(extractDir, { recursive: true });
433
+ try {
434
+ execFileSync("unzip", ["-o", "-q", zipPath, "-d", extractDir], {
435
+ timeout: 60_000,
436
+ });
437
+ }
438
+ catch (unzipErr) {
439
+ const msg = unzipErr instanceof Error ? unzipErr.message : String(unzipErr);
440
+ errors.push(`UNZIP_FAIL: ${safeName} - ${msg}`);
441
+ continue;
442
+ }
443
+ const mdFile = findMd(extractDir, extractDir);
444
+ if (mdFile) {
445
+ const mdContent = readFileSync(mdFile, "utf-8");
446
+ writeFileSync(mdOutputPath, mdContent, "utf-8");
447
+ downloaded.push(`OK: ${stem}.md`);
448
+ }
449
+ else {
450
+ errors.push(`NO_MD: ${safeName} - no .md file found in zip`);
451
+ }
452
+ // Cleanup zip
453
+ unlinkSync(zipPath);
454
+ }
455
+ catch (err) {
456
+ const msg = err instanceof Error ? err.message : String(err);
457
+ errors.push(`FAIL: ${safeName} - ${msg}`);
458
+ }
459
+ }
460
+ // Cleanup temp directory
461
+ try {
462
+ rmSync(tmpBase, { recursive: true, force: true });
463
+ }
464
+ catch { /* ignore cleanup errors */ }
465
+ let text = `Downloaded to: ${params.output_dir}\n`;
466
+ text += `Done: ${downloaded.filter((d) => d.startsWith("OK")).length}`;
467
+ text += ` | Skipped: ${downloaded.filter((d) => d.startsWith("SKIP")).length}`;
468
+ text += ` | Failed: ${errors.length}`;
469
+ if (pendingResults.length > 0) {
470
+ text += ` | Still processing: ${pendingResults.length}`;
471
+ }
472
+ if (failedResults.length > 0) {
473
+ text += ` | Parse failed: ${failedResults.length}`;
474
+ }
475
+ text += `\n\nFiles:\n${downloaded.join("\n")}`;
476
+ if (errors.length > 0) {
477
+ text += `\n\nErrors:\n${errors.join("\n")}`;
478
+ }
479
+ if (pendingResults.length > 0) {
480
+ text += `\n\nRe-run this tool to download remaining files once processing completes.`;
481
+ }
482
+ return {
483
+ content: [{ type: "text", text }],
484
+ };
485
+ });
237
486
  return server.server;
238
487
  }
239
- // STDIO mode for backward compatibility (npx, local dev)
488
+ // Sandbox server for Smithery scanning (no real credentials needed)
489
+ export function createSandboxServer() {
490
+ return createServer({
491
+ config: {
492
+ mineruApiKey: "sandbox-key",
493
+ mineruBaseUrl: "https://mineru.net/api/v4",
494
+ mineruDefaultModel: "pipeline",
495
+ },
496
+ });
497
+ }
498
+ // STDIO mode (npx, local dev, Claude Code)
240
499
  async function main() {
241
500
  const config = {
242
501
  mineruApiKey: process.env.MINERU_API_KEY || "",
@@ -248,7 +507,12 @@ async function main() {
248
507
  await server.connect(transport);
249
508
  console.error("MinerU MCP server running (stdio mode)");
250
509
  }
251
- main().catch((error) => {
252
- console.error("Fatal error:", error);
253
- process.exit(1);
254
- });
510
+ // Only run stdio when executed directly (not when imported by Smithery CLI)
511
+ const isDirectRun = process.argv[1] && (process.argv[1].endsWith('index.js') ||
512
+ process.argv[1].endsWith('index.ts'));
513
+ if (isDirectRun) {
514
+ main().catch((error) => {
515
+ console.error("Fatal error:", error);
516
+ process.exit(1);
517
+ });
518
+ }
package/dist/server.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * HTTP Server for MinerU MCP
4
- * Run with: npm run start:http or node dist/server.js
4
+ * Run with: bun run start:http or node dist/server.js
5
5
  */
6
6
  export {};
package/dist/server.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * HTTP Server for MinerU MCP
4
- * Run with: npm run start:http or node dist/server.js
4
+ * Run with: bun run start:http or node dist/server.js
5
5
  */
6
6
  import express from "express";
7
7
  import { randomUUID } from "node:crypto";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mineru-mcp",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "MCP server for MinerU document parsing API - optimized for Claude Code with 73% token reduction",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -18,7 +18,7 @@
18
18
  "start:http": "node dist/server.js",
19
19
  "dev": "tsx src/index.ts",
20
20
  "dev:http": "tsx src/server.ts",
21
- "prepublishOnly": "npm run build"
21
+ "prepublishOnly": "bun run build"
22
22
  },
23
23
  "keywords": [
24
24
  "mcp",
@@ -35,6 +35,7 @@
35
35
  ],
36
36
  "author": "Xule Lin",
37
37
  "license": "MIT",
38
+ "mcpName": "io.github.linxule/mineru",
38
39
  "repository": {
39
40
  "type": "git",
40
41
  "url": "git+https://github.com/linxule/mineru-mcp.git"