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

9.9 KiB

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

  • Scaffold project per ~/bin/CREATE_PROJECT.md (dir, GITEA_API.md symlink, IDEA.md, DECISIONS.md, CLAUDE.md, .claude/handoffs/, .backup/)
  • Write server.py (~280 lines): markdown parser + wrapper + render + send
  • Write pyproject.toml, README.md, test_server.py, .gitignore
  • Create venv, install deps (pip install -e .)
  • Smoke test: 4508 bytes accepted by printer at 192.168.0.184
  • Back up ~/.claude.json before edit (per global CLAUDE.md backup rule)
  • Register pos-print in ~/.claude.json mcpServers
  • 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
  • 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.