Build
Ships today

Agents over MCP

The tool surface, the scopes, and the consent gate.

This is the whole surface an outside agent gets — no SDK, no build step, nothing flashed to the device. An agent that speaks MCP connects to the companion's local server and calls these tools by name. Manage a device, draw a screen on it and get the answer back, or put a real app on it: it is all here, and it is all live today. The model behind the surface is MCP.

The scope on every tool

Every tool names exactly one scope, and a call is refused unless the token you handed the agent carries that scope. Five scopes, coarse on purpose — you grant a whole verb, not a per-tool checklist:

  • control — see and manage devices, change settings, move around the screens, sleep and wake, draw a live screen, and ask a question that waits for a press. The widest set.
  • apps — put apps on a device and run their lifecycle: install, remove, enable, and grant or revoke what they may touch.
  • library — read the extension library: what is available and what each one is.
  • library_admin — change it: install, configure, connect, and test extensions.
  • urgent — carries no tool of its own. It unlocks the full-screen alert form of notify; without it, that alert downgrades to a quiet card. A scope with zero tools is a real state, not a rounding error.

Every tool, and its scope

Tools
41
Scopes
5
control21apps11urgent0library2library_admin7
ToolRequired scopeWhat it does
app_installappsInstall an app onto a device. `source` is exactly one of: {catalog_id, version?} (a published catalog app), {file_path} (a local .zeph-app), or {library_id} (a compiled-in app). `dest` = 'flash' | 'sd' (catalog/file only). Hosted apps run in the companion runtime.
app_set_enabledappsEnable or disable an installed app on a device.
app_uninstallappsUninstall an app from a device (reverses a hosted install).
consent_grantappsGrant declared-tool scopes to an integration/app (additive, revocable).
consent_revokeappsRevoke a granted scope (omit 'scope' to revoke all for the id).
register_appappsDEPRECATED alias for register_surface (kept one release) — same behavior. Prefer register_surface; this name will be removed. (A registered app never ran code — it is a host-tethered surface; see the zeph-device skill.)
register_surfaceappsAuthor a durable SURFACE — a host-tethered set of device screens the user can place and bind. NOT an app: no code runs on the device or companion, and it freezes to its last snapshot when you disconnect (for a zero-setup single panel, use `present` instead). The manifest declares identity (id, name, icon, byline, description) and exports: faces (designed zui screens), widgets (slot content), pages (swipe-up panes; ir=null means you push content via update_surface), intents (button actions the user can bind — you receive them via subscribe_device_input), glances (ready-made device screens the user can add in the companion's glance editor) and skills (agent skill packages — id, semver, SKILL.md markdown, optional prefab zui screens — shown in the companion's Skills tab with your surface's name and installable into agent clients; removed with the surface). The surface joins the companion library under "From your assistant". See the zeph-device skill for the manifest recipe.
remove_appappsDEPRECATED alias for remove_surface (kept one release) — same behavior. Prefer remove_surface; this name will be removed.
remove_surfaceappsRemove your registered surface: its glances, widget slots, pages and bindings are evicted from connected devices (same rules as uninstalling any app) and the surface leaves the companion library.
update_snapshotsappsDEPRECATED alias for update_surface (kept one release) — same behavior. Prefer update_surface; this name will be removed.
update_surfaceappsPush live content for your registered surface's snapshot-backed exports: a map of snapshot ref ('app_id.export_id', must be your surface's prefix) to zui JSON tree. Content lands on connected devices immediately and re-pushes on reconnect; if your client disconnects, the last snapshots simply go stale on-device.
app_inspectcontrolInspect a local .zeph-app file: identity, requested caps, and the signature/provenance verdict. Read-only.
app_listcontrolThe companion's app-library view for a device (id, enabled, version).
catalog_refreshcontrolFetch + verify + stage the published apps catalog so catalog installs (app_install with a catalog_id) resolve.
consent_pendingcontrolThe declared scopes an integration requests that are not yet granted.
device_navigatecontrolNavigate the device with NO physical input (#554) — drive the home glance strip OR the open app's Screen stack, so you can move around the device UI yourself (e.g. open an app, push/pop its screens, switch glances) instead of asking a human to tap. Provide EXACTLY ONE of 'glance' or 'app_verb'. glance: 'next' | 'prev' | '#<index>' (0 = the Apps head) | a glance id — switches the shown home glance. app_verb: 'open' (enter/goto a Screen by 'app_ref', e.g. 'apple-music.now-playing'), 'push'/'replace' (also need 'app_ref'), 'pop'/'close' (go back; 'close' from the app's first screen exits the app). The device reports the resulting focused surface on its status channel — read get_device_state to confirm where it landed.
device_power_cyclecontrolHard power-CYCLE one device at the PMIC (#1317) — it turns off and comes back on its own (a SoC-independent restart, more thorough than a reboot). Non-destructive. A device whose firmware predates this returns an unsupported error.
device_power_offcontrolPower one device fully OFF at the PMIC (#1317). The device turns off and STAYS off until someone presses its physical power button — it does NOT come back on its own. Non-destructive (keeps all apps, data, settings and pairing). A device whose firmware predates this returns an unsupported error.
device_rebootcontrolReboot one device (#844). Default: a normal application reboot — the device goes down and returns on its own, re-attaching through discovery. Set 'dfu' to true to enter ESP32-S3 ROM USB download mode (vendor ENTER_DL) for reflash instead; in DFU the device stays in ROM until it is reflashed (it will NOT return to the app on its own). Non-destructive — keeps all apps, data, settings and pairing.
device_sleepcontrolPut one device's screen to sleep now (#952). Idempotent; refused (busy) while a firmware flash owns the screen. The device wakes on button, tap (if enabled), urgent notification, or device_wake.
device_wakecontrolWake one device's screen now (#952). Idempotent; consumes no input — pair with device_navigate to open a surface with the screen lit.
get_device_statecontrolFull state of one device by serial: the device summary, its device-synced settings (brightness, device_name, mic_gain), whether it is connected right now, and 'focused_surface' — the surface the device is CURRENTLY showing (the active glance id, or an app-surface ref), reported on the device status channel; null when the device is offline. This is the field device_navigate's landing-check reads.
get_screenshotcontrolCapture what a device is showing right now (#1372 render-back) and return it as a PNG image. Use it to SEE the result of your push_screen / present / notify — the device renders your vector IR to its own panel, and this is how you read that panel back (a ground-truth pixel grab, not a re-render of your tree). The frame is the device's native resolution + color depth. Also available as the resource 'zeph://device/{serial}/screen'. Offline devices error; capture is unsupported on very old firmware.
list_devicescontrolList every Zeph device this companion knows — connected and offline — with serial, name, firmware, connection state, transports and battery.
notifycontrolPost a notification on the device's six-rung ladder (no app/glance setup needed). Levels: badge (bell count), marquee (one live status line), notice (toast card: icon+title+text), urgent (fullscreen takeover; needs the 'urgent' consent scope, else it downgrades to notice), prompt (question + up to 3 options — the call WAITS and returns the chosen option), modal (multi-page zui body document, 'body' arg — the call WAITS for the terminal reply: {action, values, steps}). The device may downgrade: modal->prompt->notice->badge (e.g. invalid body, DND); the result reports the effective level. Replies/dismissals also stream on the intents notification channel keyed by the returned id. Re-posting with a live 'id' updates that notification in place (progress/monitor case — same id, no re-surface flash).
presentcontrolShow content on your assistant's OWN ambient surface — a zero-setup, companion-provisioned panel your client owns (NO register_app, no glance wiring). ONE call places + fills + (by default) focuses it; unlike push_screen you don't manage a scratch ref or place a glance yourself — present owns the placement you couldn't do. 'tree' is a zui JSON IR root node (the same vocabulary as push_screen). 'ttl_seconds' auto-clears the CONTENT to a subtle idle face afterwards — the surface glance stays (no surprise disappearance of the container). 'focus' (default true) brings the surface to the foreground and wakes the screen. Interactive by construction: buttons AND value controls (sliders/toggles/selects) on the surface stream back to you via subscribe_device_input. Returns { surface_ref, shown (did it land in the foreground), glance_idx }. Use present for transient content you want visible now; register_app for a standing app the user keeps and arranges themselves.
push_assetcontrolPush a RASTER image (photo, generated art, a chart you rendered — anything the declarative zui IR can't draw) to the device's asset store, then reference it from a screen. Prefer push_screen's vector IR for UI (crisp at any size, tiny, theme-aware); reach for push_asset only when you genuinely need pixels. Provide EXACTLY ONE image source: 'image' (base64 of the encoded bytes — PNG / JPEG / WebP / GIF, a 'data:...;base64,' prefix is accepted) OR 'url' (an https:// image the companion fetches under an SSRF guard: https-only, public hosts only, no redirects, image content-type, 8 MB cap). 'ref' is 'app_id.asset_id' and, exactly like push_screen, must NOT belong to a live app (an owned ref is re-fed by that app and would overwrite your asset — such refs are REFUSED; use a scratch ref like 'scratch.bg'). 'codec' picks the on-device encoding: 'qoi' (default — lossless, compact), 'jpeg' (lossy, smallest on the wire), or 'raw' (RGB565 passthrough). The companion adapts quality to the live transport and stores the decoded image under 'ref'; render it by pushing a zui tree that references it — the asset ref rides the image node's `id` (NOT a `ref` field): {"k":"image","id":"scratch.bg"} inline, or as a full-bleed background. The asset persists until overwritten or the device reboots. Oversized images are rejected — target the device panel resolution.
push_screencontrolPush an ephemeral screen to the device: a declarative zui JSON tree (the pushed-UI IR — nodes like {"k":"screen","c":[{"k":"big","t":"Hi"}]}) rendered through the device's snapshot path. 'ref' is 'app_id.export_id' and must NOT belong to a live app — an owned export is re-fed by that app's snapshot pump, which would silently overwrite your push, so such refs are REFUSED. Use a scratch ref no app owns (e.g. 'scratch.main', placed on a glance), or register your own app (register_app) and feed it via update_snapshots. The content shows wherever that export is on a glance (its face/page/widget slot). The push REPLACES that ref's previous content and stays until the next push or device reboot. Dismiss semantics: pass 'ttl_seconds' and the companion auto-clears the surface (pushes a blank tree over the same ref) when it elapses; or push new/blank content yourself at any time — there is no navigation side effect either way.
retractcontrolClear your assistant's ambient surface (the one `present` fills) back to its idle face immediately — the surface glance stays, the content goes. Use it when you're done showing something before its ttl_seconds elapses.
subscribe_device_inputcontrolSubscribe to this device's interactive events — everything the user does on YOUR surfaces and apps. Each arrives as an MCP 'notifications/message' with logger 'zeph.input' and a TAGGED payload: {serial, type:'intent', id, phase} — a button intent, id 'app_id.intent_id', phase press/release/tap/double (press+release = hold); {serial, type:'value', node_id, value} — an id-bearing value control (slider/toggle/select) on your surface moved; {serial, type:'swbutton', surface_ref, element_id, hold} — a composed software-button tap/hold. So a surface you `present` is interactive by construction — buttons AND value controls stream back. Events are scoped to you: you only see interactions on surfaces/apps you own, never another agent's or a system glance. One subscription per session per device; survives reconnects; ends when your MCP session closes.
subscribe_intentscontrolDEPRECATED alias for subscribe_device_input (kept one release) — same behavior: subscribes to your device interactive events (intents + value controls + software buttons) on logger 'zeph.input', scoped to surfaces/apps you own. Prefer subscribe_device_input; this name will be removed.
update_settingscontrolWrite device-synced settings on one device (the same validated path the companion Settings page uses). Returns a per-setting outcome: synced, offline, or a validation error.
ext_detaillibraryFull record for one extension by id (kind, status, capabilities, config).
ext_listlibraryList the extension library — providers, connectors, tools, apps and skills — with id, kind, status and enabled state. Optional 'kind' filters (provider|connector|tool|app|skill).
ext_configurelibrary_adminSet non-secret config for an extension (validated; secret keys are REFUSED — those go through ext_connect into the keychain). Privileged.
ext_connectlibrary_adminConnect a provider with an API key: verified live, then stored in the keychain (never in config, never on-device). Privileged.
ext_disablelibrary_adminDisable an extension (kept configured, hidden from pickers). Privileged.
ext_enablelibrary_adminEnable an extension (#509). Privileged.
ext_installlibrary_adminInstall a user-declared extension from a manifest.json object. Validated and refused on a builtin-id collision. Privileged (human-approval gated).
ext_removelibrary_adminRemove a user install (uninstall) or clear a builtin's credentials+config (disconnect). Privileged.
ext_testlibrary_adminRe-verify an extension's stored credentials. Privileged.

A token carries scopes; a tool call is refused unless the token carries the scope in its row. The pairing is read from the server’s own dispatch table.

A token you created is not yet a token you trust.

The first time a new token calls in, the companion asks you to approve it — and until you do, every call is refused, whatever scopes the token carries. That one-time approval is the gate the whole surface sits behind. Minting the token and approving the client is walked through in Connect Claude Code.

Next

On this page