@@ -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 - Z a - z ] : [ / \\ ] / . test ( url ) ;
110+ }
111+
104112export 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