MCP server: One MCP server.

Compose Preview Catalogs

@yschimkeMCP server · External

Browse, inspect and render Jetpack Compose Material 3 and Wear component catalogs.

UI designv3.122.0Apache-2.0Kotlin
Freeno charge per call

One address for any MCP client. No sign-in needed on this server.

What it does

24 read9 act13 other

Grouped by tool name.

What it costs

Free

Odel charges nothing per call. The publisher may charge for its own service.

Tools

46 · grouped by tool name · checked 1 Oct 2026

Read · 24Look things up

  • catalog_list_projects
  • catalog_list_previews
  • catalog_render_preview
  • catalog_list_devices
  • catalog_diff_semantics
  • catalog_history_list
  • catalog_history_diff
  • catalog_history_read
  • ui_builder_list_catalogs
  • ui_builder_search_components
  • ui_builder_list_designs
  • ui_builder_get_design
  • ui_builder_preview_catalog_recovery
  • ui_builder_export
  • ui_builder_validate
  • ui_builder_get_links
  • ui_builder_list_comments
  • ui_builder_resolve_comment_thread
  • ui_builder_view
  • catalog_list_data_products
  • catalog_get_preview_data
  • list-all-documentation
  • get-documentation-for-story
  • preview-stories

Act · 9Change something

  • ui_builder_create_design
  • ui_builder_apply
  • ui_builder_move_design_home
  • ui_builder_put_asset
  • ui_builder_share_design
  • ui_builder_set_links
  • ui_builder_rename_design
  • ui_builder_delete_design
  • ui_builder_post_comment

Other · 13Neither hinted nor named

  • request_access
  • poll_access
  • status
  • catalog_library
  • ui_builder_open
  • catalog_render_matrix
  • ui_builder_await_design
  • ui_builder_replace_design_document
  • ui_builder_design_access
  • ui_builder_acknowledge_comment
  • ui_builder_react_to_comment
  • ui_builder_await_comments
  • ui_builder_render_native
Try it in the Inspector
README18 min read

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.

AssetWhat it is
compose-preview-server-<v>.tar.gzthe server: catalog hosting, the HTTP routes, the playground, the viewer surfaces. What compose-preview serve, browse and ui-builder launch
compose-preview-mcp-<v>.tar.gzthe MCP server, behind compose-preview mcp serve

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.

shell
./gradlew check ktfmtCheckAll
npm --prefix serve-web ci
npm --prefix serve-web run verify

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.

After a restart, configured catalogs keep their home-page cards while they load. With --catalog-cache-dir on a persistent volume, each card can show its last successful hero thumbnail with a small “Loading catalog…” label. The thumbnail is scoped to the catalog's configured repository and branch. Opening a pending catalog promotes it ahead of the remaining startup fetches; the fetch already running finishes first. The loading page retries automatically until the catalog is available.

Agent plugins

Install the compose-preview local MCP plugin or the compose-catalogs hosted catalog and UI Builder plugin from yschimke/compose-agent-plugins. That guide covers each supported agent host, the canonical skill bundles from yschimke/skills, and migration from the old marketplace.

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.

Design guidelines: preview_guidelines_prompt, check_preview_guidelines

Two tools check previews against a catalog's design guidelines (ui-builder.guidelines.json, the module's build/compose-previews/ copy unless guidelines names a file or URL), on the same ee.schimke.composeai:design-guidelines engine as the CLI's compose-preview guidelines. Both take previews (preview names, FQNs or compose-preview:// URIs, judged as one batch), guidelines and surface (screen, widget or component).

  • preview_guidelines_prompt needs no key. It returns the batched request — the rules, each preview's source and accessibility nodes, and the renders as image blocks — so an agent can judge it with its own model.

  • check_preview_guidelines asks OpenRouter, with the key from COMPOSE_PREVIEW_OPENROUTER_KEY, else the one compose-preview guidelines key set stored in the user's credentials file (~/.config/compose-preview/credentials.json). A stored key is used only for a workspace whose Gradle root the user trusted with compose-preview guidelines key trust <dir>; the tool cannot grant that itself, and refuses before rendering, naming the command. Never a tool argument; without a key the tool says how to set one. model defaults to deepseek/deepseek-v4.1-flash and max_cost (US dollars, default 0.10) caps the call. It returns each preview's verdicts with the node ids and picture regions they point at, the model that answered and the cost.

    With base_url (and model, the endpoint's own name) it asks an OpenAI-compatible endpoint instead, like compose-preview guidelines --backend openai: a loopback server needs no key, and any other only gets previews when the user stored a key for that origin with compose-preview guidelines key set --for <base_url> and trusted the project. The server reads no key from COMPOSE_PREVIEW_MODEL_KEY here, because the caller picks the endpoint. Endpoints report no cost, so max_cost does not apply.

.uid designs: design_open

In ChatGPT or Codex desktop, opening a .uid UI Builder design opens it in the UI Builder editor instead of the default viewer, and edits save back to the file (compose-ui-builder#364). The editor is compose-ui-builder's MCP App (#366, its design note). It reads, saves and follows the file itself, through the host's resources/read, openai/resources/write and resources/subscribe. This server adds three things:

  • design_open, a file-entrypoint tool (extensions: ["uid"]) that takes FileInput {file: {name, resourceUri}} and only acknowledges the call. It has no {path} form for model calls. The editor can load and save only through a host that offers the openai/resource capability, so in Claude Code or Antigravity it would open an editor with no file.
  • ui://compose-ui-builder/editor, the archive's mcp-app/ui-builder-mcp-app.html shell, served as text/html;profile=mcp-app. Its placeholders are filled in on every read: the asset base URL, and the layout from the uiBuilderMcpAppLayout setting (below). _meta.ui.csp.resourceDomains and connectDomains name the asset origin. Its display mode is fullscreen only.
  • The asset origin, http://127.0.0.1:<ephemeral>/ui-builder/v/<version>/. The editor is about 45 MB unpacked, too large to inline, so the shell loads it from this origin. The listener starts on the first read of the editor resource. It serves only the files in the archive, answers GET, HEAD and OPTIONS only, and refuses path traversal. It sends:
    • Access-Control-Allow-Origin: *, with no credentials. The app's sandboxed frame may have an opaque null origin, and echoing that would grant the same access as *;
    • Access-Control-Allow-Private-Network: true in answer to a Private Network Access preflight;
    • application/wasm for .wasm, and the right type for each script, stylesheet, JSON file and font.

The compose-preview-mcp distribution carries the editor archive as ui-builder/compose-preview-ui-builder-web.zip. It is the composeai-ui-builder release the catalog pins, and it is read in place, without unpacking. To use another archive, point COMPOSE_PREVIEW_UI_BUILDER_WEB at a ZIP or an unpacked directory. The tool and the resource are registered only when the archive's ui-builder-web.json declares "mcpApp": 1. An older archive, or no archive, leaves both out, and no error is reported.

The editor opens on a focused canvas by default: the design fills the panel, with a slim file bar (name, saved state, Save, and Full editor), and a node's menu offers Quick edit and Comment, which sends the node to the chat (compose-ui-builder#378). To open designs in the whole desktop editor instead, set uiBuilderMcpAppLayout to full — on the plugin's settings page, with settings_update {"set": {"uiBuilderMcpAppLayout": "full"}}, or in ~/.compose-preview/settings.json. The value is focused (the default) or full; anything else opens focused. It is read each time the editor resource is read, so a change applies to the next design opened, and the editor's own button switches either way.

Still to check in a real host, as part of the #1236 probe:

  • whether ChatGPT/Codex desktop accepts a loopback http://127.0.0.1:<port> origin in resourceDomains / connectDomains for a local stdio server, and whether its sandbox sends the PNA preflight;
  • whether the sandbox allows WebAssembly compilation ('wasm-unsafe-eval') and WebGL;
  • whether resources/read on the .uid file reports writable: true, so that saves happen.

On a phone

The pages work on phones, and the site installs as an app with a share target: a screenshot shared to it opens the bug report with the image attached. serve --lan prints a QR code for opening a laptop's server on a phone. A plain-http LAN address is not a secure context, so installing the app, Web Share and the clipboard need HTTPS or adb reverse. See Compose Preview on a phone.

Spatial and WebXR previews

A portable bundle can publish an XR preview as a version-one SpatialScene document and its panel textures:

text
previews/<preview-id>.spatial/scene.json
previews/<preview-id>.spatial/<panel>.png

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:

shell
./gradlew :server:distTar
tar -xzf server/build/distributions/compose-preview-server-*.tar.gz
./compose-preview-server-*/bin/compose-preview-server help

The binary has five commands, and help lists them:

CommandWhat it does
serveHost previews — fetched bundles, published catalogs, or a local module's @Preview functions with --module / --discover.
uiBuild this project's previews and open the Compose UI builder against them.
playgroundserve with the snippet compile lane admitted.
designRender, export or read a UI-builder design from a server that is already up. The one command that does not serve.
help [command]The command list, or one command's options.

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:

shell
compose-preview-server ui --module app

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):

shell
export COMPOSE_PREVIEW_TOKEN=...          # or let the command ask a human to approve a grant
compose-preview-server design list --server https://preview.coo.ee
compose-preview-server design status --workspace . --summary
compose-preview-server design render spotify-wear-widget -o cover.png
compose-preview-server design export spotify-wear-widget -o Widget.kt
compose-preview-server design get spotify-wear-widget > design.json

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.

shell
compose-preview-server design get spotify-wear-widget --server https://preview.coo.ee > doc.json
compose-preview-server design render --document doc.json --local \
  --catalog wear-m3.bundle --assets ./assets -o replay.png
compose-preview-server design export --document doc.json --local   # the generated Kotlin, no server

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.

/ui-builder/projects groups multiple design files, shared JSON resources and collaborators into an app workspace. Projects can load commit-pinned .uid files from GitHub and propose reviewed edits as a pull request while displaying each design's canonical home; see UI_BUILDER_HOSTED_PROJECTS.md.

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.

Similar modules

Browse UI design