Agents & MCP
You don’t need any of this to use compose-ai-tools — rendering @Previews to
PNG already lets an agent see its work. This page is for when you want a
tighter, push-based loop than “run a command, read a file.”
The agent loop
A token-frugal feedback loop over Compose UI — the way
Playwright gave web agents one over the DOM: act by
a stable reference, observe structure rather than pixels, and turn
exploration into durable tests. These are exposed by the preview daemon’s MCP
server (and the compose-preview CLI where noted).
- Target by semantic ref, not pixels.
interactive/inputandrecord_previewaccept atarget(testTag/role+text/ a stable noderef) that the daemon resolves to the node’s centre, so a click survives layout changes instead of breaking on a coordinate. Android (Robolectric) and Desktop (Skiko). - Token-frugal observation.
render_preview observe=semantics|hashreturns thecompose/semanticstree + a hash + dimensions instead of a base64 PNG — typically a few hundred tokens versus ~1.5k. Fetch pixels only when you actually need to look. - Semantics diff.
diff_semantics(MCP) andcompose-preview diff-semantics(CLI) diff two semantics trees and report what changed semantically (text, label, role, testTag, overflow…), matched by stable ref — a deterministic, pixel-free regression signal, the Compose analogue of Playwright’s aria-snapshot diff. - Matrix render.
render_matrix(andcompose-preview render-matrix) renders one preview across a cross-product ofdevice × locale × uiMode × fontScalein a single call, returning a per-cell hash and which cells changed — “does this survive small screen + RTL + large font?” without N screenshots. Opt into a stitched contact-sheet image when you want to eyeball every cell at once. - Recording → test.
record_preview emitTest=trueturns a scripted interaction into a runnable Compose UI test (semantic targets becomeonNodeWithTag(...).performClick()steps; eachrecording.probeis diffed against the previous probe’s captured semantics intoassertExists()/assertDoesNotExist()assertions). - Structured failures. A failed render reports a typed
kindplus a one-line fix hint for recognized signatures (classpath skew, Robolectric SDK mismatch, missing@Composable, …) instead of an opaque message.
Cost budget for these in
docs/TOKEN_USAGE.md.
The MCP server
The :mcp module exposes the preview daemon’s JSON-RPC over stdio, so
MCP-aware agents can render previews on demand and be notified when bytes
change — instead of running Gradle (10s+ cold) or polling PNGs off disk with
no way to request a re-render.
Each @Preview becomes one MCP Resource under a stable
compose-preview://<workspaceId>/<module>/<previewFqn> URI. The server
supports subscribe (per-resource update notifications) and listChanged
(the set of resources mutated), plus notifications/progress for
long-running calls. Data products are read via list_data_products,
subscribe_preview_data, and get_preview_data.
For the full tool surface, URI scheme, and wire protocol see
docs/daemon/MCP.md.
Register the local server with OpenCode
OpenCode v2 stores local MCP servers under mcp.servers. Register the server
globally in the user config ($XDG_CONFIG_HOME/opencode/opencode.json when set,
otherwise ~/.config/opencode/opencode.json):
compose-preview mcp install --opencode
Use a project-local opencode.json instead, or name an explicit config file:
compose-preview mcp install --opencode --scope project
compose-preview mcp install --opencode --opencode-config /path/to/opencode.json
The command preserves existing top-level, mcp, and sibling server keys while
adding this current-v2 shape when it is missing or broken (a healthy entry is
left as it is). The launcher is the stable one (for example
~/.local/bin/compose-preview), never a versioned
compose-preview-<version>/bin/ path that the next upgrade deletes. A user-scope
entry has no --project: the server finds the project from the client’s roots
or working directory. Only --scope project adds --project=<dir>:
{
"mcp": {
"servers": {
"compose-preview-mcp": {
"type": "local",
"command": [
"/home/you/.local/bin/compose-preview",
"mcp",
"serve"
],
"codemode": false
}
}
}
}
If opencode.jsonc already exists at the selected user or project scope, the
CLI selects that existing file and takes the manual-merge path rather than
creating a competing opencode.json.
The older direct mcp.compose-preview-mcp shape and enabled: true are not
OpenCode v2 configuration. If the target is .jsonc or contains comments,
the CLI does not rewrite it because that would discard comments; it prints the
v2 snippet and the exact file to merge manually. Restart OpenCode and run
opencode mcp list to verify the connection. OpenCode skill installation is
covered in the
cross-harness guide.
Register the local server with Antigravity
Prefer the compose-preview plugin from
yschimke/compose-agent-plugins: it
registers the server and also carries the skill and hooks. When it is installed
(~/.gemini/config/plugins/compose-preview), mcp install writes no global
entry, because that would duplicate the plugin’s server.
Without the plugin, mcp install --antigravity merges compose-preview-mcp
into mcpServers of whichever Antigravity config already exists:
~/.gemini/antigravity/mcp_config.json first, then
~/.gemini/config/mcp_config.json. --antigravity-config <path> names the file
explicitly. compose-preview mcp doctor lists both paths, whether each holds an
entry, whether the plugin is installed, and any global entry that duplicates it.
Connect a remote UI-builder session
The native MCP profile can expose eight additional tools backed by the same persisted preview-server UI-builder session a browser is editing. Supply the server URL as an argument and the temporary agent grant as an environment variable, keeping the secret out of process arguments:
COMPOSE_PREVIEW_UI_BUILDER_TOKEN='<grant>' \
compose-preview mcp serve --ui-builder-url https://preview.example/
Grants keep ui-builder-read, ui-builder-write, and ui-builder-export
independent. The adapter is a remote protocol client only: it does not copy the
server’s reducer, catalog, renderer, or persistence into this repository.
Shared settings
The MCP server’s settings_read / settings_update tools (the native
settings page in ChatGPT and Codex) persist their defaults to one file,
~/.compose-preview/settings.json — or wherever COMPOSE_PREVIEW_SETTINGS_FILE
points:
{ "schema": "compose-preview-settings/v1", "values": { "darkTheme": true, "locale": "fr" } }
The compose-preview CLI reads the same file (it never writes it) with the
server’s precedence: an explicit flag beats a setting, which beats the
built-in default.
| Setting | Where the CLI applies it | The flag that wins over it |
|---|---|---|
device (id:<device>, or preview for none) |
render-matrix device axis, record overrides |
--device, --overrides device=… |
darkTheme |
render-matrix ui-mode axis, record overrides |
--ui-mode, — |
fontScale (0 for none) |
render-matrix font-scale axis, record overrides |
--font-scale, --overrides fontScale=… |
locale (BCP-47, empty for none) |
render-matrix locale axis, record overrides |
--locale, --overrides localeTag=… |
A setting fills only what the command line leaves unset: render-matrix
--locale en,ar with darkTheme on renders both locales dark. show and
render drive the Gradle render, which draws each preview exactly as its
@Preview declares, so they cannot apply these four; when any is set they
say so on stderr rather than ignore it quietly. The server-only keys
(renderResult, imageToModel, replicasPerDaemon, uiBuilderMcpAppLayout)
and any key the CLI does not know are ignored. A bad value falls back to its
default with a warning and keeps the other keys; a file that is not valid JSON
warns once and every default applies.
Deep links into the ChatGPT / Codex sidebar
The local server’s previews_library tool is a sidebar app, and
compose-preview show --link prints a link per preview that opens it there,
rendered on open:
compose-preview show --id com.example.HomeKt.HomePreview --link \
--openai-plugin-id <plugin id> --openai-marketplace <marketplace>
| Flag | Opens in | Link |
|---|---|---|
--link (or --link=desktop) |
ChatGPT / Codex desktop | codex://plugins/<id>@<marketplace>/app/previews_library?path=… |
--link=mobile |
ChatGPT mobile | chatgpt://plugins/<id>@<marketplace>/app/previews_library?path=… |
--link=web |
chatgpt.com | https://chatgpt.com/plugins/<id>/app/previews_library?path=… |
The path is the library route /preview/<compose-preview URI>,
percent-encoded (RFC 3986: a space is %20, and / ? & = : @ inside a value
are escaped). The plugin id and marketplace depend on how the plugin was
published, so they are never guessed: pass the flags, or set
COMPOSE_PREVIEW_OPENAI_PLUGIN_ID (and COMPOSE_PREVIEW_OPENAI_MARKETPLACE
for a plugin installed from a custom marketplace; a flag beats the variable).
Without a plugin id, show --link prints that guidance instead of a link and
still shows the previews. Text output adds a link: line under each preview;
--json adds a link field to each one.
The link’s workspace segment is derived the way the server derives it for a
project registered by path, from the project directory’s name and canonical
path — a project registered under a custom rootProjectName has a different
id, and its links will not resolve.
What we tell agents
Point the agent at the
compose-preview skill.
It’s the install-and-iterate playbook: it checks for the CLI, bootstraps it
via the installer if missing, and walks the agent through
rendering and verifying. The skill and the installer aren’t alternatives —
the installer is just the one command the skill runs to get the CLI in place.
</content>