How `write_pdf` Supports PDF Creation from Markdown and Page Manipulation in DesktopCommanderMCP
The write_pdf tool in DesktopCommanderMCP creates new PDFs from Markdown strings or modifies existing PDFs by deleting and inserting pages, using a dual-mode architecture that routes string content to md-to-pdf rendering and array content to pdf-lib manipulation.
This guide breaks down exactly how write_pdf works under the hood in the wonderwhy-er/DesktopCommanderMCP repository. Whether you're generating reports from Markdown or surgically editing multi-page documents, understanding this implementation helps you leverage the full power of the tool.
Tool Architecture and Registration
The write_pdf tool is registered in src/server.ts (lines 495–515) where its JSON schema description is exposed to LLM clients. Incoming calls are dispatched to handlers.handleWritePdf after validation against WritePdfArgsSchema.
// Tool definition in src/server.ts
{
name: "write_pdf",
description: "Create PDF from markdown or manipulate existing PDF pages",
// ... schema and handler binding
}
Key design decision: A single tool handles both creation and modification. This reduces API surface area while providing complete PDF workflow coverage.
Argument Validation and Routing
WritePdfArgsSchema in src/tools/schemas.ts (lines 99–120) defines the contract:
| Parameter | Type | Purpose |
|---|---|---|
path |
string | Source file path (for modification) or destination (for creation) |
content |
string | array | Markdown source or array of PDF operations |
outputPath |
string (optional) | Separate destination for modified files |
options |
object (optional) | PDF generation settings (margins, format, etc.) |
The schema includes a preprocessor that auto-parses JSON-style arrays passed as strings, accommodating LLM clients that serialize arrays differently.
Core Implementation in writePdf
The writePdf function in src/tools/filesystem.ts (lines 1015–1057) implements the dual-mode logic:
// Simplified routing logic from src/tools/filesystem.ts
export async function writePdf(args: WritePdfArgs): Promise<string> {
if (typeof args.content === 'string') {
// CREATE MODE: Markdown → PDF
const pdfBuffer = await parseMarkdownToPdf(args.content, args.options);
await fs.writeFile(args.outputPath || args.path, pdfBuffer);
} else {
// MODIFY MODE: Page manipulation
const validatedOps = args.content.map(validateOperation);
const resultBuffer = await editPdf(args.path, validatedOps);
await fs.writeFile(args.outputPath || args.path, resultBuffer);
}
return `PDF saved successfully`;
}
Markdown to PDF Conversion
parseMarkdownToPdf in src/tools/pdf/markdown.ts (lines 87–104) handles the rendering pipeline:
- Chrome installation check – Uses
chrome-aws-lambdaor local Chromium - Path injection – Injects
executablePathintomd-to-pdfoptions - Buffer generation – Returns a
Buffercontaining the rendered PDF
// Key implementation from src/tools/pdf/markdown.ts
export async function parseMarkdownToPdf(
markdown: string,
options?: PdfGenerationOptions
): Promise<Buffer> {
const chromePath = await getChromePath();
const config = {
...defaultOptions,
...options,
executablePath: chromePath, // Critical for headless rendering
};
return await mdToPdf({ content: markdown }, config);
}
Performance note: First call triggers Chrome installation/download; subsequent calls reuse the cached binary.
PDF Page Manipulation with editPdf
editPdf in src/tools/pdf/manipulations.ts (lines 95–131) provides surgical page-level editing using pdf-lib:
Page Layout Preservation
The function extracts layout metadata from the first page via getPageLayout to maintain consistency across inserted content:
const layout = await getPageLayout(pdfDoc.getPage(0));
// { width, height, xMargin, yMargin } used for new pages
Supported Operations
| Operation | Schema | Implementation |
|---|---|---|
| Delete | PdfDeleteOperationSchema |
deletePages with normalized negative indexes |
| Insert Markdown | PdfInsertOperationSchema with markdown field |
Renders to temp PDF via parseMarkdownToPdf, then splices |
| Insert PDF | PdfInsertOperationSchema with sourcePdfPath |
Loads external PDF and merges at specified index |
Index handling: Negative indexes are normalized (e.g., -1 becomes last page), matching Python-style indexing for intuitive LLM interactions.
Practical Code Examples
Create PDF from Markdown
write_pdf({
path: "reports/summary.pdf",
content: "# Quarterly Summary\n\n| Metric | Value |\n|--------|-------|\n| Revenue | $1.2M |\n| Growth | 12% |"
});
Flow: String content → parseMarkdownToPdf → Chrome headless rendering → saved buffer.
Delete Specific Pages
write_pdf({
path: "invoices/batch.pdf",
content: [
{ type: "delete", pageIndexes: [0, 3, -1] } // first, fourth, last
],
outputPath: "invoices/batch_clean.pdf"
});
Flow: Array content → editPdf → deletePages with index normalization → saved to outputPath.
Insert Markdown Page After Cover
write_pdf({
path: "contracts/contract.pdf",
content: [
{
type: "insert",
pageIndex: 1, // After page 1 (0-indexed: position 1)
markdown: "# Addendum\n\nAll terms subject to change per Section 7."
}
],
outputPath: "contracts/contract_v2.pdf"
});
Flow: Markdown rendered to temporary PDF → insertPages with inherited layout → merged output.
Merge External PDF
write_pdf({
path: "presentations/deck.pdf",
content: [
{
type: "insert",
pageIndex: 3,
sourcePdfPath: "assets/appendix.pdf" // Entire file inserted
}
],
outputPath: "presentations/deck_extended.pdf"
});
Flow: External PDF loaded via pdf-lib → pages copied and spliced at index 3 → preserved original formatting.
Source File Reference
| File | Lines | Responsibility |
|---|---|---|
src/server.ts |
495–515 | Tool registration and LLM schema |
src/tools/schemas.ts |
83–120 | Zod validation for arguments and operations |
src/handlers/filesystem-handlers.ts |
86–94 | Request dispatch |
src/tools/filesystem.ts |
1015–1057 | Mode routing (writePdf) |
src/tools/pdf/markdown.ts |
87–104 | Markdown rendering pipeline |
src/tools/pdf/manipulations.ts |
95–131 | Page delete/insert operations |
Summary
write_pdfis dual-mode: Stringcontenttriggers creation; arraycontenttriggers modification- Markdown rendering uses
md-to-pdfwith headless Chrome, located insrc/tools/pdf/markdown.ts - Page manipulation uses
pdf-libfor zero-dependency PDF surgery insrc/tools/pdf/manipulations.ts - Layout preservation: Inserted pages inherit margins and dimensions from the source document's first page
- Flexible indexing: Negative page indexes are normalized for intuitive "from end" referencing
- Immutable by default: Use
outputPathto preserve originals; omit to overwrite
Frequently Asked Questions
What libraries power write_pdf under the hood?
DesktopCommanderMCP uses md-to-pdf for Markdown rendering (which itself uses Puppeteer/Chrome) and pdf-lib for pure-JavaScript PDF manipulation. No external CLI tools like wkhtmltopdf or pdftk are required.
Can I combine multiple operations in a single write_pdf call?
Yes. The content array accepts multiple operations executed in sequence. For example: [{type:"delete",...}, {type:"insert",...}, {type:"delete",...}] removes pages, inserts new content, then removes additional pages.
How does the tool handle large PDF files?
pdf-lib loads entire PDFs into memory as Uint8Array buffers. For very large files (hundreds of MB), performance depends on Node.js memory limits. The implementation does not currently support streaming processing.
What happens if I specify both markdown and sourcePdfPath in an insert operation?
The PdfInsertOperationSchema in src/tools/schemas.ts enforces mutual exclusivity—only one content source is permitted per insert operation. Validation fails before any file I/O occurs.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →