№ 0817Skill
Display connector for Muse with glasses card loop
A Vercel-hosted MCP connector for Muse that pushes cards to a 600x600 display web app, built around a serverless socket-lifetime spike with session state kept in memory or Redis.
# Display connector — spike and card loop
Two things in one project: the socket-lifetime spike, and the real card
loop it was blocking.
## Layout
```
api/ Vercel route handlers, thin wrappers over lib/
lib/store.mjs session state, memory or Redis, never a module variable
lib/api.mjs MCP tools + the glasses endpoints, as pure functions
server.mjs local Node fallback (same handlers, optional ws)
public/ the 600x600 Web App, served at the site root
mockup/ the six-screen design comp, not deployed
```
Nothing stateful lives in `server.mjs`. That is the whole point: on
serverless, `show_card` and the wearer's poll land on different instances,
so the card must be a record, not an object in memory.
## Run it
```
npm install
npm start
```
With no Redis configured it uses the in-memory store, which is fine on one
process. Set `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` (Vercel
KV exposes the same names) and it switches automatically — the startup log
says which one is live.
## Pair the page
Identity comes from the per-user credential, since nothing documents Muse
passing a stable user id. Ask the server which display id a credential maps
to, then open the page at that id:
```
curl -H "Authorization: Bearer my-test-key" http://localhost:8080/api/whoami
```
Open `http://localhost:8080/?s=<the session it returns>`. The page
remembers it. QR pairing replaces this step later and mints the same URL
shape.
## Drive a card
```
curl -X POST http://localhost:8080/api/mcp \
-H "Authorization: Bearer my-test-key" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"ask_approval","arguments":{"action":"Send the revised quote to Dana?","detail":"£4,200"}}}'
```
The card appears within a couple of seconds. Arrow keys move the focus
ring, Enter answers, and the answer comes back through `wait_for_answer`
with the same ticket.
## Why short-poll
The page polls every 2s, backing off to 6s after a quiet minute and 12s
after three. Long-polling would fight the function duration cap for no
benefit — a second or two of latency is nothing against an agent whose
cadence is minutes, and short-poll behaves identically on a long-lived
process and on serverless.
`wait_for_answer` waits up to 25s, then returns `status: "pending"` with
the ticket. It also reports wh


ChatForm
Tgmlabs