# How `write_pdf` Supports PDF Creation from Markdown and Page Manipulation in DesktopCommanderMCP

> Discover how DesktopCommanderMCP's write_pdf tool generates PDFs from Markdown and manipulates pages. Learn about its dual-mode architecture using md-to-pdf and pdf-lib for efficient PDF creation.

- Repository: [Eduard Ruzga/DesktopCommanderMCP](https://github.com/wonderwhy-er/DesktopCommanderMCP)
- Tags: how-to-guide
- Published: 2026-08-04

---

**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](https://github.com/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`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/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`.

```typescript
// 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`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/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`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/tools/filesystem.ts) (lines 1015–1057) implements the dual-mode logic:

```typescript
// 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`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/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

```typescript
// 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`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/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:

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/server.ts) | 495–515 | Tool registration and LLM schema |
| [`src/tools/schemas.ts`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/tools/schemas.ts) | 83–120 | Zod validation for arguments and operations |
| [`src/handlers/filesystem-handlers.ts`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/handlers/filesystem-handlers.ts) | 86–94 | Request dispatch |
| [`src/tools/filesystem.ts`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/tools/filesystem.ts) | 1015–1057 | Mode routing (`writePdf`) |
| [`src/tools/pdf/markdown.ts`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/tools/pdf/markdown.ts) | 87–104 | Markdown rendering pipeline |
| [`src/tools/pdf/manipulations.ts`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/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`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/tools/pdf/markdown.ts)
- **Page manipulation** uses `pdf-lib` for zero-dependency PDF surgery in [`src/tools/pdf/manipulations.ts`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/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`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/tools/schemas.ts) enforces mutual exclusivity—only one content source is permitted per insert operation. Validation fails before any file I/O occurs.