8.0 KiB
LightCraft MCP server
lightcraft-cli mcp exposes LightCraft to AI agents through the
Model Context Protocol: newline-delimited JSON-RPC 2.0 over
stdio (protocol revision 2025-06-18; 2025-03-26 and 2024-11-05 clients are accepted). The
server lives in crates/mcp (lightcraft-mcp, layer L5) and is hand-written: no async runtime,
no C dependencies.
It runs in one of two modes:
| Mode | Command | What it drives |
|---|---|---|
| Headless (default) | lightcraft-cli mcp [--demo] [FILES/FOLDERS…] |
An in-process engine Session. Develop, render and export without a window. |
| Connect | lightcraft-cli mcp --connect [127.0.0.1:7980] |
A running desktop app started with lightcraft --control 7980, through its loopback JSON-lines control channel (control-protocol.md). Adds the UI tools (screenshot, clicks, keys, pointer gestures). |
Options: --library DIR opens (or creates) a persistent LightCraft library — the same crash-safe
format the desktop app uses (~/Pictures/LightCraft Library by default there) — so ratings, edits and albums
survive between sessions (with --demo, a new library is seeded with the demo photos);
--demo starts the headless session with the procedurally generated demo library;
--compact lists only the helper tools (see below). In connect mode the server starts even when
the app is not running yet and connects on the first call (and reconnects if the app restarts).
Logs go to stderr; stdout carries only protocol messages.
Wiring it into a client
Build once: cargo build --release -p lightcraft-cli (binary: target/release/lightcraft-cli).
Claude Code
# headless, with a folder of photos imported at start
claude mcp add lightcraft -- /path/to/lightcraft/target/release/lightcraft-cli mcp ~/Pictures/shoot
# or: drive the running desktop app (start it with `lightcraft --control 7980`)
claude mcp add lightcraft-app -- /path/to/lightcraft/target/release/lightcraft-cli mcp --connect 127.0.0.1:7980
Or check a project-scoped .mcp.json into your repo:
{
"mcpServers": {
"lightcraft": {
"command": "/path/to/lightcraft/target/release/lightcraft-cli",
"args": ["mcp", "--connect", "127.0.0.1:7980"]
}
}
}
Other clients (Claude Desktop, Cursor, …)
Every stdio MCP client takes the same shape: a command plus args. For example
claude_desktop_config.json:
{
"mcpServers": {
"lightcraft": {
"command": "/path/to/lightcraft/target/release/lightcraft-cli",
"args": ["mcp", "--demo"]
}
}
}
During development you can also point the client at cargo run --release -p lightcraft-cli -- mcp
(with "cwd" set to the repository), at the cost of a slower start.
Tools
Helpers
| Tool | Does |
|---|---|
list_commands {filter?} |
Every command: id, label, menu, shortcut, parameter doc, enabled now |
run_command {command, params?} |
Run any command by id |
import {paths, album?} |
Import files/folders (folders are scanned recursively); the first new photo becomes active |
query_photos {filter?, sort?, offset?, limit?} |
Photos in the current view (or matching a catalog Filter) |
select_photos {ids, active?, mode?} |
Set the selection / active photo |
list_controls {section?} |
Every develop slider: id (light.exposure…), range, default, current value |
get_develop {id?} |
Full develop-settings JSON |
set_develop {id?, values?, settings?, label?} |
values: {controlId: number}; settings: partial develop JSON deep-merged. Undoable |
apply_preset {preset, amount?, ids?} |
Apply a preset (ids from cmd_presets_list) |
crop {id?, rect?, angle?, reset?} |
Normalized crop rect [x0,y0,x1,y1] and straighten angle |
render_photo {id?, size?, format?, path?} |
Render with current settings → image content (PNG, or JPEG with format: "jpeg"), long edge size (default 1024) |
export {path, id?, longEdge?, quality?} |
Full-quality render to .png / .jpg / .tif / .webp (headless; the desktop app writes PNG) |
Tools taking id make that photo active first; without it they act on the active photo.
Connect mode only
| Tool | Does |
|---|---|
screenshot {maxSize?, format?, path?} |
The app window as an image, after pending renders finish |
inspect_ui |
View, panel, window/image rects, selection, status |
set_ui {state} |
Merge UI state, e.g. {"view": "detail"} |
list_widgets {filter?} / click {widget | x,y, count?} |
Widgets by automation id; real egui clicks |
press_key {key, cmd?, shift?, alt?} / type_text {text} |
Keyboard input (shortcuts) |
pointer_gesture {events} |
Gestures in normalized image coordinates (brush strokes, gradients, crop handles) |
In headless mode these return a tool error explaining how to start the app.
One tool per command
Everything is a command in LightCraft, so tools/list also contains one tool per entry of the
command registry (engine commands, plus the app's UI commands such as view.detail when
connected): the id with . replaced by _ and a cmd_ prefix — photo.rate → cmd_photo_rate,
develop.set → cmd_develop_set, edit.undo → cmd_edit_undo. Arguments are the command's
JSON params (documented in each tool's description and by list_commands). Pass --compact to
leave these out (about 90 headless / 140 connected) when a client struggles with many tools;
run_command still reaches every command.
Resources
resources/list / resources/read serve JSON snapshots:
| URI | Content |
|---|---|
lightcraft://library |
Source, filter, sort, selection, undo/redo labels (library.state) |
lightcraft://photos |
Photos in the current view (catalog.query) |
lightcraft://photo/active |
Everything about the active photo (photo.inspect) |
lightcraft://develop/active |
The active photo's develop settings (develop.get) |
lightcraft://controls |
Every develop control with its current value (develop.controls) |
Example session
→ {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"me","version":"1"}}}
← {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18","capabilities":{"tools":{…},"resources":{…}},"serverInfo":{"name":"lightcraft",…},"instructions":"…"}}
→ {"jsonrpc":"2.0","method":"notifications/initialized"}
→ {"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"set_develop","arguments":{"values":{"light.exposure":0.7}}}}
← {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"{…}"}],"isError":false,"structuredContent":{"ok":true,"controls":[{"id":"light.exposure","value":0.7}]}}}
→ {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"render_photo","arguments":{"size":768}}}
← {"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"image","data":"iVBORw0…","mimeType":"image/png"},{"type":"text","text":"{\"id\":21,\"width\":512,\"height\":768}"}],"isError":false}}
Errors from commands (unknown control, nothing selected, app not reachable) come back as tool
results with isError: true so the model can read and correct them; malformed JSON-RPC gets the
standard error codes (-32700 parse, -32600 invalid request, -32601 method not found, -32602
invalid params, -32002 resource not found).
Other CLI subcommands
lightcraft-cli render in.dng -o out.jpg --set light.exposure=0.5 --set light.contrast=20 --size 2048
lightcraft-cli render in.jpg -o out.png --settings look.json --preset <presetId>
lightcraft-cli commands [--json] # the command registry
lightcraft-cli controls [--json] # develop control ids and ranges
Tests
crates/mcp/tests/e2e.rs— M0.9 acceptance: over the stdio framing, set exposure and render; checks the decoded PNG gets brighter/darker. Runs headless and through the TCP transport (Remote) against a stand-in control server; also import → render → JPEG export of a real file.apps/lightcraft-cli/tests/cli.rs— spawnslightcraft-cli mcpwith real pipes;render;commands.