Skip to content

Commit 6b3b7e5

Browse files
wonderwhy-erdasein108coderabbitai[bot]codeant-ai[bot]
authored
feat: PDF v3 - page filtering, image extraction, performance optimization (#283)
* feat: pdf read/write * feat: pdf.js way * feat: read_file add PDF support * feat: write to PDF * feat: extract image, from pdf * feat: write pdf * feat: adjust to tools * feat: render pdf remark * feat: modify using markdown * feat: refactor, integrate with mcp tool * fix: schema parsing for pdf write tool * chore: remove experemental code * feat: add page filter, make output more comprehensive, extend too descriptions * feat: optimize performance for partial pages * fix: image extraction, compression * fix: remove png deps, adjust image optimization * chore: adjust pdf parsing test * Tests fix * Update src/handlers/filesystem-handlers.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update src/tools/pdf/extract-images.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update src/tools/mime-types.ts Co-authored-by: codeant-ai[bot] <151821869+codeant-ai[bot]@users.noreply.github.com> * Update src/tools/pdf/markdown.ts Co-authored-by: codeant-ai[bot] <151821869+codeant-ai[bot]@users.noreply.github.com> * Update src/handlers/filesystem-handlers.ts Co-authored-by: codeant-ai[bot] <151821869+codeant-ai[bot]@users.noreply.github.com> * Update test/test-pdf-parsing.js Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * fix: remove duplicate PageRange type and add missing title variable - Import PageRange type from canonical location ./lib/pdf2md.js - Remove duplicate local PageRange type definition in markdown.ts - Add missing title variable in filesystem-handlers.ts that was causing build failure * Revert "Update src/tools/pdf/markdown.ts" This reverts commit 134047f. * fix: update md-to-pdf to ^5.2.5 to address CVE-2025-65108 * fix: pdf edit test * feat: adjust pdf layout on insert/merge * chore: up md-to-pdf version * Update src/tools/pdf/manipulations.ts Co-authored-by: codeant-ai[bot] <151821869+codeant-ai[bot]@users.noreply.github.com> * Update test/test-pdf-creation.js Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Few more PDF tweaks --------- Co-authored-by: dasein <[email protected]> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: codeant-ai[bot] <151821869+codeant-ai[bot]@users.noreply.github.com>
1 parent 36caa10 commit 6b3b7e5

21 files changed

+8288
-3229
lines changed

package-lock.json

Lines changed: 6784 additions & 2841 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,21 @@
7878
],
7979
"dependencies": {
8080
"@modelcontextprotocol/sdk": "^1.9.0",
81+
"@opendocsg/pdf2md": "^0.2.2",
8182
"@vscode/ripgrep": "^1.15.9",
8283
"cross-fetch": "^4.1.0",
8384
"fastest-levenshtein": "^1.0.16",
85+
"file-type": "^21.1.1",
8486
"glob": "^10.3.10",
8587
"isbinaryfile": "^5.0.4",
88+
"md-to-pdf": "^5.2.5",
89+
"pdf-lib": "^1.17.1",
90+
"remark": "^15.0.1",
91+
"remark-gfm": "^4.0.1",
92+
"remark-parse": "^11.0.0",
93+
"sharp": "^0.34.5",
94+
"unified": "^11.0.5",
95+
"unpdf": "^1.4.0",
8696
"zod": "^3.24.1",
8797
"zod-to-json-schema": "^3.23.5"
8898
},
@@ -95,4 +105,4 @@
95105
"shx": "^0.3.4",
96106
"typescript": "^5.3.3"
97107
}
98-
}
108+
}

src/handlers/filesystem-handlers.ts

Lines changed: 92 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@ import {
66
listDirectory,
77
moveFile,
88
getFileInfo,
9+
writePdf,
910
type FileResult,
1011
type MultiFileResult
1112
} from '../tools/filesystem.js';
1213

13-
import {ServerResult} from '../types.js';
14-
import {withTimeout} from '../utils/withTimeout.js';
15-
import {createErrorResponse} from '../error-handlers.js';
16-
import {configManager} from '../config-manager.js';
14+
import { ServerResult } from '../types.js';
15+
import { withTimeout } from '../utils/withTimeout.js';
16+
import { createErrorResponse } from '../error-handlers.js';
17+
import { configManager } from '../config-manager.js';
1718

1819
import {
1920
ReadFileArgsSchema,
@@ -22,7 +23,8 @@ import {
2223
CreateDirectoryArgsSchema,
2324
ListDirectoryArgsSchema,
2425
MoveFileArgsSchema,
25-
GetFileInfoArgsSchema
26+
GetFileInfoArgsSchema,
27+
WritePdfArgsSchema
2628
} from '../tools/schemas.js';
2729

2830
/**
@@ -62,16 +64,47 @@ export async function handleReadFile(args: unknown): Promise<ServerResult> {
6264
// Use the provided limits or defaults
6365
const offset = parsed.offset ?? 0;
6466
const length = parsed.length ?? defaultLimit;
65-
67+
6668
const fileResult = await readFile(parsed.path, parsed.isUrl, offset, length);
67-
69+
if (fileResult.isPdf) {
70+
const meta = fileResult.payload?.metadata;
71+
const author = meta?.author ? `, Author: ${meta?.author}` : "";
72+
const title = meta?.title ? `, Title: ${meta?.title}` : "";
73+
// Use the provided limits or defaults.
74+
// If the caller did not supply an explicit length, fall back to the configured default.
75+
const rawArgs = args as { offset?: number; length?: number } | undefined;
76+
const offset = rawArgs && 'offset' in rawArgs ? parsed.offset : 0;
77+
const length = rawArgs && 'length' in rawArgs ? parsed.length : defaultLimit;
78+
79+
const content = fileResult.payload?.pages?.flatMap(p => [
80+
...(p.images?.map((image, i) => ({
81+
type: "image",
82+
data: image.data,
83+
mimeType: image.mimeType
84+
})) ?? []),
85+
{
86+
type: "text",
87+
text: `<!-- Page: ${p.pageNumber} -->\n${p.text}`,
88+
},
89+
]) ?? [];
90+
91+
return {
92+
content: [
93+
{
94+
type: "text",
95+
text: `PDF file: ${parsed.path}${author}${title} (${meta?.totalPages} pages) \n`
96+
},
97+
...content
98+
]
99+
};
100+
}
68101
if (fileResult.isImage) {
69102
// For image files, return as an image content type
70103
return {
71104
content: [
72-
{
73-
type: "text",
74-
text: `Image file: ${parsed.path} (${fileResult.mimeType})\n`
105+
{
106+
type: "text",
107+
text: `Image file: ${parsed.path} (${fileResult.mimeType})\n`
75108
},
76109
{
77110
type: "image",
@@ -87,7 +120,7 @@ export async function handleReadFile(args: unknown): Promise<ServerResult> {
87120
};
88121
}
89122
};
90-
123+
91124
// Execute with timeout at the handler level
92125
const result = await withTimeout(
93126
readFileOperation(),
@@ -108,28 +141,44 @@ export async function handleReadFile(args: unknown): Promise<ServerResult> {
108141
export async function handleReadMultipleFiles(args: unknown): Promise<ServerResult> {
109142
const parsed = ReadMultipleFilesArgsSchema.parse(args);
110143
const fileResults = await readMultipleFiles(parsed.paths);
111-
144+
112145
// Create a text summary of all files
113146
const textSummary = fileResults.map(result => {
114147
if (result.error) {
115148
return `${result.path}: Error - ${result.error}`;
149+
} else if (result.isPdf) {
150+
return `${result.path}: PDF file with ${result.payload?.pages?.length} pages`;
116151
} else if (result.mimeType) {
117152
return `${result.path}: ${result.mimeType} ${result.isImage ? '(image)' : '(text)'}`;
118153
} else {
119154
return `${result.path}: Unknown type`;
120155
}
121156
}).join("\n");
122-
157+
123158
// Create content items for each file
124-
const contentItems: Array<{type: string, text?: string, data?: string, mimeType?: string}> = [];
125-
159+
const contentItems: Array<{ type: string, text?: string, data?: string, mimeType?: string }> = [];
160+
126161
// Add the text summary
127162
contentItems.push({ type: "text", text: textSummary });
128-
163+
129164
// Add each file content
130165
for (const result of fileResults) {
131166
if (!result.error && result.content !== undefined) {
132-
if (result.isImage && result.mimeType) {
167+
if (result.isPdf) {
168+
result.payload?.pages.forEach((page, i) => {
169+
page.images.forEach((image, i) => {
170+
contentItems.push({
171+
type: "image",
172+
data: image.data,
173+
mimeType: image.mimeType
174+
});
175+
});
176+
contentItems.push({
177+
type: "text",
178+
text: page.text,
179+
});
180+
});
181+
} else if (result.isImage && result.mimeType) {
133182
// For image files, add an image content item
134183
contentItems.push({
135184
type: "image",
@@ -145,7 +194,7 @@ export async function handleReadMultipleFiles(args: unknown): Promise<ServerResu
145194
}
146195
}
147196
}
148-
197+
149198
return { content: contentItems };
150199
}
151200

@@ -159,7 +208,7 @@ export async function handleWriteFile(args: unknown): Promise<ServerResult> {
159208
// Get the line limit from configuration
160209
const config = await configManager.getConfig();
161210
const MAX_LINES = config.fileWriteLineLimit ?? 50; // Default to 50 if not set
162-
211+
163212
// Strictly enforce line count limit
164213
const lines = parsed.content.split('\n');
165214
const lineCount = lines.length;
@@ -172,13 +221,13 @@ export async function handleWriteFile(args: unknown): Promise<ServerResult> {
172221

173222
// Pass the mode parameter to writeFile
174223
await writeFile(parsed.path, parsed.content, parsed.mode);
175-
224+
176225
// Provide more informative message based on mode
177226
const modeMessage = parsed.mode === 'append' ? 'appended to' : 'wrote to';
178-
227+
179228
return {
180-
content: [{
181-
type: "text",
229+
content: [{
230+
type: "text",
182231
text: `Successfully ${modeMessage} ${parsed.path} (${lineCount} lines) ${errorMessage}`
183232
}],
184233
};
@@ -249,11 +298,11 @@ export async function handleGetFileInfo(args: unknown): Promise<ServerResult> {
249298
const parsed = GetFileInfoArgsSchema.parse(args);
250299
const info = await getFileInfo(parsed.path);
251300
return {
252-
content: [{
253-
type: "text",
301+
content: [{
302+
type: "text",
254303
text: Object.entries(info)
255304
.map(([key, value]) => `${key}: ${value}`)
256-
.join('\n')
305+
.join('\n')
257306
}],
258307
};
259308
} catch (error) {
@@ -262,5 +311,21 @@ export async function handleGetFileInfo(args: unknown): Promise<ServerResult> {
262311
}
263312
}
264313

265-
// The listAllowedDirectories function has been removed
266314
// Use get_config to retrieve the allowedDirectories configuration
315+
316+
/**
317+
* Handle write_pdf command
318+
*/
319+
export async function handleWritePdf(args: unknown): Promise<ServerResult> {
320+
try {
321+
const parsed = WritePdfArgsSchema.parse(args);
322+
await writePdf(parsed.path, parsed.content, parsed.outputPath, parsed.options);
323+
const targetPath = parsed.outputPath || parsed.path;
324+
return {
325+
content: [{ type: "text", text: `Successfully wrote PDF to ${targetPath}${parsed.outputPath ? `\nOriginal file: ${parsed.path}` : ''}` }],
326+
};
327+
} catch (error) {
328+
const errorMessage = error instanceof Error ? error.message : String(error);
329+
return createErrorResponse(errorMessage);
330+
}
331+
}

0 commit comments

Comments
 (0)