# Handoff: Initial mcp-pos-print build ## Session Metadata - Created: 2026-05-25 19:37:09 - Project: /home/claude/bin/mcp-pos-print - Branch: main - Session duration: ~1 hour (scope → design → build → register → push) ### Recent Commits (for context) - 421c3c9 feat: initial MCP server for printing markdown to Epson TM-m30 ## Handoff Chain - **Continues from**: None (fresh start) - **Supersedes**: None > This is the first handoff for this task. ## Current State Summary Built and shipped a stdio MCP server (`mcp-pos-print`) that exposes a single `print` tool taking markdown and rendering it to the Epson TM-m30 receipt printer (192.168.0.184:9100). Mirrors the `mcp-gemma4` layout exactly. Code committed and pushed to Gitea (https://git.sethpc.xyz/Seth/mcp-pos-print). MCP registered in `~/.claude.json` next to `gemma4`. Smoke test sent 4508 ESC/POS bytes successfully — physical paper output **not yet visually confirmed by Seth** as of handoff time. ## Codebase Understanding ### Architecture Overview Single-file stdio MCP server (`server.py`). Three layers: 1. **Inline parser** (`parse_inline`) — regex-splits a line on `**bold**` / `` `code` `` / `[text](url)` and returns `[(style, text), ...]` tuples. 2. **Width-aware wrapper** (`wrap_segments`) — wraps styled segments at 57 cols (Font B on 80mm paper) without breaking inside a styled run; supports hanging indent for continuation lines. 3. **Block parser** (`render_markdown`) — walks markdown line-by-line, dispatches on heading / hr / list / numbered / blockquote / fenced-code / blank / paragraph; calls the inline parser + wrapper for body text. `build_receipt` composes title + timestamp + body + QR codes + cut into ESC/POS bytes via `escpos.printer.Dummy`. `send_bytes` opens TCP, `sendall`, closes. ### Critical Files | File | Purpose | Relevance | |------|---------|-----------| | `server.py` | MCP server + markdown→ESC/POS translator | Core — every change goes here | | `test_server.py` | Self-test that prints a self-documenting page exercising every feature | Run after any parser change | | `pyproject.toml` | Deps: `mcp>=1.0.0`, `python-escpos>=3.0`, `qrcode>=7.0` | | | `DECISIONS.md` | Settled architecture choices + rejected alternatives | Read before proposing a redesign | | `~/bin/POS_PRINT.md` | Printer reference (IP, Font B, native QR, troubleshooting) | Authoritative — server intentionally mirrors its conventions | | `~/bin/mcp-gemma4/server.py` | Reference MCP layout this project mirrors | Pattern source | ### Key Patterns Discovered - **stdio MCP pattern in `~/bin/`**: venv at `.venv/`, deps in `pyproject.toml`, single `server.py` with `Server(...)` + `stdio_server()`, registered in `~/.claude.json` with absolute path to `.venv/bin/python`. Mirror this for any future MCP. - **ESC/POS formatting is stateful**: `p.set(bold=True)` flips a flag that stays on until flipped off. Parser must emit toggles around styled runs, not blindly append. - **Word wrap must happen after inline parse, not before**: naive wrap on the raw markdown string lands breaks inside `**bold**` markers. Parse inline runs into `[(style, text)]` first, then wrap on the style-aware token stream. ## Work Completed ### Tasks Finished - [x] Scaffold project per `~/bin/CREATE_PROJECT.md` (dir, GITEA_API.md symlink, IDEA.md, DECISIONS.md, CLAUDE.md, `.claude/handoffs/`, `.backup/`) - [x] Write `server.py` (~280 lines): markdown parser + wrapper + render + send - [x] Write `pyproject.toml`, `README.md`, `test_server.py`, `.gitignore` - [x] Create venv, install deps (`pip install -e .`) - [x] Smoke test: 4508 bytes accepted by printer at 192.168.0.184 - [x] Back up `~/.claude.json` before edit (per global CLAUDE.md backup rule) - [x] Register `pos-print` in `~/.claude.json` mcpServers - [x] `git init` + commit + create Gitea repo + push ### Files Modified | File | Changes | Rationale | |------|---------|-----------| | `~/.claude.json` | Added `mcpServers.pos-print` entry pointing at `.venv/bin/python` + `server.py` | Required to expose tool to Claude sessions | | All files in `~/bin/mcp-pos-print/` | Created from scratch | New project | ### Decisions Made | Decision | Options Considered | Rationale | |----------|-------------------|-----------| | Single `print` tool, markdown body | (a) markdown, (b) structured sections JSON, (c) many primitive tools, (d) hybrid | Seth chose (a). Claude already speaks markdown; no printer-specific schema to learn at the call site. | | Hand-rolled line-by-line parser | vs. `markdown-it-py` | Our subset (headings/bold/code/hr/lists/blockquotes/links) is small enough that the dependency would be heavier than the parser. | | Font B (57 cols) hard-coded | vs. configurable font | POS_PRINT.md mandates Font B for 80mm paper; Font A column math is wrong. | | `qr_urls` kwarg vs. inline QR syntax | | Keeps body pure text. Auto-QR for any link would surprise users with documents that have many links. | | Stdio transport | vs. SSE / HTTP | Matches `mcp-gemma4` and Claude Code's native MCP loading; no network exposure of the MCP itself. | Full rationale in `DECISIONS.md` (including rejected alternatives — image printing, primitive tools, drawer/buzzer, truncation). ## Pending Work ## Immediate Next Steps 1. **Seth visually verifies the smoke-test page.** If garbled (extra line breaks, wrong widths, missing bold, mis-aligned wrap), debug `wrap_segments` first — most likely culprit. 2. **In the next Claude Code session, run `/mcp` (or restart)** to load the new `pos-print` server. Then test from within a session: `pos-print:print(markdown="# hi\n\n**works**.")`. 3. Optional: if Seth wants the test page printed again post-fix, run `~/bin/mcp-pos-print/.venv/bin/python ~/bin/mcp-pos-print/test_server.py`. ### Blockers/Open Questions - [ ] Physical print quality unverified. Smoke test reports "OK — bytes sent" but cannot see paper output. - [ ] Hardware QR (`native=True, size=6`) untested with a real URL — the test page includes one (`https://git.sethpc.xyz/Seth/mcp-pos-print`). If QR doesn't scan, drop size or check `qrcode` package install. ### Deferred Items All explicitly listed in `DECISIONS.md` under "Deferred / Rejected": image printing, primitive tools (`print_qr` / `print_barcode` / `print_separator`), drawer-kick, buzzer, multi-receipt batching, auto-QR for inline links, input truncation. Add a tool only if a real use case appears. ## Context for Resuming Agent ## Important Context - **This MCP only works on hosts with LAN access to 192.168.0.184.** Steel141 has it. Vast.ai / rosy / matt-strix do not. The MCP entry in `~/.claude.json` is in this user's profile on this machine — no broadcast to other hosts. - **Backup convention was followed:** `~/.claude.json` backed up to `/home/claude/.claude/.backup/claude-json-1779742244.json` before edit. Restore from there if the MCP registration ever breaks Claude Code startup. - **The smoke test page is intentionally self-documenting** — it covers every supported markdown feature. Keep it that way when extending; it doubles as regression eyeball-testing. - **Brainstorming skill's design-doc + writing-plans dance was deliberately skipped** for this build. Justification: design was explicit and approved by Seth in chat, scope was bounded (single-file MCP mirroring a known pattern). Don't replicate the skip for larger work. ### Assumptions Made - Seth runs Claude Code as the `claude` user on steel141 (matches CLAUDE.md). MCP path is hard-coded to `/home/claude/bin/mcp-pos-print/.venv/bin/python`. - TM-m30 firmware accepts the standard escpos library's Font B + native QR — verified working in `pos_briefing.py` for ages. - Timezone for the timestamp header is Eastern (`America/New_York`) — matches `pos_briefing.py`. ### Potential Gotchas - **Code blocks truncate over-width lines** with a `»` continuation marker rather than wrapping. Intentional — wrapping code mangles it more than truncation does. If a user complains, consider switching to a 2-character indent on continuation. - **The `code` inline style currently renders identically to `plain`** — Font B is already monospace, and ESC/POS has no shading worth using on 57 cols. Don't waste effort on visual differentiation here. - **`p.set(font='b')` must be re-asserted after `p.qr(...)` and `p.cut()`** — they appear to reset some state. Already handled in `build_receipt` but easy to forget when extending. - **The Dummy printer profile is `"default"`** — POS_PRINT.md confirms this is correct for TM-m30. Don't pass a model-specific profile or you'll get errors from python-escpos. - **Markdown input larger than ~50KB will eventually be a problem** — not because of the parser, but because the TM-m30 has a 4KB receive buffer (per POS_PRINT.md). Long prints might stall waiting for the printer to drain. Hasn't been tested. ## Environment State ### Tools/Services Used - **Epson TM-m30 receipt printer** at 192.168.0.184:9100 (LAN, no auth, raw TCP) — ping latency 0.4ms from steel141. - **Gitea** at git.sethpc.xyz (CT 146 / 192.168.0.125) — repo created at `Seth/mcp-pos-print`, pushed via the `~/bin/gitea` CLI wrapper. - **Python 3** + `mcp` + `python-escpos` + `qrcode` in `~/bin/mcp-pos-print/.venv/`. ### Active Processes None. MCP server is invoked on-demand by Claude Code via stdio; nothing is daemonized. ### Environment Variables - `POS_PRINTER_IP` — override printer IP (default `192.168.0.184`) - `POS_PRINTER_PORT` — override port (default `9100`) - `POS_PRINTER_TIMEOUT` — TCP timeout seconds (default `10`) - `HOMELAB_PASSWORD` — present in shell env but not used by this MCP ## Related Resources - Repo: https://git.sethpc.xyz/Seth/mcp-pos-print - Reference MCP: `~/bin/mcp-gemma4/` (layout source) - Printer manual / conventions: `~/bin/POS_PRINT.md` - Most complete prior ESC/POS example: `~/bin/POS-Automation/pos_briefing.py` (function `build_receipt` at line 365) - DECISIONS.md (this project): `~/bin/mcp-pos-print/DECISIONS.md` --- **Security Reminder**: Before finalizing, run `validate_handoff.py` to check for accidental secret exposure.