Skip to content

feat(mcp): first-class lite mode — npx beeperbox against a local Beeper Desktop#17

Merged
hamr0 merged 5 commits into
masterfrom
feat/lite-mode
Jun 16, 2026
Merged

feat(mcp): first-class lite mode — npx beeperbox against a local Beeper Desktop#17
hamr0 merged 5 commits into
masterfrom
feat/lite-mode

Conversation

@hamr0

@hamr0 hamr0 commented Jun 16, 2026

Copy link
Copy Markdown
Owner

What

Makes lite mode a first-class, supported path: run beeperbox's MCP verb server standalone against a Beeper Desktop the user already runs locally — no Docker, no Electron, no Xvfb. Same single mcp/server.js the container runs, so the tool surface and serverInfo.version are identical by construction. Driven by multis (laptop users with Beeper already open).

Changes

  • npm package beeperbox (mcp/package.json, zero-dep) with a beeperbox bin → npx beeperbox (HTTP) / npx beeperbox --stdio. Already published (beeperbox@0.7.0, name reserved). Tarball is server.js + npm README only.
  • Version single-sourced from package.json; a unit test pins version == package.json and the exact 12-verb set so lite/container can't drift.
  • Startup preflight — bounded /v1/accounts probe logs a clear OK/FAIL verdict on boot (replaces the Docker HEALTHCHECK lite mode lacks). Non-fatal; BEEPERBOX_PREFLIGHT=0 opts out.
  • 🐛 Fix — sent-ledger persistence. Default was the container-only /root/.config/beeperbox-sent-ledger.json (fails on a non-root host, silently degrading the echo-guard). Now per-user XDG (~/.config/beeperbox/sent-ledger.json), parent dir auto-created. One code path (os.homedir() is /root in-container).
  • 🔒 Security fix (found by /security on this branch). The MCP server bound 0.0.0.0 unconditionally — safe in the container (loopback publish is the boundary) but in lite mode it exposed the full tool surface to the LAN, reachable unauthenticated via a spoofed Host header (demonstrated live against the host LAN IP). Now binds 127.0.0.1 by default (MCP_BIND_ADDR); the Dockerfile bakes MCP_BIND_ADDR=0.0.0.0 so the container's published port + the CI guard-check still work. Regression test pins the loopback default. Docs corrected (the "same posture as the container" claim was false).
  • CI.github/workflows/publish.yml (hamr0 OIDC trusted-publishing template, adapted for the mcp/ subdir, zero-dep). Manual workflow_dispatch, idempotent.
  • Docs — README/mcp-README "Lite mode" + security; PRD (reverses the npm + local-Beeper non-goals §3/§8, bind posture §5.3/§7); GUIDE; CHANGELOG [Unreleased].

Validation (live, against a real Beeper account on the host)

  • Send → restart → poll echo-guard round-trip reads back source:"api" + client_tag from the reloaded per-user ledger — over HTTP. The installed npx bin and stdio transports were each separately exercised against the live account (real list_accounts → 4 accounts + preflight OK).
  • Security fix proven both ways: default lite mode binds 127.0.0.1 (LAN attack → connection refused; loopback client → 200); MCP_BIND_ADDR=0.0.0.0 still binds all interfaces.
  • Preflight all branches: OK, FAIL 401-bad-token, FAIL unreachable, no-token skip, opt-out.
  • Ledger fix regression tests watched failing on the pre-fix code, passing after (44/44 unit tests green).
  • Rebuilt the Docker image (with the bind ENV): in-container homedir=/root, ledger /root/.config/beeperbox/sent-ledger.json, binds 0.0.0.0, published port serves 12 tools.

Not in this PR / follow-ups

  • npm name is reserved at 0.7.0; the OIDC publish.yml publishes the next version (a release bump) — it's idempotent and skips an already-published version. Requires the npm trusted-publisher configured for the package first.
  • Version stays 0.7.0; a release PR stamps the next minor per the established flow.

🤖 Generated with Claude Code

hamr0 and others added 5 commits June 16, 2026 12:28
…er Desktop

Run the MCP verb server standalone (no Docker/Electron/Xvfb) against a Beeper
Desktop the user already runs. Same single mcp/server.js the container runs, so
the tool surface and serverInfo.version are identical by construction.

- npm package `beeperbox` (zero-dep) with a `beeperbox` bin -> `npx beeperbox` / `--stdio`
- version single-sourced from package.json (the container COPYs it into the image);
  a unit test pins serverInfo.version == package.json and the exact 12-verb set
- startup preflight: one bounded /v1/accounts probe logs a clear reachable+token
  verdict on boot (replaces the Docker HEALTHCHECK lite mode lacks); non-fatal,
  BEEPERBOX_PREFLIGHT=0 opts out
- fix: the echo-guard sent-ledger defaulted to a container-only /root path that
  silently failed to persist on a normal host -> per-user XDG default
  ($XDG_CONFIG_HOME/beeperbox or ~/.config/beeperbox/sent-ledger.json), parent dir
  auto-created; one code path for both modes (homedir is /root in the container)
- de-containerize the BEEPER_TOKEN-missing error copy
- docs: README "Lite mode"; PRD (reverses the npm + local-Beeper non-goals, §3/§8);
  GUIDE section; CHANGELOG [Unreleased]

Validated live against a real Beeper account: the send -> restart -> poll echo-guard
round-trip reads back source:"api" across HTTP, the installed npx bin, and stdio;
all preflight branches (OK/401/unreachable/skip/opt-out); the rebuilt image keeps
the full MCP guard matrix and reads its version from the copied package.json.
Regression tests for the ledger fix were watched failing on the pre-fix code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…or mcp/)

Future lite-mode publishes run via .github/workflows/publish.yml — npm trusted
publishing over OIDC (no NPM_TOKEN), manual workflow_dispatch, idempotent
(skips an already-published version), asserts the registry end-state instead of
trusting npm's exit code. Adapted from the canonical hamr0 template for this
repo: the package root is the mcp/ subdir (defaults.run.working-directory), and
the template's `npm ci` step is dropped since the package is zero-dependency
with no lockfile. PRD §8 updated to describe OIDC publishing (no token) and the
one-time manual name reservation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
/security on the lite-mode change found a HIGH issue: the MCP server bound
0.0.0.0 unconditionally. Safe in the container (Docker publishes the port on
127.0.0.1 — that loopback PUBLISH is the boundary), but lite mode (`npx
beeperbox`) has no such layer, so it listened on every interface. A same-network
attacker could reach the full tool surface (read every message, send across
every network) unauthenticated by spoofing the Host header past the allowlist —
demonstrated live against the host LAN IP.

- new MCP_BIND_ADDR env, default 127.0.0.1 (loopback) — safe default for lite mode
- Dockerfile bakes ENV MCP_BIND_ADDR=0.0.0.0 so the container's published port
  keeps working AND the CI guard-check (runs `node` directly) still binds all ifaces
- regression test pins the loopback default so it can't silently revert
- docs corrected: the "same posture as the container" claim was false — lite
  binds loopback, the container relies on the loopback publish (README, mcp/README,
  GUIDE, PRD §5.3/§7); CHANGELOG Security entry

Verified live: default lite mode binds 127.0.0.1 (LAN attack now connection-refused,
loopback client 200); MCP_BIND_ADDR=0.0.0.0 still binds all interfaces; rebuilt
image binds 0.0.0.0 via ENV with the published port serving 12 tools; in-container
homedir=/root and ledger=/root/.config/beeperbox/sent-ledger.json grounded. Also
corrected an over-claim: the send->restart->poll round-trip was validated over HTTP;
the npx bin and stdio were each exercised with real list_accounts + preflight.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…FLIGHT=0)

Branch self-review caught it: the entrypoint launches the MCP server BEFORE its
API-readiness loop, and preflight is one-shot, so in the container it ran while
Beeper's API was still coming up (or pre-login) and logged a misleading
"preflight FAIL: unreachable" on essentially every boot. Preflight is the
LITE-mode boot check (lite mode has no healthcheck); the container already has a
Docker HEALTHCHECK + supervisor + the entrypoint's own API-wait, so it's
redundant there. Bake ENV BEEPERBOX_PREFLIGHT=0 into the image; lite mode (npx,
without this ENV) still runs it.

Verified: image ENV BEEPERBOX_PREFLIGHT=0; container with a token + dead API logs
NO preflight line and still binds 0.0.0.0; host `node mcp/server.js` (no ENV)
still logs "preflight OK ... 4 account(s)".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cuts 0.8.0 (MINOR): first-class lite mode (`npx beeperbox` against a local Beeper
Desktop), startup preflight, the sent-ledger per-user XDG fix, and the lite-mode
loopback-bind security fix (MCP_BIND_ADDR). serverInfo.version reads from
package.json, so bumping the manifest to 0.8.0 is the single stamp the npm package
+ the container + the MCP initialize response all follow.

- mcp/package.json 0.7.0 -> 0.8.0
- CHANGELOG [Unreleased] -> [0.8.0] — 2026-06-16 [MINOR]
- PRD status + release-history row + lite-mode roadmap item marked shipped
- beeperbox.context.md version header + compatibility table
- publish.yml: setup-node v6 -> v4 (match mcp-test.yml; avoid an unresolved-tag
  failure on first dispatch)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@hamr0 hamr0 merged commit 6f45177 into master Jun 16, 2026
2 checks passed
@hamr0 hamr0 deleted the feat/lite-mode branch June 16, 2026 11:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant