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)
421c3c9feat: 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:
- Inline parser (
parse_inline) — regex-splits a line on**bold**/`code`/[text](url)and returns[(style, text), ...]tuples. - 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. - 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 inpyproject.toml, singleserver.pywithServer(...)+stdio_server(), registered in~/.claude.jsonwith 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.jsonbefore edit (per global CLAUDE.md backup rule) - Register
pos-printin~/.claude.jsonmcpServers 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
- Seth visually verifies the smoke-test page. If garbled (extra line breaks, wrong widths, missing bold, mis-aligned wrap), debug
wrap_segmentsfirst — most likely culprit. - In the next Claude Code session, run
/mcp(or restart) to load the newpos-printserver. Then test from within a session:pos-print:print(markdown="# hi\n\n**works**."). - 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 checkqrcodepackage 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.jsonis in this user's profile on this machine — no broadcast to other hosts. - Backup convention was followed:
~/.claude.jsonbacked up to/home/claude/.claude/.backup/claude-json-1779742244.jsonbefore 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
claudeuser 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.pyfor ages. - Timezone for the timestamp header is Eastern (
America/New_York) — matchespos_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
codeinline style currently renders identically toplain— 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 afterp.qr(...)andp.cut()— they appear to reset some state. Already handled inbuild_receiptbut 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/giteaCLI wrapper. - Python 3 +
mcp+python-escpos+qrcodein~/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 (default192.168.0.184)POS_PRINTER_PORT— override port (default9100)POS_PRINTER_TIMEOUT— TCP timeout seconds (default10)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(functionbuild_receiptat 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.