@@ -94,6 +94,21 @@ export const allowedLocalDirs = new Set<string>();
9494 */
9595export const cliLocalFiles = new Set < string > ( ) ;
9696
97+ /**
98+ * Write-permission flags. Object wrapper (not a bare `let`) so main.ts can
99+ * mutate via the exported binding without re-import gymnastics — same
100+ * pattern as the Sets above.
101+ */
102+ export const writeFlags = {
103+ /**
104+ * Claude Desktop mounts its per-conversation drop folder as a directory
105+ * root whose basename is literally `uploads`. Files in there are one-shot
106+ * copies the client doesn't expect us to overwrite. Default: read-only.
107+ * `--writeable-uploads-root` flips this for local testing.
108+ */
109+ allowUploadsRoot : false ,
110+ } ;
111+
97112/**
98113 * Saving is allowed iff:
99114 * (a) the file was passed as a CLI arg — the user explicitly named it
@@ -105,13 +120,24 @@ export const cliLocalFiles = new Set<string>();
105120 * treat that signal as authoritative even when the path happens
106121 * to fall inside a mounted directory.
107122 *
123+ * EXCEPTION to (b): a dir root whose basename is `uploads` is treated
124+ * as read-only unless `writeFlags.allowUploadsRoot` is set. This is how
125+ * Claude Desktop surfaces attached files — writing back to them
126+ * surprises the user (the attachment doesn't update).
127+ *
108128 * With no directory roots and no CLI files, nothing is writable.
109129 */
110130export function isWritablePath ( resolved : string ) : boolean {
111131 if ( cliLocalFiles . has ( resolved ) ) return true ;
112132 // MCP file root → always read-only, regardless of ancestry
113133 if ( allowedLocalFiles . has ( resolved ) ) return false ;
114- return [ ...allowedLocalDirs ] . some ( ( dir ) => isAncestorDir ( dir , resolved ) ) ;
134+ return [ ...allowedLocalDirs ] . some ( ( dir ) => {
135+ if ( ! isAncestorDir ( dir , resolved ) ) return false ;
136+ if ( ! writeFlags . allowUploadsRoot && path . basename ( dir ) === "uploads" ) {
137+ return false ;
138+ }
139+ return true ;
140+ } ) ;
115141}
116142
117143// Works both from source (server.ts) and compiled (dist/server.js)
@@ -1158,10 +1184,17 @@ export interface CreateServerOptions {
11581184 * @default false
11591185 */
11601186 useClientRoots ?: boolean ;
1187+
1188+ /**
1189+ * Emit debug metadata to the viewer (currently: allowed roots shown
1190+ * in a floating bubble). Toggled by the `--debug` CLI flag.
1191+ */
1192+ debug ?: boolean ;
11611193}
11621194
11631195export function createServer ( options : CreateServerOptions = { } ) : McpServer {
11641196 const { enableInteract = false , useClientRoots = false } = options ;
1197+ const debug = options . debug ?? false ;
11651198 const disableInteract = ! enableInteract ;
11661199 const server = new McpServer ( { name : "PDF Server" , version : "2.0.0" } ) ;
11671200
@@ -1432,11 +1465,13 @@ Set \`elicit_form_inputs\` to true to prompt the user to fill form fields before
14321465 // Check writability (governs save button; see isWritablePath doc).
14331466 // Also requires OS-level W_OK so we don't lie on read-only mounts.
14341467 let writable = false ;
1468+ let debugResolved : string | undefined ; // only used when --debug
14351469 if ( isFileUrl ( normalized ) || isLocalPath ( normalized ) ) {
14361470 const localPath = isFileUrl ( normalized )
14371471 ? fileUrlToPath ( normalized )
14381472 : decodeURIComponent ( normalized ) ;
14391473 const resolved = path . resolve ( localPath ) ;
1474+ debugResolved = resolved ;
14401475 if ( isWritablePath ( resolved ) ) {
14411476 try {
14421477 await fs . promises . access ( resolved , fs . constants . W_OK ) ;
@@ -1595,6 +1630,21 @@ Set \`elicit_form_inputs\` to true to prompt the user to fill form fields before
15951630 viewUUID : uuid ,
15961631 interactEnabled : ! disableInteract ,
15971632 writable,
1633+ // Debug: viewer renders this in a floating bubble (--debug flag).
1634+ ...( debug
1635+ ? {
1636+ _debug : {
1637+ resolved : debugResolved ,
1638+ writable,
1639+ isWritablePath : debugResolved
1640+ ? isWritablePath ( debugResolved )
1641+ : undefined ,
1642+ cliLocalFiles : [ ...cliLocalFiles ] ,
1643+ allowedLocalFiles : [ ...allowedLocalFiles ] ,
1644+ allowedLocalDirs : [ ...allowedLocalDirs ] ,
1645+ } ,
1646+ }
1647+ : { } ) ,
15981648 } ,
15991649 } ;
16001650 } ,
0 commit comments