Skip to content
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
648c054
feat: pdf read/write
dasein108 Nov 20, 2025
8ae8a21
feat: pdf.js way
dasein108 Nov 20, 2025
638ba0d
feat: read_file add PDF support
dasein108 Nov 20, 2025
407f682
feat: write to PDF
dasein108 Nov 20, 2025
27bf915
feat: extract image, from pdf
dasein108 Nov 23, 2025
ffdb55b
feat: write pdf
dasein108 Nov 23, 2025
fa49964
feat: adjust to tools
dasein108 Nov 23, 2025
f2f9a60
feat: render pdf remark
dasein108 Nov 23, 2025
111dcf2
feat: modify using markdown
dasein108 Nov 23, 2025
86a35b2
feat: refactor, integrate with mcp tool
dasein108 Nov 24, 2025
73d7c39
fix: schema parsing for pdf write tool
dasein108 Nov 25, 2025
a4a7698
chore: remove experemental code
dasein108 Nov 25, 2025
3d0dff1
feat: add page filter, make output more comprehensive, extend too des…
dasein108 Nov 27, 2025
84e796d
feat: optimize performance for partial pages
dasein108 Nov 27, 2025
3f0ab32
fix: image extraction, compression
dasein108 Nov 27, 2025
72bca17
fix: remove png deps, adjust image optimization
dasein108 Nov 27, 2025
3fad97a
chore: adjust pdf parsing test
dasein108 Nov 29, 2025
ef9dbbc
Tests fix
wonderwhy-er Dec 1, 2025
8f93730
Update src/handlers/filesystem-handlers.ts
wonderwhy-er Dec 1, 2025
0906011
Update src/tools/pdf/extract-images.ts
wonderwhy-er Dec 1, 2025
24e6ef1
Update src/tools/mime-types.ts
wonderwhy-er Dec 1, 2025
134047f
Update src/tools/pdf/markdown.ts
wonderwhy-er Dec 1, 2025
0988d9f
Update src/handlers/filesystem-handlers.ts
wonderwhy-er Dec 1, 2025
877109c
Update test/test-pdf-parsing.js
wonderwhy-er Dec 1, 2025
307d302
fix: remove duplicate PageRange type and add missing title variable
wonderwhy-er Dec 1, 2025
b259461
Revert "Update src/tools/pdf/markdown.ts"
wonderwhy-er Dec 1, 2025
6883994
fix: update md-to-pdf to ^5.2.5 to address CVE-2025-65108
wonderwhy-er Dec 1, 2025
7180eba
fix: pdf edit test
dasein108 Dec 1, 2025
cd2f0e9
feat: adjust pdf layout on insert/merge
dasein108 Dec 1, 2025
1286099
chore: up md-to-pdf version
dasein108 Dec 1, 2025
f0687ca
Update src/tools/pdf/manipulations.ts
wonderwhy-er Dec 2, 2025
dcd643c
Update test/test-pdf-creation.js
wonderwhy-er Dec 2, 2025
ba083c2
Few more PDF tweaks
wonderwhy-er Dec 2, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9,625 changes: 6,784 additions & 2,841 deletions package-lock.json

Large diffs are not rendered by default.

12 changes: 11 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,21 @@
],
"dependencies": {
"@modelcontextprotocol/sdk": "^1.9.0",
"@opendocsg/pdf2md": "^0.2.2",
"@vscode/ripgrep": "^1.15.9",
"cross-fetch": "^4.1.0",
"fastest-levenshtein": "^1.0.16",
"file-type": "^21.1.1",
"glob": "^10.3.10",
"isbinaryfile": "^5.0.4",
"md-to-pdf": "^5.2.5",
"pdf-lib": "^1.17.1",
"remark": "^15.0.1",
"remark-gfm": "^4.0.1",
"remark-parse": "^11.0.0",
"sharp": "^0.34.5",
"unified": "^11.0.5",
"unpdf": "^1.4.0",
"zod": "^3.24.1",
"zod-to-json-schema": "^3.23.5"
},
Expand All @@ -95,4 +105,4 @@
"shx": "^0.3.4",
"typescript": "^5.3.3"
}
}
}
119 changes: 92 additions & 27 deletions src/handlers/filesystem-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import {
listDirectory,
moveFile,
getFileInfo,
writePdf,
type FileResult,
type MultiFileResult
} from '../tools/filesystem.js';

import {ServerResult} from '../types.js';
import {withTimeout} from '../utils/withTimeout.js';
import {createErrorResponse} from '../error-handlers.js';
import {configManager} from '../config-manager.js';
import { ServerResult } from '../types.js';
import { withTimeout } from '../utils/withTimeout.js';
import { createErrorResponse } from '../error-handlers.js';
import { configManager } from '../config-manager.js';

import {
ReadFileArgsSchema,
Expand All @@ -22,7 +23,8 @@ import {
CreateDirectoryArgsSchema,
ListDirectoryArgsSchema,
MoveFileArgsSchema,
GetFileInfoArgsSchema
GetFileInfoArgsSchema,
WritePdfArgsSchema
} from '../tools/schemas.js';

/**
Expand Down Expand Up @@ -62,16 +64,47 @@ export async function handleReadFile(args: unknown): Promise<ServerResult> {
// Use the provided limits or defaults
const offset = parsed.offset ?? 0;
const length = parsed.length ?? defaultLimit;

const fileResult = await readFile(parsed.path, parsed.isUrl, offset, length);

if (fileResult.isPdf) {
const meta = fileResult.payload?.metadata;
const author = meta?.author ? `, Author: ${meta?.author}` : "";
const title = meta?.title ? `, Title: ${meta?.title}` : "";
// Use the provided limits or defaults.
// If the caller did not supply an explicit length, fall back to the configured default.
const rawArgs = args as { offset?: number; length?: number } | undefined;
const offset = rawArgs && 'offset' in rawArgs ? parsed.offset : 0;
const length = rawArgs && 'length' in rawArgs ? parsed.length : defaultLimit;

const content = fileResult.payload?.pages?.flatMap(p => [
...(p.images?.map((image, i) => ({
type: "image",
data: image.data,
mimeType: image.mimeType
})) ?? []),
{
type: "text",
text: `<!-- Page: ${p.pageNumber} -->\n${p.text}`,
},
]) ?? [];

return {
content: [
{
type: "text",
text: `PDF file: ${parsed.path}${author}${title} (${meta?.totalPages} pages) \n`
},
...content
]
};
}
if (fileResult.isImage) {
// For image files, return as an image content type
return {
content: [
{
type: "text",
text: `Image file: ${parsed.path} (${fileResult.mimeType})\n`
{
type: "text",
text: `Image file: ${parsed.path} (${fileResult.mimeType})\n`
},
{
type: "image",
Expand All @@ -87,7 +120,7 @@ export async function handleReadFile(args: unknown): Promise<ServerResult> {
};
}
};

// Execute with timeout at the handler level
const result = await withTimeout(
readFileOperation(),
Expand All @@ -108,28 +141,44 @@ export async function handleReadFile(args: unknown): Promise<ServerResult> {
export async function handleReadMultipleFiles(args: unknown): Promise<ServerResult> {
const parsed = ReadMultipleFilesArgsSchema.parse(args);
const fileResults = await readMultipleFiles(parsed.paths);

// Create a text summary of all files
const textSummary = fileResults.map(result => {
if (result.error) {
return `${result.path}: Error - ${result.error}`;
} else if (result.isPdf) {
return `${result.path}: PDF file with ${result.payload?.pages?.length} pages`;
} else if (result.mimeType) {
return `${result.path}: ${result.mimeType} ${result.isImage ? '(image)' : '(text)'}`;
} else {
return `${result.path}: Unknown type`;
}
}).join("\n");

// Create content items for each file
const contentItems: Array<{type: string, text?: string, data?: string, mimeType?: string}> = [];
const contentItems: Array<{ type: string, text?: string, data?: string, mimeType?: string }> = [];

// Add the text summary
contentItems.push({ type: "text", text: textSummary });

// Add each file content
for (const result of fileResults) {
if (!result.error && result.content !== undefined) {
if (result.isImage && result.mimeType) {
if (result.isPdf) {
result.payload?.pages.forEach((page, i) => {
page.images.forEach((image, i) => {
contentItems.push({
type: "image",
data: image.data,
mimeType: image.mimeType
});
});
contentItems.push({
type: "text",
text: page.text,
});
});
} else if (result.isImage && result.mimeType) {
Comment on lines 112 to +181
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/handlers/filesystem-handlers.ts | head -200 | tail -70

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 3028


🏁 Script executed:

# Check file size first
wc -l src/handlers/filesystem-handlers.ts

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 115


🏁 Script executed:

# Look for type definitions
fd -e ts | xargs grep -l "interface.*Result\|type.*Result" | head -5

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 198


🏁 Script executed:

# Search for where payload is assigned/used
rg "payload\s*=" src/handlers/filesystem-handlers.ts -B 2 -A 2

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 58


🏁 Script executed:

# Check type definitions
cat -n src/types.ts

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 2365


🏁 Script executed:

# Find readMultipleFiles function definition
rg "function readMultipleFiles|const readMultipleFiles" src/ -B 2 -A 15

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 1448


🏁 Script executed:

# Find MultiFileResult type definition
rg "interface MultiFileResult|type MultiFileResult" src/ -B 2 -A 10

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 1521


🏁 Script executed:

# Look for payload structure definition
rg "payload.*=" src/tools/filesystem.ts -B 5 -A 5

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 777


🏁 Script executed:

# Find FileResultPayloads type definition
rg "interface FileResultPayloads|type FileResultPayloads" src/ -B 2 -A 15

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 860


🏁 Script executed:

# Search for where payload is created (PDF parsing)
rg "pages:\s*\[|images:\s*\[" src/ -B 5 -A 2 | head -60

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 1264


🏁 Script executed:

# Find PdfPayload type definition
rg "interface PdfPayload|type PdfPayload" src/ -B 2 -A 10

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 602


🏁 Script executed:

# Search for where PdfPayload is created/returned
rg "PdfPayload\s*[:{]|pages:\s*\[\|pages:" src/tools/ -B 3 -A 3 | head -80

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 58


🏁 Script executed:

# Find PdfPageItem type definition
rg "interface PdfPageItem|type PdfPageItem" src/ -B 2 -A 10

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 633


🏁 Script executed:

# Verify the exact behavior at line 163 - what happens with optional chaining
# Optional chaining: result.payload?.pages will return undefined if payload is undefined
# Then calling .forEach() on undefined should error
python3 << 'EOF'
# Demonstrate the issue
code = "result.payload?.pages.forEach(...)"
print("JavaScript optional chaining behavior:")
print(f"Code: {code}")
print("If result.payload is undefined:")
print("  result.payload?.pages evaluates to undefined")
print("  undefined.forEach(...) throws TypeError")
print("\nThis is the actual bug - optional chaining on property doesn't protect the method call")
EOF

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 162


Add optional chaining to PDF page and image iteration to prevent runtime errors.

The current code has a subtle but real bug at line 163:

result.payload?.pages.forEach((page, i) => {
    page.images.forEach((image, i) => {

Optional chaining (?.) only protects property access on the left side. If result.payload is undefined, the expression result.payload?.pages returns undefined, and calling .forEach() on undefined throws a TypeError.

Additionally, while the type system guarantees images is an array when a PdfPageItem exists, defensive programming suggests protecting it as well.

The fix is straightforward—add optional chaining to both .forEach() calls and adjust the content guard to only apply where needed:

-    for (const result of fileResults) {
-        if (!result.error && result.content !== undefined) {
-            if (result.isPdf) {
-                result.payload?.pages.forEach((page, i) => {
-                    page.images.forEach((image, i) => {
+    for (const result of fileResults) {
+        if (!result.error) {
+            if (result.isPdf) {
+                result.payload?.pages?.forEach((page) => {
+                    page.images?.forEach((image) => {
                         contentItems.push({
                             type: "image",
                             data: image.data,
                             mimeType: image.mimeType
                         });
                     });
                     contentItems.push({
                         type: "text",
                         text: page.text,
                     });
                 });
-            } else if (result.isImage && result.mimeType) {
+            } else if (result.isImage && result.mimeType && result.content !== undefined) {
                 // For image files, add an image content item
                 contentItems.push({
                     type: "image",
                     data: result.content,
                     mimeType: result.mimeType
                 });
-            } else {
+            } else if (result.content !== undefined) {
                 // For text files, add a text summary
                 contentItems.push({
                     type: "text",
                     text: `\n--- ${result.path} contents: ---\n${result.content}`
                 });
             }
         }
     }

This removes the global content guard (PDFs don't need content to emit images from payload), adds safe optional chaining on both pages?.forEach() and images?.forEach(), and applies the content check only where it's semantically required.

🤖 Prompt for AI Agents
In src/handlers/filesystem-handlers.ts around lines 140 to 176, the PDF
iteration uses result.payload?.pages.forEach(...) and page.images.forEach(...),
which can throw if pages or images are undefined; update the loops to use
optional chaining (pages?.forEach and images?.forEach) and remove the global
result.content guard for PDF handling so PDFs can emit images/text from payload
even when result.content is undefined; keep the content existence check for
non-PDF branches where actual file content is required.

// For image files, add an image content item
contentItems.push({
type: "image",
Expand All @@ -145,7 +194,7 @@ export async function handleReadMultipleFiles(args: unknown): Promise<ServerResu
}
}
}

return { content: contentItems };
}

Expand All @@ -159,7 +208,7 @@ export async function handleWriteFile(args: unknown): Promise<ServerResult> {
// Get the line limit from configuration
const config = await configManager.getConfig();
const MAX_LINES = config.fileWriteLineLimit ?? 50; // Default to 50 if not set

// Strictly enforce line count limit
const lines = parsed.content.split('\n');
const lineCount = lines.length;
Expand All @@ -172,13 +221,13 @@ export async function handleWriteFile(args: unknown): Promise<ServerResult> {

// Pass the mode parameter to writeFile
await writeFile(parsed.path, parsed.content, parsed.mode);

// Provide more informative message based on mode
const modeMessage = parsed.mode === 'append' ? 'appended to' : 'wrote to';

return {
content: [{
type: "text",
content: [{
type: "text",
text: `Successfully ${modeMessage} ${parsed.path} (${lineCount} lines) ${errorMessage}`
}],
};
Expand Down Expand Up @@ -249,11 +298,11 @@ export async function handleGetFileInfo(args: unknown): Promise<ServerResult> {
const parsed = GetFileInfoArgsSchema.parse(args);
const info = await getFileInfo(parsed.path);
return {
content: [{
type: "text",
content: [{
type: "text",
text: Object.entries(info)
.map(([key, value]) => `${key}: ${value}`)
.join('\n')
.join('\n')
}],
};
} catch (error) {
Expand All @@ -262,5 +311,21 @@ export async function handleGetFileInfo(args: unknown): Promise<ServerResult> {
}
}

// The listAllowedDirectories function has been removed
// Use get_config to retrieve the allowedDirectories configuration

/**
* Handle write_pdf command
*/
export async function handleWritePdf(args: unknown): Promise<ServerResult> {
try {
const parsed = WritePdfArgsSchema.parse(args);
await writePdf(parsed.path, parsed.content, parsed.outputPath, parsed.options);
const targetPath = parsed.outputPath || parsed.path;
return {
content: [{ type: "text", text: `Successfully wrote PDF to ${targetPath}${parsed.outputPath ? `\nOriginal file: ${parsed.path}` : ''}` }],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return createErrorResponse(errorMessage);
}
}
Loading