Lutra MCP
by withaspoon
README.md
# Lutra
**In-browser color-grading LUT creator.** Drop screenshots, camera RAW files or
a video clip, grade them with real colorist tools, export `.cube` / `.3dl` /
Unreal / Unity / ReShade / HALD LUTs. No installs, no accounts — everything
runs locally on your GPU via WebGL2. Images never leave your machine.
Live at **<https://lutra.gamebench.dev>**
## Why
Making a color LUT usually means installing gigabytes of video software. Lutra
is a ~70 KB (gzipped) web app built around one architectural rule: **every
operation is a pure per-pixel color transform**, so the entire grade bakes into
a 3D LUT *exactly*. Spatial effects (bloom, halation, grain, vignette) exist
only as a clearly-labelled preview layer and export as an *engine recipe*
rather than pixels, because they can't live in a LUT — what you see in the
grade is what the LUT does.
## Features
**Grading**
- **Contact-sheet grading** — load many shots across lighting conditions; every
thumbnail follows the grade live, because a LUT is global and has to hold up
everywhere.
- **Per-shot development** — normalize each source by ±4 EV before judging the
shared look. Developed originals, scopes, color picking, and Auto match all
see the same input; the offset stays out of the exported LUT by design.
- **Colorist toolset** — white balance (+ gray eyedropper), exposure, contrast
with pivot, highlights/shadows/whites/blacks, lift/gamma/gain/offset wheels,
master and per-channel R/G/B curves, 8-band HSL mixer (Oklab hues),
continuous Sat-vs-Sat and Luma-vs-Sat curves, saturation/vibrance with
lightness-keyed shadow/highlight control, split toning, output levels, and a
master look-strength blend.
- **Creative LUT layer** — drop in any `.cube` you already own and keep grading
on top of it with a strength slider. It stacks in right after the tone
curves, so everything below trims the look rather than feeding it — and both
still bake into one exported LUT.
- **Film response, honestly** — bloom, halation, grain and vignette preview as
a layer the UI labels *not in the LUT* in as many words. They can't bake
(bloom reads neighbouring pixels; grain and vignette depend on position in
frame), so they're hidden from thumbnails, comparisons and scopes, and export
as an **engine recipe** naming the Unreal/Unity/ReShade settings that
reproduce them.
- **Compare** — before/after (hold Space), A/B wipe, per-shot thumbnails.
Undo/redo and keyboard shortcuts throughout.
**Input**
- **Camera RAW** — `.ARW`, `.CR2`, `.CR3`, `.NEF`, `.DNG`, `.RAF`, `.ORF`,
`.RW2` and the rest via LibRaw compiled to WebAssembly. The camera's own
white balance and color matrix are applied, then the frame enters the
pipeline as **scene-linear light** at exactly the point EXR and PQ AVIF do —
so a photo and a game capture grade identically. Exposure is a fixed,
documented headroom constant, not per-image auto-brightness, so a night shot
and a snow field don't develop to different brightnesses. The 1.5 MB decoder
is behind a dynamic `import()` and never weighs on anyone else's load.
- **HDR & high bit depth** — OpenEXR (half/float, NONE/RLE/ZIP), Radiance
`.hdr`, and 10/12-bit AVIF (PQ, HLG, high-bit SDR) at full precision via
zero-dependency decoders + WebCodecs. HDR develops to display sRGB with the
same ACES fit the PQ shaper export assumes, so preview and in-engine result
agree. Browsers without WebCodecs `ImageDecoder` (Safari, Firefox) get
pre-developed SDR companions derived from the *encoded* PQ data, so pixels
match — and a clear warning when a user's HDR AVIF must be browser-tonemapped.
- **Live video grading** — MP4/MOV/WebM clips sit in the same filmstrip as
stills. Select one, play, scrub or step frames in the normal viewport, and
keep turning every grading knob while it runs. Presented frames feed the
shared WebGL grade directly from the browser/OS decoder; hardware decode is
used when that codec/browser/device combination supports it. Original clip
bytes and the playhead persist locally in browser storage and restore into
the filmstrip after a restart; they are never uploaded. Copies are chunked
and cancellable, quota is checked first, and each clip is labelled `LOCAL`
or `SESSION` so storage failure is visible rather than destructive.
**Matching & QA**
- **Auto match** — drop a reference image and a solver fits the broad-stroke
knobs so the whole contact sheet's Oklab statistics match it. The result is
ordinary knob positions: blend with a strength slider, refine by hand, undo
in one step.
- **`.cube` import** — the same solver fits the knob stack to any 3D `.cube`
(typically within ΔE ≈ 0.1), turning an opaque LUT back into an editable grade.
- **LUT Lab** — open any 3D `.cube` as float data, inspect its declared input
domain, output excursions and folded/singular cells, resize it, blend it from
0–200%, compose it before or after a second LUT, then download, stack, or
reverse-fit the result. Custom domains and values outside 0–1 survive the
round trip instead of being silently clipped.
- **Color locks** — pin colors that must survive the grade (UI colors, team
reds, skin tones). Live ΔE badges warn when the grade moves them, and Auto
match treats them as constraints.
- **Harmonize assets** — mask a palette to keep and an asset to adjust;
*Harmonize* solves only the HSL mixer so the asset matches under your look.
*Export texture fix* instead solves a standalone B→A transform and downloads
it as `.cube` + HALD for batch-applying to textures.
- **Scopes** — RGB histogram, parade, waveform, vectorscope with skin-tone
line, clipping zebras, exact in→out pixel probe, and color-vision-deficiency
simulation for gameplay-readability checks.
- **Honest baked preview** — toggle between the float pipeline and the actual
quantized LUT (16³/32³/65³, trilinear or tetrahedral) to see banding *before*
export instead of in-engine.
**Output & persistence**
- **Source-resolution stills** — export the active shot as a PNG at its original
dimensions, including per-shot development and either the float grade or the
currently selected quantized-LUT preview. A separate complete-look export
renders tuned bloom, halation, grain and vignette into the pixels.
- **Exports** — `.cube` (16/32/65), `.3dl`, Unreal 256×16 strip, Unity LDR
1024×32 strip, ReShade `lut.png`, HALD CLUT 512×512, and a **PQ shaper
`.cube`** for engines grading in scene-referred HDR before the tonemapper.
Multiple formats bundle into one zip. See [docs/exports.md](docs/exports.md)
for per-engine setup and the PQ shaper's caveats.
- **Share links** — the Share button copies a URL with the whole look gzipped
into the fragment. No server, nothing uploaded.
- **Sessions survive reload** — shots and mask scribbles persist to OPFS, the
grade to localStorage. Close the tab, come back, keep grading.
- **Works on a phone** — below 860px the image pins to the top while controls
scroll under it, hit targets grow, and two fingers pinch-zoom the viewport.
## Connect AI
One button in the toolbar. Lutra exposes its tools over the Model Context
Protocol, so Claude, ChatGPT or any MCP client can drive the app you already
have open. The point isn't text-to-LUT: the assistant can **look**. It renders
a shot, reads the scopes, nudges one named axis in plain English, renders
again, checks your locked colors — the loop a blind generator can't close.
Global grade changes remain ordinary undo steps; shot development and film
response stay separate so an undo cannot silently cross pipeline boundaries.
Two transports, picked for you. **WebMCP**, where the browser implements it
(Edge ships it; Chrome runs an origin trial from 149), hands the tools to the
in-browser agent with no pairing at all. Everywhere else you get a connector
link relayed through a Durable Object that stores nothing:
| Client | What you get |
| --- | --- |
| Claude Code | `claude mcp add --transport http lutra <url>`, to run in a terminal |
| Codex | an `[mcp_servers.lutra]` block for `~/.codex/config.toml` |
| Claude app | the bare URL, for Settings → Connectors → Add custom connector |
Tools: `get_look` · `list_shots` · `render_shot` (returns the actual image) ·
`get_scopes` (original or graded) · `nudge` · `set_look` ·
`set_shot_development` · `get_film_response` · `set_film_response` ·
`suggest_looks` · `check_locks` · `undo` · `get_share_link`. The grade schema
includes the wheels and expert curves, plus a compact toe/mid-gamma/shoulder
tone shape. Declared once in `shared/mcpTools.ts`, implemented in
`src/mcp/tools.ts`; `worker/mcp.ts` is the protocol as a pure function.
**Security.** There is no API key and no OAuth — the code in the URL *is* the
credential, so treat it like a password and use **New link** to revoke one
you've pasted somewhere public. It works only while the Lutra tab is open.
Lutra holds no API key and calls no model: the intelligence is billed to the
subscription you already pay for, which is why this costs nothing to run and
needs no account. A `localhost` link is unreachable from the Claude app, which
connects from Anthropic's servers; Claude Code and Codex run locally, so they
can use one.
## Develop
```sh
npm install
npm run dev # http://localhost:5173
npm run build # type-check + production build → dist/
npm test # vitest
```
One runtime dependency (`libraw-wasm`, dynamically imported and code-split so
it only loads for RAW files); dev deps are Vite + TypeScript only.
## Deploy
Production is served as static assets from a Cloudflare Worker — no server
code, just `dist/`:
```sh
npm run deploy # build + wrangler deploy
npm run deploy:dry # validate without deploying
```
The custom domain is a route in `wrangler.jsonc`, so DNS is kept in sync by the
deploy. The app is fully static with relative asset paths, so it also hosts on
Netlify, GitHub Pages or any file server — grading, export and every feature
above work with no backend at all. The only server-side piece is the MCP
pairing relay (`worker/`), and WebMCP doesn't even need that.
## Docs
- [docs/exports.md](docs/exports.md) — using the exported LUTs in each engine,
and what the PQ shaper does and doesn't guarantee.
- [docs/architecture.md](docs/architecture.md) — the single-`grade()`-function
design, the CPU mirror, the solver, and roadmap ideas.
- [docs/professional-parity.md](docs/professional-parity.md) — competitor-backed
capability gaps, browser feasibility decisions, and acceptance gates.
- [docs/cloud-architecture.md](docs/cloud-architecture.md) — the local-first
server boundary and the fail-closed Cloudflare budget capped at $100/year.
## License
**Proprietary — all rights reserved.** See [LICENSE](LICENSE).
The app is free to use at <https://lutra.gamebench.dev>; the source is not
licensed for reuse. This may change: releasing under an open-source licence
later is easy, and un-releasing is impossible, so the source stays closed until
that decision is made deliberately.
A built deployment still redistributes LibRaw (LGPL-2.1 or CDDL-1.0, via
`libraw-wasm`), and the bundled samples are CC0 from
[RawDB](https://rawdb.dnglab.org/). Those obligations follow the *deployed app*
rather than the source, so they apply regardless of this repository's
visibility — see [NOTICE](NOTICE), including which RawDB licences are safe to
bundle if you add a sample.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessSyncing