Skip to content

Commit 3d6ba15

Browse files
committed
fix(pdf-server): accept bare file paths in addition to file:// URLs
Clients like Claude Desktop may pass raw absolute paths (e.g. /Users/.../file.pdf) instead of file:// URLs. Handle these in validateUrl and readPdfRange.
1 parent 1668951 commit 3d6ba15

2 files changed

Lines changed: 31 additions & 8 deletions

File tree

examples/pdf-server/server.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,15 @@ describe("validateUrl with MCP roots (allowedLocalDirs)", () => {
302302
const result = validateUrl(`computer://${encoded}`);
303303
expect(result.valid).toBe(true);
304304
});
305+
306+
it("should accept bare absolute paths as local files", () => {
307+
const dir = path.resolve(import.meta.dirname);
308+
allowedLocalDirs.add(dir);
309+
310+
const filePath = path.join(dir, "server.ts");
311+
const result = validateUrl(filePath);
312+
expect(result.valid).toBe(true);
313+
});
305314
});
306315

307316
describe("isAncestorDir", () => {

examples/pdf-server/server.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,17 @@ export function isAncestorDir(dir: string, filePath: string): boolean {
101101
return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel);
102102
}
103103

104+
/**
105+
* Check if `url` looks like an absolute local file path (not a URL scheme).
106+
* Handles Unix paths (/...), home-relative (~), and Windows drive letters (C:\...).
107+
*/
108+
function isLocalPath(url: string): boolean {
109+
return url.startsWith("/") || url.startsWith("~") || /^[A-Za-z]:[/\\]/.test(url);
110+
}
111+
104112
export function validateUrl(url: string): { valid: boolean; error?: string } {
105-
if (isFileUrl(url)) {
106-
const filePath = fileUrlToPath(url);
113+
if (isFileUrl(url) || isLocalPath(url)) {
114+
const filePath = isFileUrl(url) ? fileUrlToPath(url) : url;
107115
const resolved = path.resolve(filePath);
108116

109117
// Check exact match (CLI args / roots)
@@ -116,12 +124,16 @@ export function validateUrl(url: string): { valid: boolean; error?: string } {
116124
);
117125

118126
if (!exactMatch && !dirMatch) {
127+
const diagnostics = [...allowedLocalDirs].map((d) => {
128+
const rel = path.relative(d, resolved);
129+
return `dir=${d} rel=${rel} match=${isAncestorDir(d, resolved)}`;
130+
});
119131
console.error(
120-
`[pdf-server] validateUrl REJECTED: url=${url}\n filePath=${filePath}\n resolved=${resolved}\n allowedDirs=${JSON.stringify([...allowedLocalDirs])}\n dirChecks=${JSON.stringify([...allowedLocalDirs].map((d) => ({ dir: d, rel: path.relative(d, resolved), match: isAncestorDir(d, resolved) })))}`,
132+
`[pdf-server] validateUrl REJECTED:\n url=${url}\n resolved=${resolved}\n diagnostics:\n ${diagnostics.join("\n ")}`,
121133
);
122134
return {
123135
valid: false,
124-
error: `Local file not in allowed list: \n${resolved}\nAllowed files: ${[...allowedLocalFiles].join(", ")}\nAllowed directories:\n${[...allowedLocalDirs].join("\n")}`,
136+
error: `Local file not in allowed list: ${resolved}\nAllowed directories: ${[...allowedLocalDirs].join(", ")}\nDiagnostics: ${diagnostics.join(" | ")}`,
125137
};
126138
}
127139
if (!fs.existsSync(resolved)) {
@@ -254,8 +266,10 @@ export function createPdfCache(): PdfCache {
254266
const normalized = isArxivUrl(url) ? normalizeArxivUrl(url) : url;
255267
const clampedByteCount = Math.min(byteCount, MAX_CHUNK_BYTES);
256268

257-
if (isFileUrl(normalized)) {
258-
const filePath = fileUrlToPath(normalized);
269+
if (isFileUrl(normalized) || isLocalPath(normalized)) {
270+
const filePath = isFileUrl(normalized)
271+
? fileUrlToPath(normalized)
272+
: normalized;
259273
const stats = await fs.promises.stat(filePath);
260274
const totalBytes = stats.size;
261275

@@ -463,7 +477,7 @@ export function createServer(): McpServer {
463477
title: "Read PDF Bytes",
464478
description: "Read a range of bytes from a PDF (max 512KB per request)",
465479
inputSchema: {
466-
url: z.string().describe("PDF URL"),
480+
url: z.string().describe("PDF URL or local file path"),
467481
offset: z.number().min(0).default(0).describe("Byte offset"),
468482
byteCount: z
469483
.number()
@@ -542,7 +556,7 @@ Accepts:
542556
- Local files under directories provided by the client as MCP roots
543557
- Any remote PDF accessible via HTTPS`,
544558
inputSchema: {
545-
url: z.string().default(DEFAULT_PDF).describe("PDF URL"),
559+
url: z.string().default(DEFAULT_PDF).describe("PDF URL or local file path"),
546560
page: z.number().min(1).default(1).describe("Initial page"),
547561
},
548562
outputSchema: z.object({

0 commit comments

Comments
 (0)