feat(pdf-server): use MCP roots to auto-allow local directories (#479)
When the client advertises roots capability, the server queries roots/list on initialization and subscribes to roots/list_changed notifications. Any file:// roots pointing to existing directories are added to an allowedLocalDirs set, enabling access to all PDFs under those directories without needing to enumerate them via CLI args. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> fix: revert unrelated changes from bad rebase Restores files that were accidentally modified alongside the MCP roots feature: README, pdf-server UI (search feature), threejs example, SDK guards (app.ts, app-bridge.ts), typedoc theme, media assets, and e2e snapshots.
This commit is contained in:
parent
37e127034f
commit
96c0af577d
1 changed files with 150 additions and 10 deletions
|
|
@ -1,6 +1,11 @@
|
|||
import { describe, it, expect, beforeEach, afterEach, spyOn } from "bun:test";
|
||||
import path from "node:path";
|
||||
import {
|
||||
createPdfCache,
|
||||
validateUrl,
|
||||
allowedLocalFiles,
|
||||
allowedLocalDirs,
|
||||
pathToFileUrl,
|
||||
CACHE_INACTIVITY_TIMEOUT_MS,
|
||||
CACHE_MAX_LIFETIME_MS,
|
||||
CACHE_MAX_PDF_SIZE_BYTES,
|
||||
|
|
@ -178,3 +183,65 @@ describe("PDF Cache with Timeouts", () => {
|
|||
// The timeout behavior is straightforward and can be verified
|
||||
// through manual testing or E2E tests.
|
||||
});
|
||||
|
||||
describe("validateUrl with MCP roots (allowedLocalDirs)", () => {
|
||||
const savedFiles = new Set(allowedLocalFiles);
|
||||
const savedDirs = new Set(allowedLocalDirs);
|
||||
|
||||
beforeEach(() => {
|
||||
allowedLocalFiles.clear();
|
||||
allowedLocalDirs.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
allowedLocalFiles.clear();
|
||||
allowedLocalDirs.clear();
|
||||
for (const f of savedFiles) allowedLocalFiles.add(f);
|
||||
for (const d of savedDirs) allowedLocalDirs.add(d);
|
||||
});
|
||||
|
||||
it("should allow a file under an allowed directory", () => {
|
||||
// Use a real existing directory+file for the existsSync check
|
||||
const dir = path.resolve(import.meta.dirname);
|
||||
allowedLocalDirs.add(dir);
|
||||
|
||||
const filePath = path.join(dir, "server.ts");
|
||||
const result = validateUrl(pathToFileUrl(filePath));
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it("should reject a file outside allowed directories", () => {
|
||||
allowedLocalDirs.add("/some/allowed/dir");
|
||||
|
||||
const result = validateUrl("file:///other/dir/test.pdf");
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.error).toContain("not in allowed list");
|
||||
});
|
||||
|
||||
it("should prevent prefix-based directory traversal", () => {
|
||||
// /tmp/safe should NOT allow /tmp/safevil/file.pdf
|
||||
allowedLocalDirs.add("/tmp/safe");
|
||||
|
||||
const result = validateUrl("file:///tmp/safevil/file.pdf");
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
it("should still allow exact file matches from allowedLocalFiles", () => {
|
||||
const filePath = path.resolve(import.meta.dirname, "server.ts");
|
||||
allowedLocalFiles.add(filePath);
|
||||
|
||||
const result = validateUrl(pathToFileUrl(filePath));
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it("should reject non-existent file even if under allowed dir", () => {
|
||||
const dir = path.resolve(import.meta.dirname);
|
||||
allowedLocalDirs.add(dir);
|
||||
|
||||
const result = validateUrl(
|
||||
pathToFileUrl(path.join(dir, "nonexistent-file.pdf")),
|
||||
);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.error).toContain("File not found");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue