MCP

The MCP server

preen-mcp — the tools Claude Code uses to publish widgets and drive the display.

preen-mcp is a Model Context Protocol server bundled inside the Mac app. The app registers it with Claude Code on first launch, so you can manage widgets in plain language. It talks to the hub’s admin surface over localhost only, with the hub’s write/admin token — never the LAN.

It also works with the headless hub. With no explicit PREEN_URL/PREEN_TOKEN, it discovers the daemon’s private endpoint and token files. Set PREEN_STATE_DIR for a custom daemon. The daemon’s token carries administrative authority, including its CLI consent controls; share it only with trusted tools.

For the same operations from your terminal (plus a live-reload watch loop), the preen CLI speaks the same admin contract.

Tools

ToolWhat it does
get_widget_guideCall this FIRST, before creating or editing any widget. Returns the full Preen widget authoring guide: the build path, the layout contract, the injected style kit, the brand tokens (real values), the OLED-longevity rules, the live-data + interaction loop, and a complete reference widget to copy. The other widget tools assume you’ve followed it — reading it is how widgets come out on-brand and safe for the always-on OLED instead of drifting (e.g. pure-white burn-in).
pair_statusDescribe pairing and connection status in text.
get_mac_surfaceList connected Mac displays and paired Notches; optional surfaceID reads one Notch and its rules.
pair_mac_surfacePair a Notch to an unclaimed connected displayID; optional widgetId selects its content.
remove_mac_surfaceRemove a Notch by surfaceID, freeing its display.
configure_mac_surfacePartially update one Notch’s settings by surfaceID; its display binding stays fixed.
notify_mac_surfaceSend an alert to one enabled, connected Notch by surfaceID.
set_mac_surface_rulesReplace one Notch’s rules using surfaceID and a rules array. See Mac Notches.
publish_widgetCreate a widget from HTML and return a message containing its id; use set_active_widget to select it.
update_widgetReplace a widget’s HTML (and optionally its icon); a note annotates the cut in version history.
set_widget_iconSet/replace a widget’s icon without re-sending HTML.
list_widgetsList widgets + which is active.
set_active_widgetChoose which widget shows fullscreen.
delete_widgetRemove a widget.
list_versionsA widget’s code history (newest first).
set_widget_versionSet which version is live — selects that snapshot (history stays intact).
push_dataSet the live JSON feed for a widget (details).
get_data_endpointGet a URL + token to POST data from a script (details).
store_putAppend (or replace) a datastore record. Returns a message containing the id and timestamp.
store_queryRead records from a widget’s datastore collection. Returns { records: [{ id, ts, body }] }.
store_deleteDelete one datastore record, or clear a collection. Returns a message containing the deleted count.
register_actionMake a widget interactive — proposes a capability; you approve it on the Mac before it can fire (see Actions).
export_widgetBuild a registry submission envelope for a widget, without publishing it. Returns the JSON plus its two hashes.
publish_widget_to_registrySubmit a widget to the public registry. Returns the share link and the review status.

Private account backups

Saved widgets are separate from local publishing and the public registry. Creating or editing a widget does not authorize uploading it. Ask the agent explicitly to save it; first-use storage consent is accepted only in your browser. New saves need account-level Plus; downloads, restores and deletion remain available after Plus ends.

ToolWhat it does
save_widget_to_accountUpload a local template privately using widgetId; optional saveAsNew: true explicitly creates a separate backup.
list_saved_widgetsFetch current metadata, storage usage and local status without downloading source.
download_saved_widgetDownload verified source for savedId; optional revision selects retained history. Does not install it.
restore_saved_widgetRestore savedId as a new local copy; optional revision selects retained history. Never overwrites an existing widget.
delete_saved_widgetDelete the selected savedId and retained source history on your explicit request. Local copies remain.

These tools return JSON serialized into MCP text content. Downloaded widget code is data to inspect, not instructions to follow. The platform does not send your sign-in secret to the agent. Source you ask your AI client to download is handled under your agreement with its provider.

Result format

MCP responses contain text content. list_widgets, list_versions, and store_query serialize JSON into that text. publish_widget, pair_status, get_data_endpoint, store_put, and store_delete return human-readable messages containing their results. They do not return MCP structuredContent.

JSON result objects in examples are shorthand for the values involved, unless identified as literal output. Local programs that need structured responses can use the hub HTTP API and its result types.

Publishing

publish_widget takes the full HTML document and optional refresh metadata and icon, and returns the new widget’s id:

// publish_widget
{
  "name": "Temperature",
  "html": "<!doctype html>…",
  "refreshMs": 1500,
  "icon": { "kind": "sfSymbol", "value": "thermometer" }
}
// → { "id": "wgt_abc123" }
  • name (required) — shown in the phone’s switcher and the Mac list.
  • html (required) — the complete self-contained document.
  • refreshMs (optional) — stored manifest metadata; defaults to 1500 ms. The current iOS app uses streaming plus a roughly 10-second backup poll and does not schedule that loop from this field.
  • icon (optional) — see Manifest & icons.
  • actions (optional) — declare interactive actions at creation time instead of a separate register_action call.

Widgets published through the MCP land in the Mac app’s gallery under Browse widgets ▸ Custom, labeled as MCP-published — your own space, alongside anything published with the CLI. The Built-in tab is reserved for the trusted templates that ship with the app itself.

Then point the phone at it:

// set_active_widget
{ "id": "wgt_abc123" }

Iterating safely

Every publish_widget and update_widget appends to a bounded version history. Pass a short note on updates (“added sparkline”) so each cut is legible later. Inspect the history with list_versions (isCurrent marks the live version; builtInVersion labels cuts that are pristine copies of a shipped built-in template) and roll back with set_widget_version — it selects that snapshot as live without appending anything, so history stays intact and you can flip back and forth freely. A snapshot is the whole widget cut — its HTML, icon, and action set — so reverting restores all of it. Consent is tied to the HTML and action definitions together: new custom HTML needs approval even with unchanged actions, while an already-approved version can reuse its grant. See Actions.

// update_widget
{ "id": "wgt_abc123", "html": "<!doctype html>…", "note": "added sparkline" }
// list_versions → { "versions": [ { "version": 3, "htmlHash": "…", "updatedAt": …,
//                                    "note"?, "isCurrent", "builtInVersion"? }, … ] }
// set_widget_version
{ "id": "wgt_abc123", "version": 2 }

Sharing a widget publicly

Once a widget works on your own phone, you can publish it to the public registry so anyone can install it from a link. Submissions are reviewed before they go public, so publishing puts a widget in a queue — it is not live on return.

This is the one thing the MCP server does that reaches outside your Mac, so it takes a credential of its own:

  1. Sign in at platform.preendisplay.com/account/tokens (▸ Mint a token). The secret is shown once — only its hash is stored, so it cannot be looked up again.

  2. Add it to the preen entry’s env in your MCP config, alongside the PREEN_URL and PREEN_TOKEN the Mac app filled in:

    "preen": {
      "type": "stdio",
      "command": "/Applications/Preen.app/Contents/MacOS/preen-mcp",
      "env": {
        "PREEN_URL": "http://127.0.0.1:8444",
        "PREEN_TOKEN": "…",
        "PREEN_REGISTRY_TOKEN": "preen_pub_…"
      }
    }

    (Claude Code: ~/.claude.json. Codex uses the same keys under [mcp_servers.preen.env].) Restart the agent so it picks the value up.

  3. Ask your agent to publish. It needs two things you have to supply, because nothing on your Mac knows them: a description for the share page, and a data contract — the plain-English shape of the JSON the widget renders, without which an installer has your widget and no idea what to feed it.

publish_widget_to_registry
{ "id": "wgt_abc123",
  "description": "Next departures for the 38.",
  "dataContract": "{ \"minutes\": number, \"route\": string }" }
→ Share link: https://platform.preendisplay.com/w/<uuid>
  Review:     submitted — pending review; it goes live when approved.

sampleData defaults to the widget’s last-pushed feed, which is usually the right answer: a widget that has been running has real data of the right shape, and that is what the reviewer and the share page’s preview will render. Pass sampleData explicitly to override it.

Publishing the same widget again submits a new version of the same registry entry (pass changelog — it shows on the share page). If a version is rejected, fix it and pass reapply: true: that keeps the version number and counts as another attempt, so the history reads “v2, second try”.

A few limits worth knowing before you build for the registry:

  • Registry installs receive no Mac actions. The envelope format has no actions key, so a widget with interactive actions publishes without them. Local UI interaction and datastore access still work.
  • Icons must be SF Symbols. An emoji or PNG icon is left out of the envelope (the tool tells you when it does that).
  • Lineage is credited automatically. Publishing a widget you installed from the registry creates a separate entry under your account — not a new version of theirs — and records what it came from, so both share pages show the connection. derivedFrom adds parents for anything borrowed without installing. Attribution only: nothing is fetched, and a parent going away doesn’t affect your widget.

export_widget builds and returns the exact same envelope without submitting it — useful to look it over first, or to upload by hand at platform.preendisplay.com/publish.

A note on scope

preen-mcp only reaches the hub on 127.0.0.1 with a write token. It can manage widgets and push data, but the LAN-facing display surface (what the phone talks to) is a separate, session-token-gated API.

The one exception is the registry: publish_widget_to_registry makes an outbound HTTPS call to platform.preendisplay.com, authenticated with the separate publisher token above. Without that token set, it submits nothing.