Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 19 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,18 @@
## Quickstart

```bash
git clone https://github.com/obj-p/PreviewsMCP.git
cd PreviewsMCP
swift run previewsmcp examples/spm/Sources/ToDo/ToDoView.swift
brew tap obj-p/tap
brew install previewsmcp
previewsmcp MyView.swift
```

A live macOS preview window opens. Edit the source file and the window hot-reloads.

> Want to build from source instead? See [From source](#from-source).

## Why PreviewsMCP?

PreviewsMCP compiles your `#Preview` closure into a dylib and loads it into a real app process (macOS `NSApplication` or iOS simulator `UIApplication`) with hot-reload — driven entirely from the command line or over MCP. No Xcode process required.
PreviewsMCP JIT-compiles your `#Preview` closure and links it into a real app process (macOS `NSApplication` or iOS simulator `UIApplication`) with hot-reload — driven entirely from the command line or over MCP. No Xcode process required.

That makes it a standalone, extensible preview workflow:

Expand All @@ -37,9 +39,9 @@ That makes it a standalone, extensible preview workflow:

### Solving the Xcode preview sandbox problem

Xcode previews run your code inside Apple's preview agent — a real app process, but an opaque one. You can't hook into its lifecycle, run your own initialization, or extend it. `FirebaseApp.configure()`, custom font registration, auth setup, and DI containers have nowhere to run. The ecosystem answer is "mock everything," and at scale teams maintain **micro apps** — standalone app targets that render a single feature with controlled dependencies. Airbnb's dev apps drive over 50% of local iOS builds. Point-Free's isowords has 9 preview apps. Every team pays the maintenance tax: separate targets, schemes, and mock setups that drift.
Xcode previews run your code inside Apple's opaque preview agent, so you can't run your own initialization. `FirebaseApp.configure()`, font registration, auth, and DI containers have nowhere to go. Teams work around this with **micro apps** — standalone targets that render one feature with controlled dependencies — and pay a steady maintenance tax in extra targets, schemes, and mocks. (Airbnb's dev apps drive over half of local iOS builds; Point-Free's isowords ships 9 preview apps.)

Because PreviewsMCP hosts your preview in its own app process, you can extend that process. The [setup plugin](Sources/PreviewsSetupKit/PreviewSetup.swift) provides the hook: a `PreviewSetup` protocol where `setUp()` runs once per session (SDK init, auth, font registration, DI container) and `wrap()` surrounds every preview render (themes, environment values). It's the micro app's dependency layer extracted into a reusable framework — without maintaining a separate app target.
PreviewsMCP hosts your preview in its own app process, so you can extend it. The [setup plugin](Sources/PreviewsSetupKit/PreviewSetup.swift) is the hook: `setUp()` runs once per session (SDK init, auth, fonts, DI) and `wrap()` surrounds every render (themes, environment values). It's the micro app's dependency layer as a reusable framework, with no separate target to maintain.

## Installation

Expand All @@ -55,16 +57,19 @@ brew install previewsmcp
```bash
git clone https://github.com/obj-p/PreviewsMCP.git
cd PreviewsMCP
scripts/build-jit-llvm.sh # one-time: build the LLVM JIT artifacts (slow)
scripts/build-jit-llvm-iossim.sh # one-time: build the iOS-simulator JIT artifacts
swift build -c release
```

The binary is at `.build/release/previewsmcp`.
The binary is at `.build/release/previewsmcp`. The LLVM JIT artifacts are mandatory — `swift build` fails fast without them.

### Requirements

- macOS 14+
- Xcode 16+ (for iOS simulator support)
- Apple Silicon
- `cmake` and `ninja` (only to build the JIT artifacts from source; `brew install cmake ninja`)

## Capabilities

Expand All @@ -79,7 +84,7 @@ The binary is at `.build/release/previewsmcp`.

### CLI

Every CLI subcommand talks to a daemon process over a Unix socket. The daemon auto-starts on first use (ADB-style) and stays alive across invocations — no manual lifecycle management needed.
Every CLI subcommand talks to a background daemon that auto-starts on first use, so there is no lifecycle to manage (see [Daemon model](#daemon-model)).

```bash
previewsmcp help # top-level overview
Expand Down Expand Up @@ -135,16 +140,11 @@ previewsmcp kill-daemon # stop the daemon process

#### Structured output

Read-oriented commands support `--json` for scripts and agent consumption:
Read-oriented commands (`run --detach`, `snapshot`, `variants`, `list`, `status`, `simulators`, `elements`) support `--json` for scripts and agent consumption:

```bash
previewsmcp run MyView.swift --detach --json | jq .sessionID
previewsmcp simulators --json | jq '.simulators[] | select(.state == "Booted")'
previewsmcp list MyView.swift --json
previewsmcp snapshot MyView.swift -o out.png --json
previewsmcp variants MyView.swift --variant light --variant dark -o ./shots --json
previewsmcp status --json
previewsmcp elements --json
```

### Project config
Expand Down Expand Up @@ -191,8 +191,8 @@ The CLI uses an auto-started background daemon that manages preview sessions. On

When a command appears stuck — most commonly `run` during an iOS host build — there are two places to look:

1. **The CLI's own stderr.** The daemon streams progress messages for each build phase (`detecting project`, `compiling host app`, `booting simulator`, …) back to the CLI as MCP log notifications, which the CLI forwards to stderr. Whichever phase was printed last is where it's stuck.
2. **The daemon log.** Daemon stderr is redirected to `~/.previewsmcp/serve.log` on spawn, so startup failures and anything the daemon logs outside an active RPC land in this file. Stream it in a second terminal:
1. **The CLI's own stderr.** The daemon forwards a progress message for each build phase (`detecting project`, `compiling host app`, `booting simulator`, …). The last phase printed is where it's stuck.
2. **The daemon log.** Daemon stderr goes to `~/.previewsmcp/serve.log`, which captures startup failures and anything logged outside an active command. Stream it in a second terminal:

```bash
previewsmcp logs -f # follow new lines
Expand All @@ -204,8 +204,8 @@ When a command appears stuck — most commonly `run` during an iOS host build
Other levers:

- `previewsmcp status --json` — confirm the daemon is still alive (`running` / `transitional` / `stopped`) while a command is blocked.
- `previewsmcp kill-daemon` followed by re-running the command — gives a clean daemon and a fresh `serve.log` worth of context.
- `PREVIEWSMCP_SOCKET_DIR=/tmp/previewsmcp-debug previewsmcp …` — relocates the socket, PID, and log files so you can isolate a debug run from an existing daemon. Export the variable (or prefix both invocations) so `previewsmcp logs` targets the same isolated dir rather than falling back to the default `~/.previewsmcp/serve.log`.
- Subprocess failures (`xcodebuild`, `swiftc`, `codesign`) surface their captured stderr in the error returned to the CLI; a hang, however, won'tso if the last phase logged is a build phase and the command isn't progressing, check for a live `xcodebuild` or `swiftc` process with `ps -ef | grep -E 'xcodebuild|swiftc'`.
- `previewsmcp kill-daemon`, then re-run — gives a clean daemon and a fresh `serve.log`.
- `PREVIEWSMCP_SOCKET_DIR=/tmp/dbg previewsmcp …` — relocates the socket, PID, and log files to isolate a debug run from your main daemon. Set it on every invocation (including `logs`) so they share the dir.
- Subprocess failures (`xcodebuild`, `swiftc`, `codesign`) include their stderr in the error. A hang won't, so if a build phase is stuck check for a live process: `ps -ef | grep -E 'xcodebuild|swiftc'`.