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
```
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues