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:

  1. Chrome installation check – Uses chrome-aws-lambda or local Chromium
  2. Path injection – Injects executablePath into md-to-pdf options
  3. Buffer generation – Returns a Buffer containing 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 contentparseMarkdownToPdf → 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 contenteditPdfdeletePages 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_pdf is dual-mode: String content triggers creation; array content triggers modification
  • Markdown rendering uses md-to-pdf with headless Chrome, located in src/tools/pdf/markdown.ts
  • Page manipulation uses pdf-lib for zero-dependency PDF surgery in src/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 outputPath to 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →