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 +233 -26
- package/dist/index.d.ts +40 -0
- package/dist/index.js +270 -6
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- package/package.json +3 -2
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
|
|
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
|
-
- **
|
|
8
|
-
- **
|
|
9
|
-
- **
|
|
10
|
-
- **Batch processing**
|
|
11
|
-
- **
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
##
|
|
51
|
-
|
|
52
|
-
### `mineru_parse`
|
|
230
|
+
## Usage
|
|
53
231
|
|
|
54
|
-
Parse a single
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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
|
|
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)
|
|
124
|
-
- [MinerU GitHub](https://github.com/opendatalab/MinerU)
|
|
125
|
-
- [MCP Specification](https://modelcontextprotocol.io)
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
process.
|
|
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
package/dist/server.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mineru-mcp",
|
|
3
|
-
"version": "1.0.
|
|
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": "
|
|
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"
|