# FXCanvas: briefing for AI agents

You are helping someone who runs visuals or lighting with FXCanvas. Read this before your first task. It covers how to drive FXCanvas through its MCP server without damaging a show, and how to write sources and effects the user can load.

Applies to FXCanvas 1.2 and later. The full user guide is at https://docs.fxc.studio.

## Ground rules

- The user's show may be live in front of an audience. Ask before anything visible on the output, and always before a blackout.
- There is no undo over the API. Before you change something, read it and keep the old values, so you can put them back.
- Prefer small steps you can check. After every visible change, call `get_preview_image` and look at it.
- If a tool fails, read the error code. The table near the end of this page says what each one means.

## Connecting

The MCP server is built into FXCanvas. The user turns it on in **Edit → Settings → Remote Control → Enable MCP server**. It needs an activated license.

- Address: `http://127.0.0.1:8790/mcp` (streamable HTTP). The port can differ; the settings page shows it.
- Turning it on also turns on the REST API, which the server uses (port 8080 by default).
- An API key is optional. If the user turned on **Require API Key**, send it as `Authorization: Bearer <key>`.
- Claude Desktop uses the FXCanvas extension instead of the address.
- ChatGPT desktop and Codex read `~/.codex/config.toml`. ChatGPT in a browser cannot reach FXCanvas.

Setup steps for each app: https://docs.fxc.studio/control-and-integration/mcp-server

## First calls

1. `get_status`. Go on only when `reachable` and `authOk` are true. Read `warnings`. If `readOnly` is true, you can look but not change anything; tell the user.
2. `get_live_state`: the current cue, source, palette, effects, tempo, page and globals.
3. `get_config` with no arguments: the setup categories, and how each one saves (`persistence`).
4. `get_preview_image`: a still of the output.

## Changing the setup safely

Every setup change follows four steps:

1. Read the item with its `get_*` tool. Keep the `etag`.
2. Send the change with `dryRun: true`. Put only the fields you change in `body`. Read every warning.
3. Apply it with `ifMatch` set to the `etag` from step 1.
4. Read the item again to confirm.

`PRECONDITION_FAILED` (HTTP 412) means someone else changed the item in between, often the operator. Read it again and decide again.

The `persistence` value in a reply says when the change reaches disk:

- `immediate`: saved now.
- `autosave`: saved a few seconds later.
- `manual`: not saved until someone saves. Video outputs persist only after `save_output_preset_as` or `save_current_output_preset`.

Things that catch agents out:

- Effect ids (`fx-N`) change after a cue recall or a show load. Read the effects chain again before you edit an effect.
- An effect's `etag` changes when an effect earlier in the chain is added, moved or removed.
- `update_composition` (hueShift, saturation, mirrorX, mirrorY) and `set_globals` (hue, saturation, mirrorX, mirrorY) change the same live values.
- A cue records the whole live look. Build the look first, then call `record_cue`.
- DMX universes are 0-based in the API. `displayUniverse` is the number the operator sees.

## Large results

Every text result is capped at 25 KB.

- List tools return one page at a time. `data.page` gives `offset`, `limit`, `total` and `returned`. Ask for the next page with `offset`.
- `browse_content` takes `search`, `kind` and `compact: true` for large folders such as `/sources` and `/presets`.
- `describe_api_operation` can return one `part` (`request`, `responses` or `errors`).
- If a result ends with a truncation note, ask for less.
- `get_preview_image` waits for fades to finish, about 6 seconds by default (`waitSeconds` 3 to 15). If it returns `settling: true`, wait `transitionRemainingSeconds` and call it again.

## Guides and prompts on the server

Read these resources before a larger task:

| Resource | Use it for |
|---|---|
| `fxc://skills/safe-edit-workflow` | Any setup change |
| `fxc://skills/setup-show-from-scratch` | Building a setup from a description |
| `fxc://skills/convert-setup-from-other-software` | Recreating a setup from another lighting or video program |
| `fxc://skills/build-color-palette` | Designing and showing a palette |
| `fxc://skills/patch-pixel-fixtures` | Adding and patching pixel fixtures |
| `fxc://skills/operate-show-live` | Running the show |

`fxc://openapi` is the full REST API description. The `generic` tools (`list_api_operations`, `describe_api_operation`, `call_api_operation`) reach any operation in it by `operationId`.

## What the MCP server cannot do

The operator must do these in the FXCanvas window:

- Import files: fixture types, media, show files.
- Place fixture pixels on the canvas (DMX maps).
- Save or load a whole show or rig.
- Create source presets.
- Install sources or effects. You can write the files (below). The user clicks **Refresh** in the Source Presets or Effect Presets panel to load them.

## Writing a source

A source is a generative visual: a fragment shader plus a small JSON file. FXCanvas also loads ISF files (`.fs` or `.isf`).

### Where it goes

User sources live in the FXCanvas folder in the user's Documents:

- Windows: `%USERPROFILE%\Documents\FXCanvas\Sources\`
- macOS: `~/Documents/FXCanvas/Sources/`

A native source is a folder with three files: `effect.json`, a fragment shader and a vertex shader. An ISF source is one `.fs` or `.isf` file directly in the Sources folder.

After you add or change a source, ask the user to click **Refresh** in the Source Presets panel. The running source keeps its parameter values. A shader that fails to compile does not replace the running one; the error is in the log (see the end of this page).

Give every source a unique name. A user source with the same name as a built-in source replaces it.

### effect.json

```json
{
  "name": "Drift",
  "version": "1.0",
  "author": "Your Name",
  "description": "A smooth palette gradient that drifts across the canvas.",
  "category": "Ambient",
  "colorMode": "palette",
  "shaderContract": 2,
  "shaders": { "vertex": "rectangle.vert", "fragment": "drift.frag" },
  "sourceGlobals": { "baseSpeed": 0.3, "direction": 0.0 },
  "parameters": [
    { "id": "spread", "type": "float", "label": "Spread",
      "description": "How many palette cycles fit across the canvas",
      "default": 1.0, "min": 0.25, "max": 4.0 }
  ]
}
```

- `category`: the built-in sources use Ambient, BPM, Geometric, Pattern and 3D.
- `colorMode`: use `"palette"`, so the source follows the user's palette.
- `shaderContract`: use `2`.
- `sourceGlobals.baseSpeed` adds the Speed control (0 to 10, default 1). Set `baseSpeed` so that Speed 1 looks right.
- `sourceGlobals.direction` adds the Direction control (0 to 360 degrees). 0 points right, 90 points down.
- Do not add parameters named `speed` or `direction`. They are ignored.
- An Opacity control is always added. Your shader must declare `uniform float opacity` and apply it last.
- For a source that pulses on the beat, add `"type": "bpm"` and `"bpmConfig": { "defaultEveryBeats": 1.0, "defaultEnvelope": "pulse" }`. Envelopes: `sine`, `triangle`, `saw`, `ramp`, `square`, `pulse`, `random`.

### Parameter types

Each parameter's `id` is also the name of its uniform in the shader.

| type | fields | shader uniform |
|---|---|---|
| `float` | `default`, `min`, `max` | `uniform float` |
| `int` | `default`, `min`, `max` | `uniform int` (a `float` uniform breaks it) |
| `bool` | `default` | `uniform bool` |
| `enum` | `default` (an index), `options: ["A", "B"]` | `uniform int` |
| `color` | `default: [r, g, b]`, each 0 to 1 | `uniform vec3` |
| `point2d` | `default`, `min`, `max`, each `[x, y]` | `uniform vec2` |

### The shader

- Write GLSL `#version 330` with `in vec2 vTexCoord;` and `out vec4 fragColor;`.
- With `shaderContract: 2`, FXCanvas inserts palette and beat uniforms and helper functions after `#version`. Do not declare them again.
- Copy `rectangle.vert` from any built-in source folder (in the FXCanvas install, `data/sources/<any source>/`).

Palette helpers:

- `fxcPaletteCyclic(t)`: a palette color for a value that repeats, such as an angle or a phase.
- `fxcPaletteBounded(t)`: a palette color for a value from 0 to 1, such as height or distance.
- `fxcColor(t)`: the color of this pixel, following a `colorMode` enum parameter if you declare one with options `["Space", "Energy", "Cycle", "Beat Step"]`.

Time and beat uniforms you can declare:

- `accumulatedOffset`: time scaled by the Speed control. Animate on this, so Speed 0 freezes the image.
- `time`: wall-clock seconds. Avoid it for motion; it ignores Speed.
- `envEnergy` (0 to 1, the beat envelope), `beatFraction`, `cyclePos`, `cycleIndex`, `phase`, `beat`.
- `resolution` (`vec2`, in pixels).

There is no audio input in the shader. Users make a parameter react to audio in the app (Modulation, Audio).

Its whole fragment shader (`drift.frag`, the same as the built-in Gradient source):

```glsl
#version 330
uniform float accumulatedOffset;
uniform float direction;
uniform float opacity;
uniform float spread;
in vec2 vTexCoord;
out vec4 fragColor;

void main() {
    vec2 uv = vTexCoord;
    float ang = radians(direction);
    vec2 travel = vec2(cos(ang), -sin(ang));   // 90 degrees points down
    float along = dot(uv - 0.5, travel);
    float paletteT = (along - accumulatedOffset * 0.5) * spread;
    vec3 color = fxcPaletteCyclic(paletteT);
    fragColor = vec4(color, clamp(opacity, 0.0, 1.0));
}
```

Output straight (not premultiplied) alpha: `vec4(color, coverage * opacity)`. Alpha means coverage, where the shape is. Never set alpha from brightness.

### What makes a source good

The output often drives LED fixtures, so think like a lighting designer:

- A dark pixel is a fixture that is off. Decide coverage on purpose. Washes matter.
- Each control does one job. A shape control must not change the tempo.
- Cut the ends of a range where the image degenerates (one flat color, one giant blob, a white wash). Set defaults to a good look.
- Blend between palette colors with a linear `mix`, never `smoothstep`, or the gradient bands.
- Do not wrap a palette position with a hard `fract()`; use `fxcPaletteCyclic` for repeating values.
- Do not color with the same value that builds the shape, or color and shape move as one.
- Tone-map additive glow so colors keep their hue instead of clipping to white.
- For noise, normalize fractal sums and use a lacunarity like 2.02 rather than 2.0.

### ISF files

FXCanvas loads ISF sources (no image inputs) and ISF effects (with an image input). It supports Float, Bool, Long, Color and Point2D inputs; event inputs become triggers. It maps ISF colors to the user's palette, and each ISF source has a **Use Palette** toggle. Audio and image inputs on a source are skipped. Details: https://docs.fxc.studio/visuals/sources

## Writing an effect

An effect processes the image of the source. User effects live in `Documents/FXCanvas/Effects/`, in the same layout as sources: a folder with `effect.json`, a fragment shader and a vertex shader, or an ISF file with an image input.

```json
{
  "name": "Mirror",
  "type": "mirror",
  "version": "1.0",
  "description": "Mirrors the image across an axis.",
  "category": "Transform",
  "shaders": { "vertex": "effect.vert", "fragment": "mirror.frag" },
  "parameters": [
    { "id": "axis", "type": "enum", "label": "Axis", "default": 0,
      "options": ["Horizontal", "Vertical", "Both"] },
    { "id": "offset", "type": "float", "label": "Offset", "default": 0.5, "min": 0.0, "max": 1.0 }
  ]
}
```

- `type` is a short id. Cues and presets store it, so do not change it later.
- In the shader, read the image with `uniform sampler2D inputTexture;`. `in vec2 texCoordVarying` is in pixels, so `vec2 uv = texCoordVarying / resolution;`.
- Effects can also declare `resolution`, `time`, `beatPhase`, `beatsElapsed` and the palette uniforms.
- Copy `effect.vert` from a built-in effect folder (`data/effects/<any effect>/`).
- After you add or change an effect, ask the user to click **Refresh** in the Effect Presets panel.
- The free tier allows 2 active effects at a time.

## Palettes and presets

- **Palettes**: use the MCP tools. `generate_palette_colors`, then `create_palette` (dry run first), then `activate_palette`. A palette holds 1 to 20 colors as `#RRGGBB`. Built-in palettes are read-only.
- **Source presets** (saved parameter sets) cannot be created through MCP. The user saves them in the Source Presets panel. As a file, a preset is a JSON file in `Documents/FXCanvas/Presets/Sources/`:

```json
{
  "name": "Bullseye",
  "source": { "type": "Pulse",
              "params": { "speed": 1.2, "rings": 3.0, "softness": 0.0, "center": [0.5, 0.5] } }
}
```

`source.type` is the source's name. All presets share one folder, so check that a name is free. Name presets in words a lighting designer would use, never after the source.

To show a preset, use `browse_content` with path `/presets`, then `activate_content`.

## Fixtures

Fixture types are GDTF files. The user imports them in the app (Import GDTF, Import MVR, the GDTF Share library) or builds one with Create Custom Fixture. After that, an agent can find and patch them with `search_fixture_types`, `create_fixtures` and `update_patch`.

## Errors

| Code | Meaning and what to do |
|---|---|
| `FXCANVAS_UNREACHABLE` | FXCanvas is closed or its REST API is off. Ask the user to start FXCanvas and turn the REST API on. |
| `UNAUTHORIZED` (401) | The API key is missing or old. Ask the user to copy the snippet again. |
| `ENDPOINT_NOT_AVAILABLE` | FXCanvas is older than the MCP server. Ask the user to update. |
| `NOT_FXCANVAS` | Another program answered on that port. Check the address. |
| `SHOW_NOT_READY`, `MAIN_THREAD_TIMEOUT`, busy, 503 | FXCanvas is busy, often loading a show. Wait and retry. |
| `NO_SHOW_OPEN` | Open a show first. |
| `NOT_FOUND` for an effect | Effect ids changed. Read the effects chain again. |
| `PRECONDITION_FAILED` (412) | Someone changed the item. Read again and decide again. |
| `PATCH_CONFLICT`, `FOOTPRINT_OVERFLOW` | DMX addresses overlap or run past the universe. Pick other addresses. |
| `FEATURE_GATED` | A license limit was reached, for example the number of effects. |

After a timeout on a write, read the item again before you retry. The write may have happened.

## Other facts

- **License**: the MCP server, the REST API, OSC, the Stream Deck plugin and web ISF import need an activated license. The free tier allows 2 active effects, 2 outputs, a 1280 × 720 canvas, 1 DMX universe and 2 DMX maps.
- **Logs**: `Documents/FXCanvas/logs/`, one file per launch. In the app: Tools → Log Viewer. Shader compile errors appear there.
- **Folders** in `Documents/FXCanvas/`: `Sources`, `Effects`, `Presets`, `Shows`, `Rigs`, `CustomFixtures`, `Extensions`, `logs`.
- **Source switch fade**: `update_settings` with `{"sourceSwitchFadeSeconds": 0.5}` fades source changes (0 to 30 seconds; 0 is instant). It is the same value as the Transition Speed knob in the Live panel's Source Globals. Cues use their own fade time.
