luckjingle-mcp
This server lets Claude (or any MCP client) control one or more Bluetooth thermal label printers: scanning, configuring, and printing text/images/labels to them.
Scan for nearby BLE printers and probe their GATT services to verify compatibility
Add, list, select, remove, and manage multiple named printer profiles
Set the active printer address or tune per-printer options (paper width, density, font)
Print wrapped text with alignment/font-size control
Print image files (PNG/JPG/etc), optionally dithered, resized to paper width
Compose framed labels with reusable named borders, and save/reprint labels by name
Save and manage reusable label templates and border images
Check daemon availability and get instructions for starting the required background daemon
Provides tools for discovering, probing, adding, selecting, and printing to Bluetooth LE thermal label/sticker printers, including text and image printing with configurable width, density, and font options.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@luckjingle-mcp scan for my Bluetooth label printer and print a test label"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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 overhttp://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
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.
Ask Claude to run
scan_printers(or start the daemon andcurl -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 asPPS1_6306_BLE, not any recognizable brand name) rather than the retail brand printed on the box.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.
Save it: ask Claude to run
add_printer("<name>", "<address>")- e.g.add_printer("kitchen", "AA:BB:CC:DD:EE:FF")- orcurl -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 printerprint_text/print_imageuse 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-mcpOr 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 itadd_printer(name, address, driver="luckprinter")- add or update a named printer profile; the first one added becomes activelist_printers()- list every configured printer and which is activeselect_printer(name)- switch the active printerremove_printer(name)- remove a configured printerset_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 settingsset_printer_options(width, density, font_path, name=None)- tune print quality:widthin dots (384 is standard),density0-2 (light/normal/ dark, leave unset for the printer's own default);nametargets a specific printer instead of the active oneprint_text(text, font_size=24, align="left", printer=None)- print wrapped text;printertargets a specific printer for this job onlyprint_image(image_path, dither=True, printer=None)- print an image file, resized to the paper width and converted to black & white;printertargets a specific printer for this job onlyprint_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);
ditheroverrides the automatic choice (dithered only when a border is present); passsave_asto 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_labellater
list_labels()- list every saved labelremove_label(name)- delete a saved labelprint_saved_label(name, printer=None)- reprint a label saved withsave_labelorprint_label(..., save_as=...)save_border(name, image_path)- save an image as a reusable named border thatprint_label/save_labelcan frame text withlist_borders()- list every saved border's nameremove_border(name)- delete a saved borderget_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 withprint_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. Runprobe_deviceto confirm the GATT layout matches, and checkPROTOCOL.mdin thermal-pocket-printer-basic for the full command reference if you need to adaptluckjingle_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) viaset_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:
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.Implement
PrinterDriver. Readluckjingle_mcp/drivers/base.pyfor the interface, andluckjingle_mcp/drivers/luckprinter.pyfor 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 byPrinterSession.Register it by adding your driver class to the
DRIVERSdict inluckjingle_mcp/drivers/__init__.py, keyed by the name people will pass toadd_printer(..., driver="...").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).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 -vTests 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 toolsadd_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.
print_imageA
Print an image file (PNG/JPG/etc) on the label printer.
The image is resized to the printer's configured paper width and converted to black & white. Leave dither=True for photos/gradients; set dither=False for already-black-and-white line art or text to keep edges crisp. printer names which configured printer to use for this one print job, overriding the active printer - leave unset to use the active printer.
Inputs: image_path (required), dither, printer. Returns: result
print_textB
Print a block of text on the label printer, wrapped to the paper width.
align may be "left", "center", or "right". printer names which configured printer to use for this one print job, overriding the active printer - leave unset to use the active printer.
Inputs: text (required), align, printer, font_size. Returns: result
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
Scored across 12 tools
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.
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.
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.
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
Related MCP Connectors
Thermal label MCP server: render, validate, debug and convert ZPL, EPL, TSPL and CPCL labels.
Create and manage dynamic QR codes and read their scan analytics from Claude and other MCP clients.
Twinr turns any website into native iOS and Android apps, and 50,000+ businesses have used it to launch on the App Store and Google Play. The Twinr MCP server brings your app into the AI tools you already work in. Connect Claude, ChatGPT, Cursor, Lovable or any MCP client to your Twinr account, and run your app by asking: check on builds, refresh your branding and start new builds, without opening the dashboard.
Automate 1,000+ services from any MCP-compatible AI agent: build Applets, run actions and queries.
Related MCP Servers
- AlicenseAqualityDmaintenanceConnect AI agents to physical printers. Print receipts, shipping labels, and packing slips to your existing BizPrint-connected printers from Claude and other MCP clients.71MIT
- AlicenseAqualityCmaintenanceEnables 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.51MIT
- AlicenseNot gradedqualityBmaintenanceEnables printing labels on Brother P-touch tape printers via MCP, allowing natural language requests to create, preview, and print labels.MIT
- AlicenseAqualityCmaintenanceEnables 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.82MIT