AI Agent Control
Let your AI coding agent verify a change by driving a real, running copy of your app on a VibeView cloud device — installing the build, tapping through the UI, reading back what’s on screen, and reporting whether it actually worked. This is the same live-development loop described in Live Development, extended so an agent (not just you) can drive the session from the command line.
Install and authenticate
npm install -g vibeview
vibeview login
vibeview login opens your browser to complete authentication and saves a
token locally. In CI or any non-interactive environment, set
VIBEVIEW_API_TOKEN instead — every command accepts it as an alternative to
being logged in.
Making it available to your agent
Installing the CLI doesn’t by itself tell your coding agent that VibeView exists. One command sets that up:
vibeview agent-setup
It does three things:
- Installs the agent skill into
~/.claude/skills/vibeview-agent/, where Claude Code discovers it automatically in every project. The skill describes the whole workflow — build requirements, the verify loop, cleanup — so the agent knows how to use VibeView, not just that it exists. Pass--projectto install into the current project’s.claude/skills/instead (useful when teammates should get it too, since that directory can be committed). - Offers to register the MCP server with Claude Code
(
claude mcp add vibeview -- vibeview mcp), so the tools are visible in every session. Pass--mcpto register without asking, or--no-mcpto skip. Other MCP clients are configured per client — see Using it with an MCP client below. - Suggests a line for your project’s
CLAUDE.md(orAGENTS.mdif you use other agents) telling the agent to verify UI changes on a live device. This is the strongest per-project nudge: the skill teaches the agent how, the instruction line tells it when.
Do you need both the skill and MCP? Usually not. If your agent can run shell commands (Claude Code in a terminal, for example), the skill alone is enough — it drives the CLI directly, and skipping MCP keeps tool definitions out of your agent’s context. Register the MCP server when the agent can’t shell out: Claude Desktop, Copilot agent mode, restricted environments where named tools are allowed but arbitrary commands aren’t. For those clients MCP isn’t an extra — it’s the only way in.
Agents other than Claude Code can be pointed at the skill file directly — it’s plain instructions any coding agent can follow — or given the MCP server via their own client configuration.
One honest note: even fully set up, an agent asked only to “implement X” won’t always verify on a device unprompted. The setup above raises the odds considerably, but saying “implement X and verify it on the device” is what makes it reliable.
The dev loop
- Upload a debug build of your app (only needed again after a native
change — see Preparing Your Build):
vibeview upload-app ./path/to/app-debug.apk - Start Metro in your React Native project (
yarn startor equivalent). - Start a session in the background:
This prints avibeview dev --detach --jsonsession_readyevent with asession_idyour agent uses to target every command that follows (--session <id>).session_readyfires once the session actually accepts commands — the device is up and the app is installed — so the firstui-treecan follow it immediately. The event also includes the URL of a page where you can watch and interact with the session — detached sessions never open a browser themselves, so open that URL yourself if you want to watch along. - Drive the session with the command verbs below — always start with
ui-treeto see what’s on screen and get element references (@e5, etc.) to act on. If the app is still on its splash or loading screen, the tree can come back empty or near-empty — that’s the app booting, not a broken session. Wait a few seconds and fetch it again. - When you’re done, stop the session:
vibeview dev-stop
Every command also works against a session you already started from the
VibeView dashboard — pass its session ID with --session <id> instead of
starting a new one with dev --detach.
Watching along — and taking over
Every session has a live, fully interactive page (the url in
session_ready, or page_url from the MCP dev_start tool). The bundled
skill instructs agents to share it with you the moment a session starts, so
you can watch the agent work in real time — and drive the device yourself
whenever you want. The agent’s commands and your input coexist; keep it to
a single browser tab per session.
That takeover is also how agents get unstuck. The skill tells them to stop and ask you whenever a screen needs something only a human should provide — signing in with real credentials, a 2FA code, a CAPTCHA, or a risky confirmation — rather than guessing. Do your part on the session page, tell the agent you’re done, and it re-reads the screen (verifying the blocker is actually gone) before continuing.
A detached session records its state in a .vibeview/ directory in your
project root (that’s how later commands find it without --session). Add
.vibeview/ to your project’s .gitignore — it’s local machine state, not
something to commit.
Commands
Every command below accepts --session <id> (targets a specific session;
falls back to the current project’s dev --detach session if omitted) and
--json (prints the raw result as one JSON line instead of human-readable
text).
| Command | Purpose |
|---|---|
ui-tree | Fetch the current screen’s UI elements, each with a reference (@e5) to act on. |
logs | Read recent app logs from the device — JS console output, native errors, and crash messages. |
screenshot | Capture a screenshot of the current screen and save it to disk. |
tap <target> | Tap an element by reference (e.g. @e5) or raw coordinates (e.g. 100,200). |
long-press <ref> | Long-press (touch and hold) an element. |
swipe <direction> | Swipe up, down, left, or right. |
scroll <direction> | Scroll the current view up, down, left, or right. |
scroll-to <text> | Scroll until an element matching that text is visible, stopping at the end of the list. |
drag <from> <to> | Drag between two points, each an element reference or raw coordinates. |
alert <get|accept|dismiss> | Inspect or respond to a visible iOS system alert. |
type <text> | Type text into the currently focused input field. |
clear-text | Clear the currently focused input field. |
press <button> | Press a device button or system gesture (home, back, d-pad, etc). |
open-url <url> | Open a deep link or URL inside the app. |
wait | Wait a moment before the next action. |
find <text> | Find an element by its text, optionally constrained to be near/above/below another element. |
tap-focused <ref> | TV only: move focus to an element and select it in one step. |
focus <ref> | TV only: move focus to an element without selecting it. |
After every action, the response tells you what changed on screen — a full
element list, the specific differences, or a note that nothing changed —
so an agent always knows the current state before deciding what to do next.
That means a normal loop is ui-tree once at the start, then act, act,
act: you don’t need to re-fetch the tree between steps.
Use the tree and the screenshot for different things
The UI tree describes structure: which elements exist, what text they carry, and where they sit. It carries no colour, contrast, or stacking information, so a screen can pass every check the tree can express and still be visibly broken — text rendered in a colour that vanishes into the background, or an element drawn underneath another one.
Use the tree to find elements and confirm structure. Take a screenshot whenever a change affects how something looks, and inspect the image itself.
Over MCP, screenshot writes the file and returns its path; pass
inline: true to receive the image itself. An agent that can’t open files
on the machine running the server needs inline: true to see anything at
all — it costs context, so it’s worth reserving for the checks where
appearance actually matters.
A worked example
Adding a heading to a screen and confirming it renders, with hot reload already running:
vibeview ui-tree # what's on screen now?
# ...edit your component in your editor; Fast Refresh pushes it in ~1s...
vibeview ui-tree # the new heading appears in the tree
vibeview screenshot --out ./check.png # ...and actually looks right
vibeview tap @e7 # keep going: tap through the flow
No rebuild and no re-upload — a JavaScript or asset change reaches the device through Metro. You only rebuild when native dependencies change.
Element references go stale as soon as the screen changes. Using a stale one
is refused and the action does not run; because nothing happened, the
response reports the screen as unchanged rather than returning a fresh
element list, so run ui-tree again to pick up live references before
retrying.
A few commands are worth knowing about specifically:
scroll-tois the right way to reach something off-screen — it keeps scrolling until an element whose text or accessibility label matches is visible, and gives up when the content stops moving, instead of you guessing how manyscrollcalls are needed. It works out which way to scroll on its own for ordinary vertical lists; pass--directionfor horizontal rows of items.dragis the only command for a precise two-point gesture — dragging a slider to a value, reordering a list by dragging an item, or panning a map.swipeandscrollonly take a direction and can’t express those. Each end can be an element reference or coordinates, so you can mix them. Options let you slow the drag down for precision, press and hold before the drag starts (for reorder gestures), and hold at the destination before releasing.alerthandles iOS system alerts such as permission prompts, which sit above your app and block it. Usealert getto read the message and the exact button labels, thenalert acceptoralert dismiss— optionally with--buttonto choose a specific label.logsis how an agent finds out why something broke. It returns the app’s own output — JS console lines, native errors, crash messages — scoped to the app under test, not the whole device. When the app crashes or freezes, the screen alone can’t explain it; the log usually can. Each response ends with acursorvalue: pass it back as--sinceto receive only lines that arrived after your previous read, so acting and then checkinglogs --since <cursor>shows exactly what that action logged.--tailcaps how many lines come back (default 100, newest kept).
For Apple TV and Android TV apps, start the session with
--platform tvos or --platform androidtv and navigate with the d-pad via
press (dpad_up, dpad_down, dpad_left, dpad_right, dpad_center).
Using it with an MCP client
The same commands are also available as MCP tools, so any MCP-compatible agent can call them directly instead of shelling out to the CLI. MCP is an open standard, so this works with any model and any client that speaks it — not just one vendor.
The server runs over stdio: the command is vibeview and the argument is
mcp. Every client expresses that slightly differently.
Claude Code (command line):
claude mcp add vibeview -- vibeview mcp
Cursor — .cursor/mcp.json in your project (or the global one in
~/.cursor/):
{
"mcpServers": {
"vibeview": {
"command": "vibeview",
"args": ["mcp"]
}
}
}
Claude Desktop — claude_desktop_config.json, same mcpServers shape
as above.
VS Code (GitHub Copilot agent mode) — .vscode/mcp.json:
{
"servers": {
"vibeview": {
"command": "vibeview",
"args": ["mcp"]
}
}
}
Anything else — point the client at command vibeview with args
["mcp"]. If the client can’t find the binary, use the absolute path from
which vibeview (a global npm install usually resolves without it).
Authentication comes from the same place as the CLI: run vibeview login
once, or set VIBEVIEW_API_TOKEN in the environment your client launches
the server with. If your client supports per-server environment variables,
add it there:
{
"mcpServers": {
"vibeview": {
"command": "vibeview",
"args": ["mcp"],
"env": { "VIBEVIEW_API_TOKEN": "your-token" }
}
}
}
Tool names are the command names above with hyphens replaced by underscores
(ui_tree, tap, scroll_to, long_press, and so on), and each takes its
arguments by name plus an optional session_id.
Three tools exist only over MCP, covering the steps a CLI user would do with ordinary shell commands, so an agent with no shell access can still run the whole loop:
| Tool | Purpose |
|---|---|
upload_app | Upload a debug build by path and get back the app ID to start a session with — the equivalent of vibeview upload-app. |
dev_start | Start a session that stays open for the lifetime of the connection — the equivalent of vibeview dev --detach. |
dev_stop | Stop the session dev_start started. |
Note that dev_start is stateful: once it succeeds, every later tool call
that doesn’t name a session_id targets that session. Only one can run at a
time, and it keeps billing streaming minutes until dev_stop is called or
the connection ends.
Roku channels are the exception: their reload loop is driven from the
terminal, so dev_start does not accept roku — start those sessions with
vibeview dev --platform roku and point your agent at the running session
instead.
Driving an embed session
A session someone started from an embedded demo can be
driven with the same commands. You target the session that visitor is
already using: pass its id with --session <id> (or session_id over MCP)
and every command lands on the device in front of them. Agent Control never
starts a second session for this, and no additional device is used.
Getting the session id
The embedded page announces it to your page. Listen for the
session:started event — it carries the id — and hand that to your agent:
window.addEventListener('message', (event) => {
if (event.data?.source !== 'vibeview-embed') return;
if (event.data.type === 'session:started') {
const sessionId = event.data.sessionId; // pass this to Agent Control
}
});
If the visitor lands in a queue you’ll get session:queued first — that one
carries only their queue position, not an id, because no device has been
handed out yet. Wait for session:started. See
Page events for the full list.
Keep your credential on your own server. The embed page never receives one, and your page shouldn’t hold one either — it only needs the session id.
Two things have to be true first:
- Agent Control is allowed on that embed key. It’s a per-key setting under Settings → Embeds, off by default, and only an organization admin can turn it on — see Allow Agent Control.
- You’re calling as a Developer or Admin in the organization that owns the key. Viewer credentials are refused, and a token never carries more authority than the member it belongs to.
People viewing your embed in a browser never gain any of this. There’s no command surface on the embedded page — driving the session is something your organization does with its own credentials, from your own tooling — and turning the setting on changes nothing about what a visitor can do.
If either condition isn’t met the command is refused outright and nothing runs on the device. The same applies the moment the key stops being active — turning the setting off, disabling the key, or revoking it all end agent access at the very next command, even mid-session.
What’s available on an embed session
An embed deliberately keeps visitors inside your app, and Agent Control holds that same line. Everything you need to read and drive a screen is there; the two things that would take a visitor out of your app are not.
| Command | On an embed session |
|---|---|
ui-tree, screenshot, logs | Read the screen and the app’s own output, exactly as on any other session. |
tap, long-press, drag, swipe, scroll, scroll-to | Available, by element reference or by coordinates. |
type, clear-text | Available. |
alert | Available — respond to a system alert sitting over your app. |
find, wait | Available. |
tap-focused, focus | Available on TV sessions. |
press <button> | Limited to back and the d-pad (dpad_up, dpad_down, dpad_left, dpad_right, dpad_center). home, lock, and siri are refused: they would drop the visitor out of your app, blank their screen, or hand them to a system assistant. |
open-url | Not available. Opening a deep link or URL is the one command whose whole purpose is leaving the app. |
A refused command says what isn’t allowed and what is, so an agent can correct itself instead of retrying blindly — and the refusal happens before anything reaches the device.
The visitor is using the screen too
This is the one place where you aren’t alone on the device. A visitor can tap between two of your commands, so the screen you read a moment ago may not be the screen you’re acting on, and an element reference you were holding can be stale by the time you use it.
When that happens the command comes back unsuccessful without acting, and the response carries a fresh reading of the current screen — including whatever the visitor just changed. Treat that as ordinary rather than as a failure to retry: re-read the screen and pick the element again from what’s there now. Repeating the same reference will keep failing, because it no longer points at anything.
Sharing the screen is the point of this, not a flaw to work around. The visitor keeps tapping while you work, and they watch your actions happen live on their own device.
The skill
The npm package also ships a ready-made skill at
skills/vibeview-agent/SKILL.md describing this entire workflow (build
requirements, the loop, verifying, cleanup, and the full command reference)
in a form coding agents with filesystem access can read and follow directly.
It covers the same ground as this page.
Billing
A session started this way is an ordinary VibeView session — it bills
streaming minutes the same as any session started from the dashboard.
Remember to run vibeview dev-stop (or stop the session your agent
started) when you’re done so it doesn’t keep running unattended.