@sylphx/pdf-reader-mcp 2.3.1 → 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.
Files changed (3) hide show
  1. package/README.md +53 -0
  2. package/dist/index.js +95 -3
  3. package/package.json +1 -1
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,14 +412,19 @@ class PdfError extends Error {
330
412
  }
331
413
 
332
414
  // src/utils/pathUtils.ts
333
- import path from "node:path";
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 = path.normalize(userPath);
340
- return path.isAbsolute(normalizedUserPath) ? normalizedUserPath : path.resolve(PROJECT_ROOT, normalizedUserPath);
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
@@ -360,6 +447,11 @@ var loadPdfDocument = async (source, sourceDescription) => {
360
447
  }
361
448
  pdfDataSource = new Uint8Array(buffer);
362
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
+ }
363
455
  pdfDataSource = { url: source.url };
364
456
  } else {
365
457
  throw new PdfError(-32602 /* InvalidParams */, `Source ${sourceDescription} missing 'path' or 'url'.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sylphx/pdf-reader-mcp",
3
- "version": "2.3.1",
3
+ "version": "2.4.0",
4
4
  "description": "An MCP server providing tools to read PDF files.",
5
5
  "type": "module",
6
6
  "bin": {