Files
mcp-pos-print/.claude/handoffs/2026-05-25-193709-initial-mcp-pos-print-build.md
2026-05-25 19:40:21 -04:00

154 lines
9.9 KiB
Markdown

# 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.