Skip to main content
Glama
README.md
# mtg-export

A CLI that turns a Magic: The Gathering deck into a **printable PDF proxy
sheet** — a 3×3 grid of card images with the border trimmed (2.5% of the width
off the sides, 2% off top/bottom) printed at ~95% of real card size
(≈2.375″×3.40″) on US-Letter pages, with faint cut guides. Point it at an
Archidekt deck, a text card list, or your clipboard.

Card images come from [Scryfall](https://scryfall.com), respecting their usage
policy (descriptive User-Agent, request throttling, and on-disk image caching so
files are never re-downloaded).

With `--modernize`, old-frame cards are **redrawn in modern M15 frames** by
[mtg-crucible](https://github.com/domainellipticlanguage/mtg-crucible), so a
1996 card prints as legibly as a current one.

![Example proxy sheet](docs/example.jpg)

*A page from a generated proxy sheet — 3×3 cards at true size with cut guides.
Printings here span 1993 black and white borders, 1997 and 2003 frames, and M15.*

![The same page with --modernize](docs/example-modernized.jpg)

*The same nine cards with `--modernize`. The seven old-frame cards are redrawn —
note that they also pick up current oracle wording, so "Interrupt" becomes
"Instant" and Swords to Plowshares exiles rather than removes from the game.
History of Benalia and Atraxa are already M15, so they are left exactly as
printed.*

## Install

```bash
npm install          # pdf-lib + @modelcontextprotocol/sdk + mtg-crucible
```

Requires Node 18+ (developed on Node 24). No Python. `mtg-crucible` pulls in a
native canvas (`@napi-rs/canvas`) with prebuilt binaries; it is only loaded when
you pass `--modernize`, so the default path stays pure JS.

Optionally link it as a global command:

```bash
npm link             # then run `mtg-export ...` anywhere
```

## Usage

```bash
# Archidekt deck by id or URL (public/unlisted work with no login)
node index.js 2087352
node index.js https://archidekt.com/decks/2087352/the-public-library

# From a text export file
node index.js --file mydeck.txt

# From the clipboard (macOS pbpaste) — e.g. after "Copy" on Archidekt/Moxfield
node index.js --clipboard

# Redraw old-frame cards in a modern frame (old cards are hard to read)
node index.js 2087352 --modernize
```

### Options

| Flag | Description |
| --- | --- |
| `-o, --output <file>` | Output PDF path (default: derived from the deck/file name) |
| `--size <size>` | Scryfall image size: `small` \| `normal` \| `large` \| `png` (default `normal`) |
| `--modernize` | Redraw pre-M15 cards in a modern frame from their art crop (see below) |
| `--modernize-all` | Modernize every card, no exceptions |
| `--include-basics` | Include basic lands (they are **excluded by default**) |
| `--single` | One copy of each card, ignoring deck quantities |
| `--cache-dir <dir>` | Where to cache images/metadata (default `~/.cache/mtg-export`) |
| `-c, --clipboard` | Read the card list from the clipboard |
| `-h, --help` | Show help |

### Text list format

One card per line, in the common Archidekt/Moxfield/MTGA export shape. The set
code and collector number are honored when present, so you get the exact
printing:

```
1x Sol Ring (mh1) 213
1 Delver of Secrets (isd) 51        # double-faced: both faces are printed
2 Lightning Bolt (2x2) 117 *F*      # *F* (foil) is accepted and ignored for art
3 Forest                            # basic land — excluded unless --include-basics
Counterspell                        # bare name resolves to a default printing
```

`# ...` / `//` comment lines and section labels (`Deck`, `Sideboard`,
`Commander`, …) are ignored. If a collector number doesn't match the named card,
it falls back to a name lookup rather than printing the wrong card.

## Modern frames (`--modernize`)

Old-border cards are hard to read at proxy size — thin type, low-contrast text
boxes, pre-modern mana symbols. `--modernize` throws those printed card images
away and rebuilds the card:

1. Scryfall's `art_crop` (the bare illustration, no frame) is downloaded.
2. The card's oracle data — name, mana cost, type line, rules text, flavor text,
   P/T, loyalty, rarity, artist, set, collector number — is mapped onto a
   [mtg-crucible](https://github.com/domainellipticlanguage/mtg-crucible)
   `CardData`, with `designer` set to `mtg-export`.
3. Crucible draws a current-frame card around the art.

**You also get current oracle text**, because Scryfall's rules text is the
up-to-date wording rather than what was printed on the card. "Interrupt" becomes
"Instant", "Summon Dragon" becomes "Creature — Dragon", Swords to Plowshares
exiles instead of removing from the game, and mana abilities stop mentioning the
mana pool. For a proxy you are going to play with, that is often worth as much
as the frame.

**Your printing is still respected.** The art crop, artist, set code and
collector number all come from the exact printing your deck specifies, so the
illustration you picked is the illustration you get. What changes is the frame
around it and the wording inside it.

### What is left alone

Modernizing is for cards that are hard to read, not a restyle of the whole
collection, so these keep their printed image:

| Kept as printed | Why |
| --- | --- |
| Already an M15 frame (`frame: "2015"`) | Nothing to gain — and the real printing is more faithful |
| Full-art printings | The art treatment is the point of the printing |
| Basic lands | If you chose specific lands, you chose them for a reason |
| Planeswalkers | The art crop is a 626×457 landscape; a planeswalker's art box is far taller, so the crop comes out badly zoomed |
| Split cards | Scryfall has one art crop covering both halves, so both halves would share a mangled crop |

Every run reports what it did and why:

```
  modernized 27 of 117 cards
    kept printed — already an M15 frame: 90
```

Pass `--modernize-all` to override all of it and push every card through
crucible — useful when you want one visually uniform sheet.

The two images at the top of this README are the same nine-card deck run with
and without the flag: [archidekt.com/decks/24672003](https://archidekt.com/decks/24672003).

### Details

Templates are picked up from the Scryfall data, so sagas, classes, levelers,
adventures, transform and MDFC cards all keep their proper layout (double-faced
cards still print as two slots). Snow, devoid, nyx and colorless full-bleed
frames carry over too.

`--size` selects render quality instead of a Scryfall image size —
`small`→low, `normal`→medium (default), `large`/`png`→high. A modernized sheet
is trimmed only on the sides (so cards print at a full 3.5″ tall), because
crucible's footer runs to the bottom edge of the card and the usual top/bottom
trim would slice through it.

Rendering is much slower than downloading, so renders are cached under
`<cache-dir>/modern/`, keyed by Scryfall id, quality, format and crucible
version. A re-run of the same deck is near-instant.

Caveats:

- Renders are crucible's interpretation of the card, not a facsimile of the
  printed one — set symbols, holo stamps and art-frame quirks won't match.
- A deliberately *retro-frame* reprint looks like any other old card to
  Scryfall, so it gets modernized. There is no field that distinguishes "this
  card is old" from "this printing is retro on purpose".
- The art crop is 626×457, which matches a standard card's art box almost
  exactly — but at `--size large`/`png` it is upscaled ~3×, so art is softer
  than the frame around it.
- If a card fails to render, the printed Scryfall image is used for it instead
  and a note is logged; the sheet is never left with a gap.

## File size (for GitHub)

`normal` and `large` are JPEG, which keeps a full ~100-card EDH deck small enough
to commit to a repo:

| `--size` | Format | ≈ per card | ≈ 100-card PDF |
| --- | --- | --- | --- |
| `normal` (default) | JPEG 488×680 | ~90 KB | ~9 MB |
| `large` | JPEG 672×936 | ~180 KB | ~18 MB |
| `png` | PNG 745×1040 | ~1 MB | ~100 MB (too big — local printing only) |

Use `normal` for anything you'll upload; use `large` for the nicest home prints.

`--modernize` renders are somewhat bigger — at the default (`normal` → medium
quality, 745×1040 JPEG) a modernized card is ~130 KB, against ~90 KB for a
printed one. How much that costs depends on how much of the deck is old.
`large` and `png` render at 2010×2814 and are local-printing only.

## Private Archidekt decks

Public and unlisted decks need no login. For a **private** deck, provide your
Archidekt credentials via environment variables or a `.env` file (copy
`.env.example`):

```
ARCHIDEKT_USERNAME=your-username
ARCHIDEKT_PASSWORD=your-password
```

The tool tries anonymously first and only logs in (JWT) if the deck isn't
publicly accessible.

## MCP server

The same functionality is exposed as an [MCP](https://modelcontextprotocol.io)
tool so an agent (Claude Desktop, Claude Code, etc.) can generate proxy sheets.

Run it over stdio:

```bash
node mcp.js        # or: npm run mcp   (also installed as the `mtg-export-mcp` bin)
```

Register it with an MCP client, e.g. Claude Desktop `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "mtg-export": {
      "command": "node",
      "args": ["/absolute/path/to/mtg-export/mcp.js"],
      "env": {
        "ARCHIDEKT_USERNAME": "your-username",
        "ARCHIDEKT_PASSWORD": "your-password"
      }
    }
  }
}
```

(`env` is only needed for private decks.)

It provides one tool, **`create_proxy_pdf`**:

| Argument | Type | Notes |
| --- | --- | --- |
| `deck` | string | Archidekt deck id or URL. Provide this **or** `cards`. |
| `cards` | string | Text card list (one per line). Provide this **or** `deck`. |
| `output` | string | Absolute path for the PDF (recommended). |
| `size` | enum | `small` \| `normal` \| `large` \| `png` (default `normal`). With `modernize`, selects render quality. |
| `modernize` | boolean | Redraw pre-M15 cards in modern frames (default `false`). |
| `modernize_all` | boolean | Modernize every card, no exceptions (default `false`). |
| `include_basics` | boolean | Include basic lands (default `false`). |
| `single` | boolean | One copy of each card (default `false`). |

It writes the PDF to disk and returns the path plus a summary (cards resolved,
basics excluded, pages, size, any unresolved names).

### Test with the MCP Inspector

Copy `.env.example` to `.env` and fill in your credentials (only needed for
private decks):

```bash
cp .env.example .env
```

Run the inspector against the server:

```bash
npm run inspect
# equivalently:
npx @modelcontextprotocol/inspector node mcp.js
```

This opens a local web UI where you can list the tool and call `create_proxy_pdf`
interactively.

## How it works

1. **Input** → a list of `{name, quantity, printing}` entries (Archidekt deck,
   text file, or clipboard).
2. **Resolve** → Scryfall. Archidekt entries carry the exact printing's Scryfall
   id, so images are batch-resolved directly; text entries resolve by
   set+collector / name+set / name.
3. **Cache** → every downloaded image is stored under the cache dir and reused.
4. **Modernize** (optional) → with `--modernize`, cards that would benefit have
   their art crop plus oracle data run through mtg-crucible to produce a fresh
   modern-frame card image, itself cached.
5. **Render** → `pdf-lib` lays out a 3×3 grid at ~95% of card size, trimming the
   border (2.5% of the width off the sides, 2% off top/bottom), with cut guides;
   double-faced cards contribute a front and a back slot.

## Notes

- Basic lands are excluded by default
- Deck quantities are printed by default; use `--single` for one of each.
- Foil is cosmetic for proxies — the printing is respected but the same art is
  used.