Skip to main content
Glama
italocjs

niimbot-mcp

by italocjs
README.md
# niimbot-mcp

An MCP server + toolset to print to **NIIMBOT** BLE label printers directly —
no vendor app, no cloud. The MCP provides the *toolset and guidance*; the
calling agent composes what to print (text / QR / image).

Reverse-engineered from the community protocol work
([niimbluelib](https://github.com/MultiMote/niimbluelib),
[niimprint](https://github.com/AndBondStyle/niimprint)) and confirmed live
against a **NIIMBOT B1**.

## Hardware confirmed

- **NIIMBOT B1**, 203 dpi, 50 mm print head (384 px wide).
- BLE quirk: despite advertising the ISSC `49535343-…` UART service, the B1
  **only replies** on characteristic `bef8d6c9-9c21-4c9e-b632-bd58c1009f9f`.
  Both write and notify go through that one characteristic.

## Protocol (packet framing)

```
55 55 | TYPE | LEN | DATA… | XOR_CHECKSUM | AA AA
checksum = TYPE ^ LEN ^ (each data byte)
```

Working print flow (B1):
`SET_DENSITY 0x21` → `SET_LABEL_TYPE 0x23` → `START_PRINT 0x01` (2-byte page
count) → `START_PAGE 0x03` → `SET_DIMENSION 0x13` (6-byte: rows, cols, copies)
→ bitmap rows `0x85` (`pos:2 | black_count:3 | repeats:1 | row_data`) →
`END_PAGE 0xE3` → poll `GET_PRINT_STATUS 0xA3` → `END_PRINT 0xF3`.

> The bare 1-byte `START_PRINT` and the 4-byte `SET_DIMENSION` produce blank
> output or many copies on the B1 — use the forms above.

### ⚠️ BLE flow control (the big one)

Stream every packet with **write-WITH-response** (`write_gatt_char(..., response=True)`).
With `response=False`, the BLE stack silently drops packets under backpressure:

- ~half the rows dropped → the page **clips mid-way** (~120px on a 240px label),
  which looks exactly like a wrong label-size / geometry bug (it is not).
- all rows dropped → **nothing feeds**, which looks like a transient BLE drop or
  a dead link (it is not).

If a print clips or fails to feed, check this FIRST — before geometry, label
size, or connection theories. Also: **do not send `ALLOW_PRINT_CLEAR` (0x20)** —
the B1 does not support it and it silently breaks every subsequent print.

### Safe margin

Content is inset by a configurable `safe_margin_mm` (default 3mm) on all sides so
text/QR captions don't print off the label edge. `check_fit` accounts for it.

## Install

```bash
python3 -m venv .venv
./.venv/bin/pip install -U bleak pillow qrcode "mcp[cli]"
```

macOS: grant your terminal Bluetooth permission (System Settings → Privacy &
Security → Bluetooth) on first run.

## MCP tools

| Tool | Purpose |
|------|---------|
| `niimbot_guide` | Read first. How-to + current effective config. |
| `niimbot_get_config` | Current stored paper/printer config. |
| `niimbot_set_paper` | Persist label size / orientation / density / address. |
| `niimbot_scan` | Discover nearby printers. |
| `niimbot_status` | Live battery / paper / firmware (connects). |
| `niimbot_preview_text` / `_qr` / `_image` | Render to PNG **without** printing. |
| `niimbot_print_text` / `_qr` / `_image` | Render **and** print. |

### Orientation

- `horizontal` (default): text reads left-to-right across the label width.
- `vertical`: rotated 90°, for long thin labels.

QR and images auto-fit the label either way. Config persists to
`~/.config/niimbot-mcp/config.json`.

## Register with Kiro Crew

Add to `~/.kiro/agents/kirocrew.json` under `mcpServers`:

```json
"niimbot": {
  "command": "/Users/italo/Code/niimbot-mcp/.venv/bin/python",
  "args": ["/Users/italo/Code/niimbot-mcp/server.py"]
}
```

## Layout

```
niimbot/
  packet.py   # 55 55…AA AA framing + codec
  client.py   # async BLE client + print flow
  config.py   # persisted paper/printer settings
  render.py   # text/QR/image → oriented, label-sized 1-bit bitmap
server.py     # MCP server (toolset + guide)
scan.py probe.py diag.py  # discovery utilities used during bring-up
```