Perch Display turns a spare iPhone on a stand into a programmable, glanceable display for your Mac. You author a widget once — a single self-contained HTML document — and your Mac feeds it live data. The phone renders it fullscreen and updates the instant the data changes.
The pieces
- The Mac hub (the Perch app) runs a small local HTTPS/WebSocket server on your network. It stores your widgets, serves their HTML, and pushes their data.
- The iPhone display (the Perch iOS app) pairs with the hub over your LAN and shows the active widget fullscreen.
perch-mcpis an MCP server bundled with the Mac app. It lets Claude Code (or any MCP client) publish widgets and push data in plain language.
The one rule: no code runs on the phone
This is the decision (D7 in the spec) that shapes everything: a widget is a pure function of injected data. It does no network, holds no token, and never re-runs logic on the phone.
The native iPhone app holds the authenticated session and does all network.
It fetches the widget HTML, polls the data endpoint, and injects the result into
the running page one-way — HTML via loadHTMLString, data as a structured
JavaScript argument. A strict Content-Security-Policy (connect-src 'none') is
injected before your code loads, so fetch, XMLHttpRequest, and WebSockets are
simply unavailable inside a widget.
<!-- The entire data contract, from the widget's side: -->
<script>
window.PERCH.onData((d) => render(d)); // d is your JSON, redelivered on every update
window.PERCH.ready(); // optional: signal "UI is hydrated"
</script>
Why it matters:
- The session token never enters agent-authored widget JS — it stays in native code.
- Cross-widget data isolation is automatic.
- Polling/streaming is written once in
PerchKit; widgets never think about it.
How data flows
- You (or Claude Code) publish a widget’s HTML to the hub.
- The hub serves that HTML at
GET /widget/:idand holds a JSON feed for it. - The phone fetches the HTML, then polls
GET /api/data/:ideveryrefreshMs(default 1500 ms) over the trusted native channel. - On each poll the native app calls
window.PERCH.__deliver(payload), which invokes youronDatacallback with the fresh JSON.
You write step 1 and the onData renderer. Perch Display does the rest.
Next: build one end-to-end in the Quickstart.