label-printer-plugin

August 16, 2026 · View on GitHub

Print labels on Brother QL and P-touch label printers from Claude Code — text and QR labels, at the size of whatever roll is loaded, on whichever printer you name.

There are two ways to use this, and they are independent.

Standalone (default). Nothing to deploy. The plugin discovers printers on this machine and the LAN, keeps a registry of them, renders labels itself and sends them straight to the printer.

Claude Code ──▶ scripts/render.py ──raw TCP :9100──▶ Brother QL
                (renders PNG)     └─USB via ptouch-print──▶ Brother P-touch

Via the bundled MCP server. The older path, still supported: a streamable-HTTP MCP that renders labels and posts them to a small USB print bridge. P-touch only, and it needs the bridge deployed first — see Running the MCP server.

Claude Code ──MCP──▶ label-printer ──HTTP──▶ print bridge ──USB──▶ Brother P-touch
                     (renders PNG)           (:9180)               (24mm tape)

Quick start

/plugin marketplace add danielrosehill/Claude-Code-Plugins
/plugin install label-printer

Then, in Claude Code:

find my label printers

print a label saying "Spare fuses / Garage shelf B" with a QR to https://inv.example/a/42

Or drive the scripts directly:

python3 scripts/labelctl.py discover
python3 scripts/labelctl.py registry add --id ql-810w --model QL-810W \
  --host 192.168.68.63 --description "Office shelf" --media DK-22210
python3 scripts/render.py preview --text "Spare fuses" --qr https://inv.example/a/42 -o /tmp/l.png
python3 scripts/render.py print   --text "Spare fuses" --qr https://inv.example/a/42

preview never opens a socket to the printer — see No dry run.

Skills

SkillPurpose
profile-printersFind label printers and register them with a name and description
print-labelPrint a label at a chosen media size
qr-labelsPrint a label carrying a QR code
label-templatesSave a label you print repeatedly, and print from it
install-driversInstall the Linux tooling — brother_ql, ptouch-print, mDNS, CUPS
label-an-assetTag one item — asset number, name, QR to its page (MCP)
label-a-storage-unitTag a box, bin or shelf (MCP)
batch-label-runLabel a whole shelf, or generate labels from a CSV (MCP)
printer-troubleshootingNothing printed — work out why

/label routes to any of them.

Printer support

BackendModelsTransportStatus
brother_ql_networkBrother QL-500 … QL-1060Nraw TCP :9100working
brother_ql_usbsame, over USBbrother_ql -b pyusbworking
ptouch_usbBrother PT (P-touch)ptouch-printworking
cupsanything with a CUPS queuelp -dworking
zebra_wipZebra ZD/ZT/GK/GX/ZQdiscovered and registered, not yet printable
dymo_wipDYMO LabelWriterdiscovered and registered, not yet printable

Verified end to end 2026-08-16 against a Brother QL-810W over the network, on Ubuntu with brother_ql 0.9.4 and Pillow 12.1.1. Printing to a *_wip backend fails with an explicit message rather than attempting anything. What a Zebra backend would involve is sketched in docs/backends.md.

Where things are stored

Two locations, deliberately separate. Ask scripts/paths.sh --json rather than hard-coding either.

PathCommitted?
Printer registry — LAN addresses, true on one network~/.claude-plugins/label-printer/printers.jsonnever
Templates — labels you reuse, worth mirroring<user-data-root>/label-printer/templates/yes, in your own repo

The user-data root is $CLAUDE_USER_DATA, else whichever of ~/.claude-user-data or ~/.local/share/claude-plugins already exists, else ~/.claude-user-data. It adopts an existing root rather than starting a second one, and reports which rule fired.

Media

reference/media-catalogue.json carries DK and TZe product codes with their pixel geometry — 24 DK geometries and 7 TZe widths, plus the product codes that map onto them.

python3 scripts/labelctl.py media list
python3 scripts/labelctl.py media show DK-11202

The registry records which roll is loaded, and that decides the canvas. Continuous rolls grow to fit the text, capped at 90 mm; die-cut labels are a fixed length and text shrinks to fit. An installed_media that does not match the physical roll produces a wrongly-sized label rather than an error.

No dry run

brother_ql print converts and transmits in a single step. There is no dry-run flag, and --no-cut suppresses the cutter, not the print. So render.py preview is a pure Pillow path that never opens a socket — it is the only safe way to see a label before spending media. Its output always carries "printed": false.

Other findings that cost time to work out — brother_ql discover being USB-only and broken without -b, networked label printers not appearing in CUPS at all, the harmless deprecation warning on every invocation — are in docs/backends.md.

Label layout

Both renderers put the QR square on the left, the heading in large bold type, and an optional second line beneath it. Tape narrower than about 18 mm drops the second line — there is no room for it.

The standalone renderer sizes the canvas from the loaded media and caps a continuous label at 90 mm, shrinking type to fit rather than stretching the roll. On die-cut media the QR is additionally capped at 45% of the label length so it cannot squeeze the text into a sliver.

The rest of this section describes the MCP renderer, whose behaviour differs:

QR labels are a uniform 62 mm along the tape: QR square on the left, heading in large bold type, optional second line beneath it. Long names wrap to two lines rather than stretching the tape. A heading too wide to shrink legibly widens the label instead, up to a 120 mm cap.

Example asset label

Tape narrower than 18 mm drops the second line — there is no room for it.

Hebrew and other RTL text

Hebrew works in either field, on its own:

print_text_label(text="נקודת מסירת משלוחים")

Two things are handled for you. Font fallback: Inter carries no Hebrew glyphs at all, so any string containing Hebrew or Arabic switches to the vendored Noto Sans Hebrew faces — otherwise you get blank tape, not an error. Bidi: Pillow is normally built without libraqm, so it lays glyphs out left-to-right and will not reorder RTL text itself. The server applies the bidi algorithm explicitly and pins the layout engine to BASIC, so the result is identical whether or not your Pillow has raqm.

Arabic will pick the same fallback font and be reordered correctly, but contextual letterforms are not shapedBASIC layout does no glyph joining. Hebrew needs no joining, so it is unaffected. If you need correct Arabic, install a raqm-enabled Pillow, drop the _shape() call and the BASIC pin, and let raqm do both.

Mixing scripts on one label does not work — use artwork instead

The font is chosen per label, not per run: one Hebrew character anywhere sends the whole label to Noto Sans Hebrew, and that face has no Latin glyphs, so the Latin half comes out as tofu boxes (□□□□). Splitting across heading and subtext does not help — the choice is made once for both.

print_label(heading="Rosehill | רוזהיל")     # -> "□□□□□□□□ | רוזהיל"

Rather than a per-run font fallback, render the label yourself and submit it — see Custom artwork below. That also fixes bidi properly, since a raqm-enabled Pillow on the rendering side does real mixed-direction layout.

Custom artwork

When the built-in layouts can't express the design — mixed scripts, a logo, custom typography, columns — render your own bitmap and print it:

label_canvas()
# {'tapeMm': 24, 'heightPx': 288, 'maxWidthPx': 3600, 'maxLengthMm': 300,
#  'scalePxPerMm': 12, ...}

print_image_label(image="<base64 PNG>", copies=1)

The tape width is the label's height; the label runs as long as you like along the tape. Render at heightPx for the loaded tape (12 px/mm — the bridge downscales to the 180 dpi head and trims the blank leader, so no end margins are needed).

  • image — PNG or JPEG, as a data: URL or a bare base64 string. Wrapped base64 is fine. 8 MiB decoded cap.
  • fitscale (default) resizes proportionally to the tape height; pad keeps the artwork's own scale and centres it across the tape; exact refuses anything whose height isn't already the tape height.
  • Transparency is composited onto white. Submitting RGBA without this would print a solid black slab, since transparent converts to black rather than to bare tape.
  • Length is capped at 300 mm, looser than the 120 mm auto-layout cap — that one guards against a long string stretching tape, which doesn't apply to artwork composed deliberately.

Use preview_image_label to see the exact bitmap that would be sent, before spending tape.

Running the MCP server

Everything below is for the MCP path only. Skip it entirely if you are using the standalone scripts.

The bridge, the ptouch-print patch some models need, and the hardware notes live in ptouch-cube-print-bridge. Set that up first — the MCP path does not work without it.

Tools

ToolWhat it does
printer_statusBridge reachability, loaded tape width, printer error word
printer_capabilitiesWhat the driver does and does not expose — check before promising a setting
preview_labelRender a label and return the image without printing
print_labelGeneral-purpose: heading, optional subtext, optional QR, N copies
print_text_labelText only, no QR, up to 3 stacked lines
print_asset_labelInventory asset template — A- prefix, name underneath
print_storage_labelStorage unit template — S- prefix, contents underneath
print_batchUp to 50 different labels in one run
label_canvasThe pixel canvas to render your own artwork onto
preview_image_labelShow submitted artwork as it would reach the printer, without printing
print_image_labelPrint arbitrary PNG/JPEG artwork you rendered yourself

Point the plugin at your deployed MCP:

export LABEL_PRINTER_MCP_URL=http://<host>:<port>/mcp

Source is in mcp-server/. It speaks streamable HTTP on :3000 by default, or stdio with MCP_TRANSPORT=stdio.

docker build -t label-printer-mcp mcp-server/
docker run -d --name label-printer -p 3000:3000 \
  -e PRINT_BRIDGE_URL=http://<bridge-host>:9180 \
  label-printer-mcp

Configuration

VariableDefaultMeaning
PRINT_BRIDGE_URLhttp://127.0.0.1:9180Where the print bridge listens
LABEL_WIDTH_MM62Preferred QR-label length along the tape
LABEL_TAPE_MM24Fallback tape width when the printer is off
ASSET_PREFIXA-Prefix applied by print_asset_label
STORAGE_PREFIXS-Prefix applied by print_storage_label
FONT_BOLD / FONT_REGULARvendored InterLatin font paths
FONT_HEBREW_BOLD / FONT_HEBREW_REGULARvendored Noto Sans HebrewUsed automatically for Hebrew/Arabic text
MCP_TRANSPORTstreamable_httpOr stdio
MCP_HTTP_HOST / MCP_HTTP_PORT0.0.0.0 / 3000HTTP bind

The MCP has no authentication — bind it to a trusted LAN or VPN, or put it behind a gateway that authenticates.

Linux users: read this before hunting for settings

ptouch-print is a print-only driver. It exposes no device settings at all, so auto power-off cannot be changed from Linux — not through the driver, not over Bluetooth, not in the mobile apps, and not under Wine. It is a one-time change in Brother's Windows/macOS Printer Setting Tool, stored in the printer's NVRAM.

The printer also cannot be woken over USB once it sleeps. Someone has to press the button.

Full details, the complete option list, and the measurements behind the 60-minute figure: docs/linux-printer-settings.md.

Tested on

Standalone path — Brother QL-810W over the network (brother_ql_network, tcp://…:9100), DK-22210 continuous and DK-11202 die-cut, on Ubuntu with brother_ql 0.9.4, Python 3.14 and Pillow 12.1.1. Verified end to end 2026-08-16: mDNS discovery, registry, preview and print. brother_ql_usb, ptouch_usb and cups follow the same code path but no hardware was on hand for them.

MCP path — Brother PT-P710BT (P-Touch Cube Plus), 24 mm laminated tape, ptouch-print v1.8, Ubuntu. ptouch-print also supports PT-P700, PT-P750W, PT-D460BT, PT-D610BT, PT-E550W, PT-P900Wc and others — the rendering there is model-independent, but only the P710BT has been exercised end to end.

License

MIT — see LICENSE. Vendored fonts are SIL OFL 1.1: Inter (mcp-server/fonts/OFL.txt) and Noto Sans Hebrew (mcp-server/fonts/OFL-NotoSansHebrew.txt). ptouch-print is a separate GPL-3.0 project.