@sylphx/pdf-reader-mcp 2.3.0 → 2.4.0
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 +53 -0
- package/dist/index.js +104 -5
- package/package.json +14 -11
package/README.md
CHANGED
|
@@ -171,6 +171,10 @@ Add to Cline's MCP settings:
|
|
|
171
171
|
1. Go to **Settings** → **AI** → **Manage MCP Servers** → **Add**
|
|
172
172
|
2. Command: `npx`, Args: `@sylphx/pdf-reader-mcp`
|
|
173
173
|
|
|
174
|
+
### Ontheia
|
|
175
|
+
|
|
176
|
+
Add the server in **Settings** → **MCP Servers** → **Add Server** with command `npx` and args `@sylphx/pdf-reader-mcp`. See [Ontheia's compatible MCP servers](https://docs.ontheia.ai/getting-started/03_compatible-mcp-servers/) for the full list.
|
|
177
|
+
|
|
174
178
|
### Smithery (One-click)
|
|
175
179
|
|
|
176
180
|
```bash
|
|
@@ -538,6 +542,55 @@ Response Order:
|
|
|
538
542
|
|
|
539
543
|
---
|
|
540
544
|
|
|
545
|
+
## 🔒 Security & Sandboxing
|
|
546
|
+
|
|
547
|
+
By default the server can read any local file the host process can access and fetch any HTTP(S) URL. When running outside a sandbox you should restrict it to a specific working set.
|
|
548
|
+
|
|
549
|
+
### Restricting filesystem access
|
|
550
|
+
|
|
551
|
+
Use `--allow-dir` (repeatable) or the `MCP_PDF_ALLOWED_DIRS` env var (`:` or `,` separated). Once set, all `path` sources must resolve inside one of the allowed directories — relative paths, absolute paths, and `..` traversal are all checked after resolution.
|
|
552
|
+
|
|
553
|
+
```bash
|
|
554
|
+
# CLI flags
|
|
555
|
+
npx @sylphx/pdf-reader-mcp --allow-dir=/srv/pdfs --allow-dir=/data/reports
|
|
556
|
+
|
|
557
|
+
# Environment
|
|
558
|
+
MCP_PDF_ALLOWED_DIRS="/srv/pdfs:/data/reports" npx @sylphx/pdf-reader-mcp
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
```json
|
|
562
|
+
{
|
|
563
|
+
"mcpServers": {
|
|
564
|
+
"pdf-reader": {
|
|
565
|
+
"command": "npx",
|
|
566
|
+
"args": ["@sylphx/pdf-reader-mcp", "--allow-dir=/srv/pdfs"]
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
### Disabling or restricting HTTP
|
|
573
|
+
|
|
574
|
+
```bash
|
|
575
|
+
# Block all URL sources
|
|
576
|
+
npx @sylphx/pdf-reader-mcp --no-http
|
|
577
|
+
MCP_PDF_ALLOW_HTTP=false npx @sylphx/pdf-reader-mcp
|
|
578
|
+
|
|
579
|
+
# Allowlist hosts (everything else rejected)
|
|
580
|
+
npx @sylphx/pdf-reader-mcp --allow-host=cdn.example.com --allow-host=files.internal
|
|
581
|
+
MCP_PDF_ALLOWED_HOSTS="cdn.example.com,files.internal" npx @sylphx/pdf-reader-mcp
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
| Setting | CLI flag | Environment variable | Default |
|
|
585
|
+
|---------|----------|----------------------|---------|
|
|
586
|
+
| Filesystem allowlist | `--allow-dir=<path>` (repeatable) | `MCP_PDF_ALLOWED_DIRS` (`:` or `,` separated) | unrestricted |
|
|
587
|
+
| Disable HTTP | `--no-http` | `MCP_PDF_ALLOW_HTTP=false` | enabled |
|
|
588
|
+
| HTTP host allowlist | `--allow-host=<host>` (repeatable) | `MCP_PDF_ALLOWED_HOSTS` (`,` separated) | any host |
|
|
589
|
+
|
|
590
|
+
Denied requests fail fast with an `Access denied` error before any disk read or network call.
|
|
591
|
+
|
|
592
|
+
---
|
|
593
|
+
|
|
541
594
|
## 🔧 Troubleshooting
|
|
542
595
|
|
|
543
596
|
### "Absolute paths are not allowed"
|
package/dist/index.js
CHANGED
|
@@ -319,6 +319,88 @@ import fs from "node:fs/promises";
|
|
|
319
319
|
import { createRequire } from "node:module";
|
|
320
320
|
import { getDocument } from "pdfjs-dist/legacy/build/pdf.mjs";
|
|
321
321
|
|
|
322
|
+
// src/utils/config.ts
|
|
323
|
+
import path from "node:path";
|
|
324
|
+
var splitList = (value, separators) => value.split(separators).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
325
|
+
var parseDirs = (values) => values.map((dir) => path.resolve(path.normalize(dir)));
|
|
326
|
+
var parseBool = (value, fallback) => {
|
|
327
|
+
if (value === undefined)
|
|
328
|
+
return fallback;
|
|
329
|
+
const v = value.trim().toLowerCase();
|
|
330
|
+
if (v === "false" || v === "0" || v === "no" || v === "off")
|
|
331
|
+
return false;
|
|
332
|
+
if (v === "true" || v === "1" || v === "yes" || v === "on")
|
|
333
|
+
return true;
|
|
334
|
+
return fallback;
|
|
335
|
+
};
|
|
336
|
+
var parseCliFlags = (argv) => {
|
|
337
|
+
const dirs = [];
|
|
338
|
+
const hosts = [];
|
|
339
|
+
let noHttp = false;
|
|
340
|
+
for (const arg of argv) {
|
|
341
|
+
if (arg.startsWith("--allow-dir=")) {
|
|
342
|
+
dirs.push(arg.slice("--allow-dir=".length));
|
|
343
|
+
} else if (arg.startsWith("--allow-host=")) {
|
|
344
|
+
hosts.push(arg.slice("--allow-host=".length).toLowerCase());
|
|
345
|
+
} else if (arg === "--no-http") {
|
|
346
|
+
noHttp = true;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return { dirs, hosts, noHttp };
|
|
350
|
+
};
|
|
351
|
+
var envList = (raw, separators, transform = (v) => v) => raw ? splitList(raw, separators).map(transform) : [];
|
|
352
|
+
var readSecurityConfig = (argv = process.argv.slice(2), env = process.env) => {
|
|
353
|
+
const cli = parseCliFlags(argv);
|
|
354
|
+
const envDirs = envList(env["MCP_PDF_ALLOWED_DIRS"], /[:,]/);
|
|
355
|
+
const envHosts = envList(env["MCP_PDF_ALLOWED_HOSTS"], /,/, (h) => h.toLowerCase());
|
|
356
|
+
const mergedDirs = [...cli.dirs, ...envDirs];
|
|
357
|
+
const mergedHosts = [...cli.hosts, ...envHosts];
|
|
358
|
+
return {
|
|
359
|
+
allowedDirs: mergedDirs.length > 0 ? parseDirs(mergedDirs) : null,
|
|
360
|
+
allowHttp: cli.noHttp ? false : parseBool(env["MCP_PDF_ALLOW_HTTP"], true),
|
|
361
|
+
allowedHosts: mergedHosts.length > 0 ? mergedHosts : null
|
|
362
|
+
};
|
|
363
|
+
};
|
|
364
|
+
var cached = null;
|
|
365
|
+
var getSecurityConfig = () => {
|
|
366
|
+
if (cached === null) {
|
|
367
|
+
cached = readSecurityConfig();
|
|
368
|
+
}
|
|
369
|
+
return cached;
|
|
370
|
+
};
|
|
371
|
+
var isPathAllowed = (absPath, allowedDirs) => {
|
|
372
|
+
if (allowedDirs === null)
|
|
373
|
+
return true;
|
|
374
|
+
if (allowedDirs.length === 0)
|
|
375
|
+
return false;
|
|
376
|
+
const normalized = path.resolve(absPath);
|
|
377
|
+
return allowedDirs.some((dir) => {
|
|
378
|
+
const rel = path.relative(dir, normalized);
|
|
379
|
+
if (rel === "")
|
|
380
|
+
return true;
|
|
381
|
+
if (rel.startsWith(".."))
|
|
382
|
+
return false;
|
|
383
|
+
if (path.isAbsolute(rel))
|
|
384
|
+
return false;
|
|
385
|
+
return true;
|
|
386
|
+
});
|
|
387
|
+
};
|
|
388
|
+
var isUrlAllowed = (urlString, config) => {
|
|
389
|
+
if (!config.allowHttp)
|
|
390
|
+
return false;
|
|
391
|
+
let parsed;
|
|
392
|
+
try {
|
|
393
|
+
parsed = new URL(urlString);
|
|
394
|
+
} catch {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
398
|
+
return false;
|
|
399
|
+
if (config.allowedHosts === null)
|
|
400
|
+
return true;
|
|
401
|
+
return config.allowedHosts.includes(parsed.hostname.toLowerCase());
|
|
402
|
+
};
|
|
403
|
+
|
|
322
404
|
// src/utils/errors.ts
|
|
323
405
|
class PdfError extends Error {
|
|
324
406
|
code;
|
|
@@ -330,20 +412,29 @@ class PdfError extends Error {
|
|
|
330
412
|
}
|
|
331
413
|
|
|
332
414
|
// src/utils/pathUtils.ts
|
|
333
|
-
import
|
|
415
|
+
import path2 from "node:path";
|
|
334
416
|
var PROJECT_ROOT = process.cwd();
|
|
335
417
|
var resolvePath = (userPath) => {
|
|
336
418
|
if (typeof userPath !== "string") {
|
|
337
419
|
throw new PdfError(-32602 /* InvalidParams */, "Path must be a string.");
|
|
338
420
|
}
|
|
339
|
-
const normalizedUserPath =
|
|
340
|
-
|
|
421
|
+
const normalizedUserPath = path2.normalize(userPath);
|
|
422
|
+
const resolved = path2.isAbsolute(normalizedUserPath) ? normalizedUserPath : path2.resolve(PROJECT_ROOT, normalizedUserPath);
|
|
423
|
+
const { allowedDirs } = getSecurityConfig();
|
|
424
|
+
if (!isPathAllowed(resolved, allowedDirs)) {
|
|
425
|
+
throw new PdfError(-32600 /* InvalidRequest */, `Access denied: path '${userPath}' is outside the allowed directories.`);
|
|
426
|
+
}
|
|
427
|
+
return resolved;
|
|
341
428
|
};
|
|
342
429
|
|
|
343
430
|
// src/pdf/loader.ts
|
|
344
431
|
var logger3 = createLogger("Loader");
|
|
345
432
|
var require2 = createRequire(import.meta.url);
|
|
346
|
-
var
|
|
433
|
+
var PDFJS_ROOT = require2.resolve("pdfjs-dist/package.json").replace("package.json", "");
|
|
434
|
+
var CMAP_URL = `${PDFJS_ROOT}cmaps/`;
|
|
435
|
+
var STANDARD_FONT_DATA_URL = `${PDFJS_ROOT}standard_fonts/`;
|
|
436
|
+
var WASM_URL = `${PDFJS_ROOT}wasm/`;
|
|
437
|
+
var ICC_URL = `${PDFJS_ROOT}iccs/`;
|
|
347
438
|
var MAX_PDF_SIZE = 100 * 1024 * 1024;
|
|
348
439
|
var loadPdfDocument = async (source, sourceDescription) => {
|
|
349
440
|
let pdfDataSource;
|
|
@@ -356,6 +447,11 @@ var loadPdfDocument = async (source, sourceDescription) => {
|
|
|
356
447
|
}
|
|
357
448
|
pdfDataSource = new Uint8Array(buffer);
|
|
358
449
|
} else if (source.url) {
|
|
450
|
+
const config = getSecurityConfig();
|
|
451
|
+
if (!isUrlAllowed(source.url, config)) {
|
|
452
|
+
const reason = config.allowHttp ? `host is not in the allowed list` : `HTTP access is disabled`;
|
|
453
|
+
throw new PdfError(-32600 /* InvalidRequest */, `Access denied: URL '${source.url}' rejected (${reason}).`);
|
|
454
|
+
}
|
|
359
455
|
pdfDataSource = { url: source.url };
|
|
360
456
|
} else {
|
|
361
457
|
throw new PdfError(-32602 /* InvalidParams */, `Source ${sourceDescription} missing 'path' or 'url'.`);
|
|
@@ -377,7 +473,10 @@ var loadPdfDocument = async (source, sourceDescription) => {
|
|
|
377
473
|
const loadingTask = getDocument({
|
|
378
474
|
...documentParams,
|
|
379
475
|
cMapUrl: CMAP_URL,
|
|
380
|
-
cMapPacked: true
|
|
476
|
+
cMapPacked: true,
|
|
477
|
+
standardFontDataUrl: STANDARD_FONT_DATA_URL,
|
|
478
|
+
wasmUrl: WASM_URL,
|
|
479
|
+
iccUrl: ICC_URL
|
|
381
480
|
});
|
|
382
481
|
try {
|
|
383
482
|
return await loadingTask.promise;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sylphx/pdf-reader-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "An MCP server providing tools to read PDF files.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -69,29 +69,32 @@
|
|
|
69
69
|
"prepare": "node_modules/.bin/lefthook install || true"
|
|
70
70
|
},
|
|
71
71
|
"dependencies": {
|
|
72
|
-
"@sylphx/mcp-server-sdk": "^2.1.
|
|
72
|
+
"@sylphx/mcp-server-sdk": "^2.1.1",
|
|
73
73
|
"@sylphx/vex": "^0.1.11",
|
|
74
|
-
"glob": "^13.0.
|
|
75
|
-
"pdfjs-dist": "^5.
|
|
74
|
+
"glob": "^13.0.6",
|
|
75
|
+
"pdfjs-dist": "^5.6.205",
|
|
76
76
|
"pngjs": "^7.0.0"
|
|
77
77
|
},
|
|
78
78
|
"overrides": {
|
|
79
79
|
"esbuild": "^0.25.0",
|
|
80
|
-
"preact": "^10.28.2"
|
|
80
|
+
"preact": "^10.28.2",
|
|
81
|
+
"defu": "^6.1.7",
|
|
82
|
+
"rollup": "^4.60.2",
|
|
83
|
+
"vite": "^6.4.2"
|
|
81
84
|
},
|
|
82
85
|
"devDependencies": {
|
|
83
|
-
"@biomejs/biome": "^2.
|
|
86
|
+
"@biomejs/biome": "^2.4.12",
|
|
84
87
|
"@sylphx/biome-config": "^0.4.1",
|
|
85
88
|
"@sylphx/bump": "^1.6.1",
|
|
86
|
-
"@sylphx/doctor": "^1.
|
|
89
|
+
"@sylphx/doctor": "^1.34.0",
|
|
87
90
|
"@sylphx/tsconfig": "^0.3.1",
|
|
88
91
|
"@types/glob": "^8.1.0",
|
|
89
|
-
"@types/node": "^25.0
|
|
92
|
+
"@types/node": "^25.6.0",
|
|
90
93
|
"@types/pngjs": "^6.0.5",
|
|
91
94
|
"bunup": "0.16.10",
|
|
92
|
-
"lefthook": "^2.
|
|
93
|
-
"typedoc": "^0.28.
|
|
94
|
-
"typedoc-plugin-markdown": "^4.
|
|
95
|
+
"lefthook": "^2.1.6",
|
|
96
|
+
"typedoc": "^0.28.19",
|
|
97
|
+
"typedoc-plugin-markdown": "^4.11.0",
|
|
95
98
|
"typescript": "^5.9.3",
|
|
96
99
|
"vitepress": "^1.6.4"
|
|
97
100
|
},
|