# LightBites developer docs: scripts, the command line and MCP

> LightBites for Mac can be controlled from outside, over a connection that never leaves your computer. Run any command, press any button, read what happened. Use it from a shell script, a test, or an AI assistant (Model Context Protocol).

Source: http://localhost:3000/docs · Summary for language models: http://localhost:3000/llms.txt

## Overview

LightBites listens on a port on your own Mac when you ask it to. You send one line of JSON, you get one line back. Three kinds of request cover everything a person can do in the app.

- **Commands** (`engine.execute`): Everything the app does to your photos is a named command: rate, flag, move, export, adjust a slider. Run any of them by name.
- **Screens** (`ui.set · ui.inspect`): Open a screen, a sheet or a photo, and read what the app is showing: the selection, the screen, timings.
- **Controls** (`ui.tree · ui.press`): Find any button, toggle, field, row or photo in a window by its name, press it, type into it, or choose a menu item.

The controls layer reads the app’s own accessibility information, the same names VoiceOver reads. It works inside the app, so it sends no clicks or keystrokes to the system and needs no permission.

## Quick start

1. Start the app with the port switched on. Use the demo library (`LIGHTCRAFT_DEMO=1`, an in-memory sample library that saves nothing) or a copy (`LIGHTCRAFT_LIBRARY=/path/to/a/copy`); without either, the app opens your own library and anything you run changes it.

```sh
LIGHTCRAFT_DEMO=1 LIGHTCRAFT_CONTROL_PORT=18731 \
  /Applications/LightBites.app/Contents/MacOS/LightBites
```

2. Send a request:

```sh
echo '{"id":1,"method":"host.state"}' | nc -w 2 127.0.0.1 18731
```

The reply comes back as one line:

```json
{"id":1,"ok":true,"result":{"active":21,"selectionCount":1,"visibleCount":24,"undo":null,"redo":null,…}}
```

3. Or from Python:

```python
import json, socket

def call(method, params=None, port=18731):
    with socket.create_connection(("127.0.0.1", port)) as s:
        s.sendall((json.dumps({"id": 1, "method": method, "params": params or {}}) + "\n").encode())
        return json.loads(s.makefile().readline())

print(call("ui.press", {"label": "4 stars"}))
```

## Protocol

Plain text over a local TCP connection. One JSON object per line, in both directions.

```json
{"id": 7, "method": "ui.press", "params": {"label": "4 stars"}}
```

```json
{"id": 7, "ok": true, "result": {"label": "4 stars", "path": "0.0.2.3", "role": "AXButton"}}
```

- `id` is yours. It comes back unchanged, so you can match replies to requests. It can be a number or a string.
- `method` is the name of the request; `params` is an object, and may be left out.
- A connection can send many requests, one after another. Each is answered in turn.
- A request that takes time (an export, say) is answered when it is done, up to two minutes.

A failed request has `"ok": false`, a short `code`, and an `error` in plain words. Commands report their own codes, such as `unknownCommand`, `disabled` (the command can’t run now) and `badParams`. Requests about controls use `ui`.

```json
{"id": 8, "ok": false, "code": "ui", "error": "no element named “Nothing here”"}
```

## Commands

Every action on your photos is a named command, the same one the menu and the shortcuts run. This is the most exact way in: the parameters are plain values, and nothing depends on what is on screen. Ask `engine.commands` what exists; often used are `develop.set`, `develop.adjust` and `develop.get` for the sliders, `library.select` for the selection, and `edit.undo`.

#### `engine.commands`

Every command the app has, with its menu path, shortcut, parameters, and whether it can run right now (and why not).

Returns: a list of { id, label, menu, shortcut, params, enabled, disabled_reason? }.

#### `engine.execute`

Run one command by id, exactly as the menu or a shortcut would. Photo edits land on the undo stack like any other.

Parameters:

- `command`: The command id, such as develop.set or library.select.
- `params` (optional): The command's parameters (see engine.commands).

```json
{"method": "engine.execute", "params": {
  "command": "develop.set",
  "params": {"control": "light.exposure", "value": 0.8}
}}
```

#### `host.state`

A snapshot of the library: the active photo, how many are selected, how many are shown, and the names of the next undo and redo steps.

Returns: { active, selectionCount, visibleCount, undo, redo, unsaved, … }.

## Controls

Anything you can click in a window can be found by name and used. This is for what commands don’t cover: choosing a photo, an album or a tab, flipping a switch, filling in a field, or choosing a menu item. A sheet is its own window: pass `"window": "sheet"` while one is open.

#### `ui.tree`

The controls a person can operate in a window, each with its name, value, whether it is enabled, and where it is. Texts and groups are left out unless you ask.

Parameters:

- `window` (optional): main (default), settings, second, sheet (the one open over a window), or part of a window title.
- `filter` (optional): Keep controls whose name or role contains this text.
- `all` (optional): true lists every element, including texts, groups and the develop sliders.
- `nested` (optional): true returns the tree as it is, not a flat list.

Returns: { window, count, unnamed, controls: [{ path, label, role, value?, enabled, frame }] }. frame is in points from the window's top left; unnamed counts the controls that have no name.

```json
{"method": "ui.tree", "params": {"filter": "stars"}}

→ {"count": 5, "unnamed": 0, "window": "All Photos", "controls": [
    {"label": "1 stars", "role": "AXButton", "path": "0.0.2.0", "enabled": true, "frame": [272, 969, 20, 24]},
    {"label": "2 stars", "role": "AXButton", "path": "0.0.2.1", "enabled": true, "frame": [293, 969, 20, 24]},
    …
  ]}
```

#### `ui.press`

Press a button, flip a toggle, choose a row in the sidebar, choose a photo, pick a tab. Name the control by label, or by the path from ui.tree.

Parameters:

- `label` (optional): The control's name. An exact match wins, then one that contains it.
- `path` (optional): The path from ui.tree (also accepted as id). It is good while the screen holds still.
- `role` (optional): Narrow a label to one kind of control, such as AXButton.
- `index` (optional): Which one, counting from 0, when several controls share the label.
- `action` (optional): confirm opens a photo (a double click), showMenu, increment and decrement step a slider. Default: a press.
- `window` (optional): As for ui.tree.

Returns: the control that was pressed, or an error that says why not (disabled, not found, no such action).

#### `ui.setValue`

Set a slider to a number, or replace the text in a field, typed the way the keyboard would.

Parameters:

- `label | path`: The control, as for ui.press.
- `value`: A number for a slider, text for a field.

#### `ui.text`

Type where the keyboard focus is. Press a text field first (ui.press on it focuses it).

Parameters:

- `text`: What to type.

#### `ui.menuItem`

Choose an item in the menu bar, as the menu would, if it is enabled.

Parameters:

- `path`: The titles leading to it, such as ["View", "Show Sidebar"].

## Screens and state

Move the app around, and check what it shows.

#### `ui.set`

Go to a screen, open a sheet or a photo, or change a view option. Pass only what should change.

Parameters:

- `screen` (optional): grid, photo, edit, crop, masking, remove, redEye, presets, info, commands, people, map.
- `photo` (optional): A photo id to show.
- `sheet` (optional): albums, filter, settings, export, sync, history, versions, keywords, about, or none.
- `span` (optional): all, months or years for the grid.
- `windowSize` (optional): [width, height] in points.

#### `ui.inspect`

What the app is showing now: the screen, the photo, background activity, and timings of the last render.

Returns: { screen, photo, activity, perf, … }.

#### `ui.key`

A keyboard shortcut, as the app takes it right now. Says whether it meant anything.

Parameters:

- `key`: How a shortcut is written: 3, Shift+X, Cmd+A, Right.

Returns: { handled, key }.

#### `ui.screenshot`

The window as a PNG, as the screen draws it, toolbar and sidebar included.

Parameters:

- `path`: Where to save it.
- `window` (optional): As for ui.tree.

Returns: { path, width, height }.

## AI assistants (MCP)

An assistant that speaks the Model Context Protocol can use LightBites through `lightcraft-cli`, the command-line tool of the open-source LightCraft engine (https://github.com/elrumo/lightcraft). It connects to the port you opened and turns its requests into tools.

Claude Code:

```sh
claude mcp add lightbites -- lightcraft-cli mcp --connect 127.0.0.1:18731
```

Any MCP client:

```json
{
  "mcpServers": {
    "lightbites": {
      "command": "lightcraft-cli",
      "args": ["mcp", "--connect", "127.0.0.1:18731"]
    }
  }
}
```

The assistant gets tools to take a screenshot, list the controls on screen (`list_widgets`), press one (`click`, by id or by `label`), set a slider or a field (`set_value`), choose a menu bar item (`menu_item`), press keys, type text, move between screens (`set_ui`), and run any command (`run_command`, `get_develop`, `set_develop`, `query_photos`, `export` and more). Start LightBites first; the connection is made on the first request, and again if the app restarts.

## Scripts

To run a fixed list of requests without a connection, put them in a file, one per line, and point the app at it. `{"sleep": 500}` waits half a second.

```jsonl
{"method": "ui.set", "params": {"screen": "grid"}}
{"method": "ui.press", "params": {"label": "Portfolio"}}
{"sleep": 500}
{"method": "ui.press", "params": {"label": "3 stars"}}
{"method": "ui.screenshot", "params": {"path": "portfolio.png"}}
```

```sh
LIGHTCRAFT_DEMO=1 LIGHTCRAFT_SCRIPT=/tmp/tour.jsonl \
  /Applications/LightBites.app/Contents/MacOS/LightBites
# replies are appended to /tmp/tour.out, one line per request
```

## Recipes

### Rate the selected photo

The rating bar is a row of buttons named “1 stars” to “5 stars”.

```json
{"method": "ui.press", "params": {"label": "4 stars"}}
```

### Open a photo

Photos are controls named by title. Pressing chooses one; confirm opens it.

```json
{"method": "ui.press", "params": {"label": "Lagoon", "action": "confirm"}}
```

### Go to an album

The sidebar's rows are named by album.

```json
{"method": "ui.press", "params": {"label": "Portfolio"}}
```

### Set exposure exactly

A develop slider is a control with an id. Commands take its real value.

```json
{"method": "engine.execute", "params": {"command": "develop.set",
 "params": {"control": "light.exposure", "value": 0.8}}}
```

### Nudge a slider the way the arrow keys do

The develop sliders listen for increment and decrement. List them with ui.tree and all.

```json
{"method": "ui.press", "params": {"label": "Exposure", "action": "increment"}}
```

### Take a picture of the window

Useful after a change, to check what the app shows.

```json
{"method": "ui.screenshot", "params": {"path": "/tmp/lightbites.png"}}
```

## Safety

- Off unless you ask: nothing listens until `LIGHTCRAFT_CONTROL_PORT` is set.
- This computer only: the port is open on `127.0.0.1`, not on your network.
- Web pages can’t use it: a request that doesn’t start with a JSON object closes the connection before anything runs.
- Any program you run can use it while the port is open: open it only while you need it.
- No synthetic input: it never moves your pointer or presses keys at the system level.

## Limits

- Mac only. The commands work on every platform; the controls layer is built for the Mac app.
- Pop-up menus: the items of a menu button, once it is open, aren’t listed. The menu bar itself works with `ui.menuItem`.
- Develop sliders appear with `all: true`, as plain elements, and show no current value. Read values with `develop.get` and set them with `develop.set`.
- A few icon-only buttons still carry the name of their symbol until they get a proper label.
- A `path` from `ui.tree` is good while the screen holds still. Prefer `label` in anything you keep.
