Skip to main content
Glama
christianblandford

label-printer-mcp

README.md
# label-printer-mcp

Print a 4x6 shipping label from a PDF — as a CLI, a Python library, or an MCP server.

A return label turns up as a 4x6 page, or as a US Letter sheet with the label in one corner and a packing slip below
it, or sideways. This finds the label, crops to it **losslessly**, checks it really is 4x6, and prints it at exactly
100% on a 4x6 thermal printer.

## Why it crops instead of scaling

A carrier's barcode encodes the tracking number in the **width of its bars**. Scale the label — or rasterise and
resample it — and you can get a parcel whose barcode a scanner won't read, with nothing wrong on screen to warn you.
It gets rejected at the counter.

So the crop here is a **CropBox change**: the page keeps every object it had and simply shows a smaller window onto
them. Vector artwork stays vector and prints at whatever resolution the printer has. Nothing is resampled, ever, and
printing passes `print-scaling=none` so CUPS doesn't helpfully fit the label to the media.

A test pins this: it counts the barcode's vector drawings after the crop. A rasterising crop would leave one image and
zero drawings, and would pass a size check while producing a label a scanner may refuse.

## Why it refuses

Finding the label on a letter sheet is a heuristic. Candidates are scored on **aspect ratio alone** — "biggest block"
and "most ink" both pick the packing slip on a busy sheet, while a 4x6 label is the only thing on it with a 1.5 ratio
and a short side over three inches.

When nothing scores well it refuses and says what it saw, with measurements, rather than cropping to the wrong thing:

```
no region looks like a 4x6 label. Closest: 3.41x8.01in (aspect 2.35), 8.5x11.0in (aspect 1.29).
A 4x6 has aspect 1.50 and a short side of 4.0in; refusing rather than cropping to the wrong thing.
```

```
that region is 3.2x4.8in, not 4.0x6.0in (aspect is right, size is not). Scaling it would corrupt the
barcode, so nothing was written. Print it as-is only if you mean to.
```

Paper is cheap; a mislabelled return is not.

## Install

```sh
python3 -m venv .venv
.venv/bin/pip install -e .          # or: pip install pymupdf mcp
```

## CLI

```sh
python -m labelprint inspect   label.pdf              # what it is, and where the label looks to be
python -m labelprint normalise label.pdf out.4x6.pdf  # crop to the label; --page N for a later page
python -m labelprint preview   label.pdf out.png      # look before you print
python -m labelprint queues                           # the print queues this machine has
python -m labelprint print     out.4x6.pdf MyPrinter  # DRY RUN: prints the command, sends nothing
python -m labelprint print     out.4x6.pdf MyPrinter 1 --live
```

`print` is a dry run unless you pass `--live`. Everything returns JSON.

## MCP server

```json
{
  "mcpServers": {
    "label": {
      "command": "/path/to/label-printer-mcp/.venv/bin/python",
      "args": ["/path/to/label-printer-mcp/server.py"]
    }
  }
}
```

Tools: `inspect`, `prepare`, `preview`, `printers`, `send`. `send` is a dry run unless called with `live=True`, so an
assistant has to say explicitly that it means to move paper — and should show you the preview first.

## Library

```python
from pathlib import Path
import labelprint

r = labelprint.normalise(Path("return.pdf"), Path("out.4x6.pdf"))
if r.ok:
    print(r.size_in)                                    # [4.0, 6.0]
    labelprint.print_label(Path(r.out), "MyPrinter", dry_run=False)
else:
    print(r.reason)                                     # why it refused, with measurements
```

## Printer setup

Add the printer in **System Settings → Printers & Scanners**, then:

```sh
lpstat -p                    # the queue name
lpoptions -p <queue> -l      # what options it actually supports
```

That second command matters. A CUPS queue **silently ignores options it doesn't advertise**, which looks exactly like
them working — so check that your printer really takes `media=Custom.4x6in` and `print-scaling` before trusting a
label that came out looking fine.

## Tests

```sh
.venv/bin/python test_labelprint.py
```

15 tests on synthetic fixtures shaped like the real thing: a 4x6 page, a label on a letter sheet, a label **above a
packing slip** (the case the finder exists for), a landscape label, an undersized label, a packing slip alone, a blank
page. The fixtures are synthetic on purpose — a real return label carries somebody's address and tracking number.

## Requirements

macOS or Linux with CUPS (`lp`, `lpstat`), Python 3.10+, and [PyMuPDF](https://pymupdf.readthedocs.io/).
The MCP server additionally needs the [MCP SDK](https://github.com/modelcontextprotocol/python-sdk) (v2+).

## Licence

MIT.