
Compose Preview Catalogs
io.github.yschimkev3.88.0Updated Oct 1, 2026
Browse, inspect and render Jetpack Compose Material 3 and Wear component catalogs.
Overview
Lets an assistant browse, inspect and render Jetpack Compose Material 3 and Wear component catalogs through a hosted preview service.
- What it does
- Exposes hosted Compose preview catalogs over a remote Streamable HTTP MCP endpoint, so an assistant can list and inspect Material 3 and Wear components and request rendered previews. The service also supports made-to-order renders and structured data products, and a separate local stdio MCP server offers tools such as render_preview and find_previews_for_file for editor and agent plugins. Spatial and WebXR preview scenes can be published and viewed in a browser stage.
- When to use it
- Use it when an assistant needs to look up or render Jetpack Compose Material 3 and Wear component previews without setting up a local build, or when a local editor or agent plugin needs preview discovery and rendering against a workspace.
- Requirements
- A remote endpoint at preview.coo.ee over Streamable HTTP; no local runtime or package is required for the hosted service. Access may use the optional X-Compose-Preview-Token header or the service's interactive access grant. The local stdio MCP server and the standalone distribution run on Java 17 or newer, with Java 21 needed for the UI builder's PNG and SVG export.
Installation
In SourceWeft
- Open Compose Preview Catalogs in the dashboard and add it to a workspace.
- Enable the server for the chats that should use its tools.
Web executable via Streamable HTTP. Remote servers run from the web runtime once configured in a workspace.
Other MCP clients
Add this to your client's mcpServers config.
{
"mcpServers": {
"compose-preview": {
"type": "http",
"url": "https://preview.coo.ee/mcp"
}
}
}README
Compose Preview Server
The server behind compose-preview serve: catalog hosting, live render sessions, the playground,
and the browser viewer surfaces. Its history was extracted from
yschimke/compose-ai-tools; the CLI remains there and
launches this repository's distribution.
What this repository ships
Archives on each GitHub release, and nothing on Maven Central.
The Compose/Wasm frontend is not an asset of these releases. It is built and released by
yschimke/compose-ui-builder as
compose-preview-ui-builder-web-<v>.zip on that repository's own releases, and this build resolves
it from there — the server archive carries an unpacked copy either way, and the standalone zip is
for serving the editor yourself or pointing an existing serve at with --ui-builder-dir.
Nothing here is a Maven coordinate. Six modules used to publish — compose-preview-serve,
compose-preview-mcp, and four more that existed on Central only because the first one's POM named
them — and the arrangement cost more than it bought. A project dependency reaches a published POM as
a coordinate, so adding one to :server meant publishing the dependency too; getting that wrong
shipped compose-preview-server:ui-builder-export-jvm:unspecified in 3.1.0 and an unresolvable
compose-preview-serve from 3.3.0 through 3.8.0. Six releases nobody could resolve, for transitives
nobody wanted.
The one library consumer was compose-ai-tools' :cli, which compiled two wire-drift tests against
compose-preview-serve. Those tests launch the distribution now
(compose-ai-tools#5436) — the artifact
serve runs anyway, so they check the wire that actually ships. Released coordinates up to and
including 3.24.0 stay resolvable on Central; there simply will not be new ones.
render-host exists because rendering a packed bundle and reading a preview timeline out of git
open no sockets, and a caller doing only that should not link ktor-server-*, jmdns and
kotlin-reflect to do it. It is published from
compose-ai-tools, not from here: that is where
everything it depends on lives, and offline behaviour belongs in that layer
(#180). It used to be this
repository's :render-host module, publishing as compose-preview-render-host; that coordinate
stays resolvable at its final 2.x for anyone pinned to it, and the new one is on compose-ai-tools'
version line. The measured before/after and the transitives it deliberately cannot drop are recorded
in its build file there, and checkRenderHostIsServerFree moved with it.
The UI builder is a second repository —
yschimke/compose-ui-builder — and :server
consumes it as releases. Its :ui-builder-runtime owns authoritative persistent design state, exact
catalog validation and revision-pinned export orchestration there; :server supplies
HTTP/authentication and adapts its narrow render request onto the render host. The runtime therefore
has no Ktor, daemon/render-host, MCP or Compose UI dependency, while the offline render host has no
UI-builder protocol or service edge. Three jars and a BOM come from Maven Central at
composeai-ui-builder in gradle/libs.versions.toml, and the editor archive from that repository's
GitHub release; -PcomposeUiBuilderDir swaps all four for a checkout when working on both at once.
The build is intentionally repository-independent. Compose Preview implementation artifacts resolve
from Maven Central at the version in gradle/libs.versions.toml; wire contracts resolve separately
from compose-preview-contracts. There is
no mavenLocal() repository and no shared version catalog outside this repository. A composite
build is opt-in only — -PlocalBuilds=tools,daemon,contracts for the upstreams,
-PcomposeUiBuilderDir for the UI builder — and for unreleased contract or generator changes an
explicit
local dependency manifest selects artifacts compiled from
local checkouts. Leaving those options unset retains the released dependency graph.
Java
Run the distribution on Java 17 or newer. The UI builder's PNG/SVG export needs Java 21.
The two numbers are one difference, and it is worth stating plainly rather than leaving to be
inferred from build files. The server itself, mcp serve, and every artifact this repository
publishes target Java 17, because compose-ai-tools' CLI compiles against them on a 17 toolchain
and its serve / mcp serve commands launch this distribution's start script as a separate
process, resolving java from JAVA_HOME/PATH. Only the UI builder's renderer is above that
floor: the design it rasterizes is drawn by a Compose preview compiled for Java 21, packaged into
the render bundle, and unpacked into a daemon that runs on the server's own JVM.
On an older JVM the server still starts and everything else works; the UI builder loses PNG and SVG
export and says so at startup, naming the version it found and the one it needs. Point JAVA_HOME
at a Java 21 JDK to get it back, or pass --ui-builder-state-dir none to run without the UI builder
at all. The published container image
(deploy/image/Dockerfile) is built on Temurin 21 and clears both.
Both floors are declared once, as java-server and java-ui-builder in
gradle/libs.versions.toml, which is also where the reasoning lives.
:server:checkServerJvmFloor fails the build if anything above java-server reaches the
distribution's classpath, and the render bundle carries java-ui-builder as data so the startup
message cannot drift from the bytes it describes.
Build
./gradlew check needs one JDK: 17. The UI builder's frontend lane compiles at 21 in its own
repository, and the only lane here that still wants both JDKs is visual-harness, which builds that
repository's Wasm distributions from a checkout. Gradle finds a 21 installed anywhere it already
scans (/usr/lib/jvm, SDKMAN, asdf, jabba); if it cannot, the failure is
No matching toolchains found for requested specification: {languageVersion=21} and the fix is to
install one, not to lower the target.
The independently installable visual harness lives in preview-harness/. The experimental
Compose/Wasm frontend lives in wasm-ui/. The UI builder itself — editor, renderer, artwork and the
Jetcaster fixtures — lives in
yschimke/compose-ui-builder; this build resolves
its editor archive, runtime and export from that repository's releases, and the harness builds the
renderer and fixture distributions from a checkout of it. The server distribution packages the
editor at /ui-builder/; the existing catalog-scoped /wasm/<system>/ preview application remains
a distinct feature and route. The editor route opens an interactive Wasm editor around the frozen
Jetcaster design; clean benchmark modes remain available to the independent visual harness. The
editor mounts an exact retained renderer runtime under /ui-builder/runtime/<runtimeId>/ in a
sandboxed iframe and receives measured node/slot geometry without placing editor overlays in the
Compose tree. Catalog runtimes retain a small commit-pinned source descriptor after their catalog
generation is swept, so an older design revision can recover its exact runtime after a restart. The
compressed archive shares the bounded catalog blob pool; expanded historical trees are
process-local leases with an inactivity timeout and a hard count ceiling rather than an unbounded
second artifact store.
The Remote Compose authoring extension is behind the default-off compile-time option
-PuiBuilderRemoteCompose=true. It spans two repositories — the MCP adapter's half is generated
here, and the server's half is baked into the UI-builder export at its build time, so enabling the
server half means building against a UI-builder checkout with the option
(-PcomposeUiBuilderDir=…). See
feature scope and verification.
Remote catalog MCP
The server can expose every hosted catalog through one aggregate Streamable HTTP MCP endpoint at
/mcp. Enable it with --agent-grants --catalog-mcp; published resources require a
short-lived preview grant, while made-to-order renders and structured data products require a
live grant. The endpoint is separate from the stateful UI-builder authoring MCP surface, but both
use the same authenticated user approval and revocation flow. See
the catalog MCP design and setup guide.
Local daemon MCP
compose-preview-mcp also exposes the local stdio MCP server used by editor and agent plugins.
render_preview remains a token-frugal semantics observation by default; a local client that can
read the same filesystem passes inline: false to receive the rendered PNG's absolute pngPath,
dimensions, SHA-256, elapsed render time, and a per-session changed signal instead of image bytes.
After discovery, find_previews_for_file maps an absolute source path — or one relative to a
registered workspace — to the preview URIs declared there. Both returned URIs can be passed
directly to render_preview.
Spatial and WebXR previews
A portable bundle can publish an XR preview as a version-one SpatialScene document and its panel
textures:
The viewer opens these scenes in an orbitable Three.js/WebGL stage and offers Enter VR when the
browser exposes an immersive-vr WebXR session. WebXR requires a secure context, so a headset must
reach the server over HTTPS (localhost remains suitable for desktop WebGL development). Uploaded
bundles may carry JSON scene documents and PNG/JPEG/WebP textures only; all assets are served from
the scene's same-origin /spatial/ route.
Build and run the standalone distribution with:
The binary has five commands, and help lists them:
Flags may still be passed with no command in front of them: compose-preview-server --module app
is exactly compose-preview-server serve --module app, and stays supported.
ui needs a build host — the compose-preview binary — because discovering and building a local
Gradle project is work this server asks for over a pipe rather than doing itself. The builder's
palette is a packaged design-system catalog; what ui adds is the project, by pointing
--ui-builder-components at the module's discovered components.json so the Compose export
generates call sites for your composables. From a checkout with the CLI installed:
design is the client half of that lane, and the one command here that starts no server: it talks
to one that is already up — a local serve, or a deployment — and writes a design's pixels or its
generated source to a file, which is what a session otherwise re-invents as a curl into /mcp, a
jq to unwrap the envelope and a base64 -d
(#529):
The credential is read from $COMPOSE_PREVIEW_TOKEN (or the older
$COMPOSE_PREVIEW_UI_BUILDER_TOKEN that scripts/ui-builder/design-sync.mjs reads), never from a
flag. With neither set — or after a restart has dropped the grant — the command runs the server's
own device-code flow: it prints an approval link and a code, waits for a human, and carries on.
A refused export prints the generator's own diagnostics to stderr and exits non-zero, writing
nothing, so it composes in CI.
design status is the bounded SessionStart probe: it reads the checkout's
ui-builder/designs/index.json, totals unacknowledged comments on server-home designs, and compares
each tracked temporary copy with the server's current document. It never starts an authorization
flow, never follows a tracked server URL to another origin, and --summary prints one redacted line
(or nothing when there is nothing to act on). --json emits the versioned status envelope.
--local is the half of that command which needs no server at all: it runs the same generator,
compiler and render daemon a server would, here, against a catalog bundle on disk — and says
why a frame is missing rather than only that it is, which the wire reply cannot
(#551). That is what makes a
broken host debuggable: design get captures its document (a read, not the render lane under
suspicion) and the file replays anywhere.
A local render prints the classpath it resolved, the daemon opener it built and the compiler's own
diagnostics; --components <catalog>=<components.json> names the record a record-driven catalog's
call sites are proven against, exactly as serve --ui-builder-components does.
A signed-in person can browse every design the service permits them to read at
/ui-builder/designs. The page separates owned designs from designs shared by somebody else,
shows the exact service reason when one cannot open, and lets an owner manage sharing inline. It is
actor-scoped and has no connection to the all-designs operator surface at /admin/ui-builder.
A served catalog's own composables can also be offered inside the builder's catalogs as a
component pack (--ui-builder-packs confetti-mobile=mobile,confetti-wear=wear), switched on by an
author from the editor's settings; see
docs/design/UI_BUILDER_COMPONENT_PACKS.md.
Releases attach that distribution to the GitHub release, then build the production
ghcr.io/yschimke/compose-preview-host image. The canonical Docker and preview.coo.ee
configuration lives in deploy/image and
deploy/preview.coo.ee.
Releasing
One lane in release.yml: build
compose-preview-server-<v>.tar.gz, compose-preview-mcp-<v>.tar.gz and
compose-preview-ui-builder-web-<v>.zip, attach them to the GitHub release, then build the
compose-preview-host image.
The GitHub release stays a draft until that lane has succeeded, so a failed build never leaves a tag whose assets do not exist.
There used to be a second, skippable Maven lane, with a release:no-maven label and a
publish_maven workflow input to turn it off. Both are gone with the publication — every release is
now what that label used to ask for.
Repository boundary
checkServeModuleBoundary walks the resolved runtime classpath, transitives included. It rejects
project dependencies, renderer/daemon implementations, the Gradle plugin, and any unregistered
ee.schimke.composeai coordinate. Update its positive allowlist only when a reviewed dependency
floor change is intentional.
The source package remains ee.schimke.composeai.cli.serve for binary/source continuity. A package
rename is independent of repository ownership and is not part of the extraction.
Source: README.md at commit 5d91e49
Tools
0Version history
1- v3.88.0LatestOct 1, 2026

