Skip to main content
Glama

luckjingle-mcp

An MCP server that lets Claude (or any MCP client) design and print things to cheap Bluetooth thermal label/sticker printers, and manage more than one of them - print to whichever one you select, or name one explicitly per job.

Printer support is pluggable: each printer family is a small driver (see "Adding support for a new printer" below). Right now there's one driver, luckprinter, covering "LuckPrinter SDK" printers - the ones sold under names like NHOWIN, NDYIN, C&Co 3128, DP-L1S, etc, that all run on the "LuckPrinter SDK" behind the "Luck Jingle" phone app (159+ rebrand models share this SDK).

These printers don't publish an official protocol or SDK. luckprinter talks to them using a protocol reverse engineered by the community by decompiling the Android app (see NOTICE.md for credits/ sources): a standard ESC/POS GS v 0 raster image command plus a handful of vendor-specific control commands. If your printer's BLE service exposes characteristics ff01/ff02 under service ff00 (check with probe_device, see below) it's likely compatible as-is - but see the "Adding support for a new printer" section if a test print doesn't work, since more than one incompatible protocol shares that same GATT layout.

Other servers in this repo

This repo also has netprint-mcp - a separate, much simpler MCP server for printing documents (PDFs, images, etc) to a regular network printer already set up on this machine, via CUPS. No BLE, no daemon - see its own README for setup.

Related MCP server: labelprint

Architecture

This is split into two processes, which is unfortunately necessary on macOS:

  • luckjingle-daemon - owns the actual Bluetooth connection. Must run as its own independent app (see "macOS Bluetooth permission" below), not as a subprocess of Claude.

  • luckjingle-mcp - the MCP server Claude talks to. It's a thin client that forwards tool calls to the daemon over http://127.0.0.1:8765.

Requirements

  • Python 3.10+

  • macOS, Linux, or Windows with Bluetooth LE support (uses bleak)

Install

cd /path/to/iocane   # wherever you cloned this repo
python3 -m venv .venv
.venv/bin/pip install -e .

macOS Bluetooth permission (important)

On macOS, any process descending from an app that hasn't declared Bluetooth usage in its Info.plist will hard-crash (not prompt) the instant it touches CoreBluetooth. Since this MCP server runs as a subprocess of whatever launched Claude, that includes it. macos_app/LuckJingleDaemon.app works around this: it's a minimal app bundle that declares NSBluetoothAlwaysUsageDescription, so macOS can show a normal permission prompt for it instead.

You must start the daemon by opening this app directly - not from inside Claude/a terminal spawned by Claude:

open /path/to/iocane/macos_app/LuckJingleDaemon.app

(The bundle's launcher script locates its own venv relative to itself, so this works from whatever path you cloned the repo to. If the daemon doesn't start and nothing obvious shows up, check ~/Library/Logs/luckjingle-daemon.log.)

The first time, macOS will show a Bluetooth permission prompt attributed to python3.14 (or similar) - accept it. It's a background app (no dock icon, no window) that just listens on 127.0.0.1:8765; leave it running whenever you want to print. If you want it running automatically, add it to System Settings → General → Login Items.

Known glitch: the permission dialog can sometimes get visually stuck (looks like clicking "Allow" does nothing). If that happens, check whether it actually took effect before assuming otherwise - macOS's own dialog rendering can lag behind the real permission state; killing and reopening the daemon app is usually enough to get a clean prompt.

On Linux/Windows this split isn't necessary, but the daemon/client architecture works the same way regardless - just run luckjingle-daemon however you'd normally run a background service.

Find and verify your printer

  1. Power on the printer and make sure your phone isn't already connected to it via its own app (force-quit the app, or turn the phone's Bluetooth off temporarily) - a printer already connected elsewhere won't advertise for a new connection.

  2. Ask Claude to run scan_printers (or start the daemon and curl -X POST http://127.0.0.1:8765/scan -d '{"timeout": 10}'). Results are sorted by signal strength - your printer is usually the strongest signal since it's right next to you, but cheap BLE modules often advertise a generic factory name (ours showed up as PPS1_6306_BLE, not any recognizable brand name) rather than the retail brand printed on the box.

  3. Before trusting a name-based guess, confirm it with probe_device (address) - a compatible printer's GATT services should include:

    service 0000ff00-... with characteristics:
      0000ff01-...  (notify)
      0000ff02-...  (write)

    This UUID pattern is a common generic BLE-serial scheme also used by unrelated devices, so a name match alone isn't proof - but the service layout combined with a signal-strength match is a strong signal.

  4. Save it: ask Claude to run add_printer("<name>", "<address>") - e.g. add_printer("kitchen", "AA:BB:CC:DD:EE:FF") - or curl -X POST http://127.0.0.1:8765/add_printer -d '{"name": "kitchen", "address": "..."}'. The first printer you add becomes the active one automatically. (If you only ever have one printer, set_printer_address(address) is a shorter equivalent that skips naming it.)

Printer profiles are saved to ~/.config/luckjingle-mcp/config.json and reused for future print jobs, so you only need to do this once per printer.

Managing multiple printers

  • list_printers() - see every configured printer and which one is active.

  • select_printer("<name>") - switch which printer print_text/ print_image use by default.

  • print_text(..., printer="<name>") / print_image(..., printer="<name>")

    • print to a specific printer for one job, without switching the active one.

  • set_printer_options(..., name="<name>") - tune width/density/font for a specific printer instead of the active one.

  • remove_printer("<name>") - drop a configured printer. If it was active, another remaining one (if any) becomes active.

Register the MCP server with Claude Code

claude mcp add luckjingle -- /path/to/iocane/.venv/bin/luckjingle-mcp

Or add directly to your MCP config (e.g. ~/.claude.json or .mcp.json):

{
  "mcpServers": {
    "luckjingle": {
      "command": "/path/to/iocane/.venv/bin/luckjingle-mcp"
    }
  }
}

This just needs to be done once; the daemon app still needs to be started separately each time you want to print (see above).

Tools

  • scan_printers(timeout=6.0) - list nearby BLE devices (name, address, signal strength)

  • probe_device(address, timeout=15) - connect and list GATT services/characteristics, to verify a candidate device before adding it

  • add_printer(name, address, driver="luckprinter") - add or update a named printer profile; the first one added becomes active

  • list_printers() - list every configured printer and which is active

  • select_printer(name) - switch the active printer

  • remove_printer(name) - remove a configured printer

  • set_printer_address(address) - shortcut for the single-printer case: updates the active printer's address (creating one named "default" if none exists yet)

  • get_printer_config() - show the active printer's saved settings

  • set_printer_options(width, density, font_path, name=None) - tune print quality: width in dots (384 is standard), density 0-2 (light/normal/ dark, leave unset for the printer's own default); name targets a specific printer instead of the active one

  • print_text(text, font_size=24, align="left", printer=None) - print wrapped text; printer targets a specific printer for this job only

  • print_image(image_path, dither=True, printer=None) - print an image file, resized to the paper width and converted to black & white; printer targets a specific printer for this job only

  • print_label(text, font_size=24, align="center", border=None, dither=None, save_as=None, printer=None)

    • compose and print a text label, optionally framed by a saved border (e.g. "Flux Capacitor" with the "dancing kittens" border); dither overrides the automatic choice (dithered only when a border is present); pass save_as to also save it for reprinting later

  • save_label(name, text, font_size=24, align="center", border=None, dither=None)

    • save a label without printing it, for print_saved_label later

  • list_labels() - list every saved label

  • remove_label(name) - delete a saved label

  • print_saved_label(name, printer=None) - reprint a label saved with save_label or print_label(..., save_as=...)

  • save_border(name, image_path) - save an image as a reusable named border that print_label/save_label can frame text with

  • list_borders() - list every saved border's name

  • remove_border(name) - delete a saved border

  • get_daemon_info() - checks whether the daemon is reachable and explains how to start it if not

To print a custom design (a label with a logo, a QR code, a styled layout, etc), have Claude generate a PNG with whatever tool/script fits, save it to disk, then call print_image with that path.

Saved labels and borders

  • Labels are a saved (text, font size, alignment, border) combo you can reprint by name later without repeating everything - either save one up front with save_label, or print now and save in the same call with print_label(..., save_as="...").

  • Borders are a saved image (e.g. a strip of clipart) that frames a label's text. Save one with save_border("dancing kittens", "/path/to/ kittens.png"), then reference it by name: print_label("Flux Capacitor", border="dancing kittens"). The text always sits on a plain white panel in the middle so it stays legible regardless of the border's own colors - the border image itself is tiled to frame that panel, so a short repeating strip works better than one large single graphic.

Saved labels and border images live alongside the printer config, in ~/.config/luckjingle-mcp/ - config.json for the label/border metadata, borders/ for the saved border image files themselves.

Troubleshooting

  • Any MCP tool call fails with a connection error: the daemon app isn't running - see "macOS Bluetooth permission" above.

  • Connects (printer's LED changes) but nothing physically prints: the printer probably isn't in the luckprinter (LuckPrinter SDK) family this driver targets, or uses a variant that builds the image command differently. Run probe_device to confirm the GATT layout matches, and check PROTOCOL.md in thermal-pocket-printer-basic for the full command reference if you need to adapt luckjingle_mcp/drivers/luckprinter.py, or write a new driver (see below).

  • Printer turns itself off unexpectedly: this happened during development and turned out to be a bug in an earlier version of this driver (an "disable auto-shutdown" command that actually set the auto-shutdown timer to 0). If you see it again, check nothing is sending 10 FF 12 00 00.

  • Prints too light/dark: set density (0-2) via set_printer_options.

Adding support for a new printer

Support for a printer family is a small "driver" object, not a fork of this project. If probe_device shows a GATT layout this repo doesn't already handle, or a compatible-looking printer just doesn't print correctly with the luckprinter driver (see NOTICE.md - more than one incompatible protocol shares that same ff01/ff02 layout, so a matching GATT layout alone doesn't guarantee compatibility), here's how to add one:

  1. Reverse-engineer the protocol. These printers don't publish an SDK, so this means decompiling the vendor's Android/iOS app (e.g. with JADX) and finding the BLE write calls, the same way NOTICE.md's sources did for luckprinter. You're looking for: the GATT write/notify characteristic UUIDs, how the printer is woken/enabled, how a print darkness/density setting is sent (if supported), how a raster image command is framed, and how a job is fed/ended.

  2. Implement PrinterDriver. Read luckjingle_mcp/drivers/base.py for the interface, and luckjingle_mcp/drivers/luckprinter.py for a complete worked example - it's under 40 lines. You only supply command bytes and GATT characteristics; image resizing, dithering, bitmap packing, chunked writes, and text rendering are all shared and already handled by PrinterSession.

  3. Register it by adding your driver class to the DRIVERS dict in luckjingle_mcp/drivers/__init__.py, keyed by the name people will pass to add_printer(..., driver="...").

  4. Add protocol tests. See tests/test_printer_protocol.py - these use a fake transport that just records what was written, so you can pin down your driver's exact command bytes without needing the hardware present for every test run (you'll obviously still want to confirm against real hardware before calling it done).

  5. Open a PR. Mention what hardware you tested against - a driver that's only ever run against the one printer its author owns is worth having, but should say so.

Development

.venv/bin/pip install -e ".[test]"
.venv/bin/pytest -v

Tests don't need real printer hardware - they use fake transports that record what would have been sent over BLE/serial, so they run the same on a laptop with no printer nearby.

Available Tools

12 tools
add_printerA

Add (or update, if the name already exists) a named printer profile. If this is the first printer configured, it becomes the active one automatically - otherwise use select_printer to switch to it.

driver identifies the printer's protocol family - "luckprinter" (the default) covers the LuckPrinter-SDK rebrand family (NHOWIN, PPS1, C&Co 3128, DP-L1S, etc) and is the only driver available so far.

Inputs: name (required), address (required), driver.

get_daemon_infoA

Explain how to start the required LuckJingle print daemon, and whether it is currently reachable.

Inputs: no named parameters. Returns: result

get_printer_configA

Return the active printer's saved configuration (address, driver, paper width, etc). Use list_printers to see every configured printer.

Inputs: no named parameters.

list_printersA

List every configured printer (name, driver, address, settings) and which one is currently active - the one print_text/print_image use when no printer is named explicitly.

Inputs: no named parameters.

probe_deviceA

Connect to a BLE device and list its GATT services/characteristics.

Useful for confirming a candidate address (found via scan_printers) is really a supported printer before adding it - a compatible printer should expose a custom service containing characteristics ending in ff01, ff02, and ff03. Note that this GATT layout alone doesn't tell you which driver to use - more than one incompatible protocol variant shares it - so still confirm with a real test print after adding it.

Inputs: address (required), timeout. Returns: result

remove_printerA

Remove a configured printer. If it was the active one, another remaining configured printer (if any) becomes active.

Inputs: name (required).

scan_printersA

Scan for nearby Bluetooth LE devices to find the printer's address.

Put the printer in pairing mode (usually: hold its power button, or open its companion app once) before scanning, then look for a device name that resembles the printer (e.g. containing "D1", "GB", "Q3", "Print", or a string of letters/numbers) in the results.

Requires the LuckJingle print daemon to be running (see get_daemon_info).

Inputs: timeout. Returns: result

select_printerA

Set which configured printer print_text/print_image use by default when no printer is named explicitly on the call.

Inputs: name (required).

set_printer_addressA

Save the Bluetooth address of the printer for future print jobs.

This is a shortcut for the common single-printer case - it updates whichever printer is currently active (creating one named "default" if none exists yet). To configure more than one printer, use add_printer instead, which lets you name each one.

Inputs: address (required). Returns: result

set_printer_optionsA

Tune print quality for a printer. width is the paper width in dots (384 is standard for common 48mm/58mm thermal labels - only change it if prints come out cropped or skewed). density (0=light, 1=normal, 2=dark) adjusts print darkness if prints look too light/dark - leave unset to use the printer's own default. font_path points to a .ttf/.otf file to use for print_text instead of the built-in fallback. name picks which configured printer to change settings for - leave unset to use the active printer.

Inputs: name, width, density, font_path.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A4.2/5.0

    Scored across 12 tools

    Disambiguation5/5

    Each tool targets a distinct action: scanning, probing, configuring, selecting, removing, printing, and daemon status are all clearly separated. The only potentially overlapping tools (set_printer_address vs add_printer, get_printer_config vs list_printers) are explicitly differentiated in their descriptions.

    Naming Consistency5/5

    Every tool follows a consistent snake_case verb_noun pattern, such as remove_printer, scan_printers, add_printer, print_text, and get_daemon_info. There are no mixed naming conventions or vague generic verbs.

    Tool Count5/5

    12 tools is well-scoped for this server's purpose: device discovery, printer profile management, configuration, and printing are all covered without unnecessary duplication. Each tool earns its place in the workflow.

    Completeness5/5

    The tool surface covers the full lifecycle: scanning and probing to find a device, adding/updating/removing printers, selecting an active printer, configuring options, checking daemon status, and printing text or images. There are no obvious dead ends or missing core operations for the stated label-printer domain.

    Maintenance

    ActivityStale
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      A
      quality
      D
      maintenance
      Connect AI agents to physical printers. Print receipts, shipping labels, and packing slips to your existing BizPrint-connected printers from Claude and other MCP clients.
      7
      1
      MIT
    • A
      license
      A
      quality
      C
      maintenance
      Enables printing PNG labels to a nearby thermal label printer over local serial or Bluetooth, with tools for rendering previews, checking printer status, and printing with confirmation.
      5
      1
      MIT
    • A
      license
      Not graded
      quality
      B
      maintenance
      Enables printing labels on Brother P-touch tape printers via MCP, allowing natural language requests to create, preview, and print labels.
      MIT
    • A
      license
      A
      quality
      C
      maintenance
      Enables AI agents to print Markdown cards, checklists, and pictures on Phomemo M02 Pro thermal printers over Bluetooth, as well as check printer status and manage print jobs.
      8
      2
      MIT