Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

132 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

chromatik-mcp

A drop-in LX/Chromatik package that lets an agent read, explain, compose into, and debug a running Chromatik show over MCP.

Status: the v1 tool surface is working end-to-end — discovery, parameters, tempo, modulation wiring, channels/patterns/effect chains, mixer performance controls (crossfader, cue/aux), MIDI mapping, palette (read-write), snapshots, model views, render previews, OSC addressing, and a generated semantic catalog of what each component does. See docs/build-plan.md for the roadmap and docs/tool-conventions.md for the tool-surface conventions.

Docs: oveddan.github.io/chromatik-mcp — for AI agents, the full docs are available as plain markdown at llms-full.txt (llms.txt index).

Quick start

# download the latest release (or: cd package && mvn install -Pinstall)
curl -L --create-dirs -o ~/Chromatik/Packages/chromatik-mcp.jar \
  https://github.com/oveddan/chromatik-mcp/releases/latest/download/chromatik-mcp.jar
# then: enable Chromatik-MCP in Chromatik Preferences → Plugins, restart, and connect:
claude mcp add --transport http chromatik "http://127.0.0.1:$(jq -r .port ~/.chromatik-mcp/status.json)/mcp"

The plugin publishes its endpoint in ~/.chromatik-mcp/status.json ({pid, port, host, url, projectPath, lxVersion, serverVersion, buildTime, connected, lastActivityAt}); the endpoint works with any streamable-HTTP MCP client. Enabling the Chromatik-MCP plugin is the only checkbox — the UI status section (left pane, GLOBAL tab) enables itself. By default the server binds an ephemeral port on 127.0.0.1; a fixed port or non-loopback bind (remote clients) can be configured in ~/.chromatik-mcp/config.json ({"port": 7770, "host": "0.0.0.0"}) — there is no authentication layer, so a non-loopback bind gives anyone on the network full control of the show. The repo also ships a project-scope .mcp.json for Claude Code (pin the port to 3232 per docs/install.md and it just works). Full walkthrough and troubleshooting: docs/install.md. Concrete agent flows — building show structure, chaining effects, macro mapping, multi-agent patterns: docs/usage-examples.md.

Capabilities

Everything is addressed by canonical LX path (e.g. /lx/mixer/channel/1/fader), as returned by the discovery tools. Mutations are undoable in Chromatik with Cmd-Z unless noted.

Discover

tool what it returns
get_status server identity + liveness: serverVersion, buildTime (detect a stale process after installing a new jar), uptime, connection state
get_project_info LX version, project file, channel count, OSC engine state, the output object (/lx/output/enabled — the "Live" toggle pixels won't reach fixtures without — plus brightness and gamma), and engine globals (speed playback-rate multiplier, framesPerSecond)
get_tempo the engine clock: bpm, clock source (internal / MIDI-synced / OSC-driven), beats-per-bar, launch quantization (why fire_trigger sometimes answers pending: true), tap/nudge paths, live beat position, and a beat-pulse path usable as a wire_trigger source
list_channels the mixer: channels (with patternMode playlist/blend and per-pattern contributing), master, and every effect chain — including pattern-hosted effects. Each channel carries a controls block (crossfade group, blend mode, auto-mute, cue/aux, pattern auto-cycle + transition settings) and the top-level mixer object holds the crossfader (0 = full A, 1 = full B) and cue/aux preview buses — all with settable paths. Defaults to a compact detail: summary shape sized for surveying a whole project; pass detail: full for the complete payload
list_parameters every parameter on a component plus its child components (a pattern's effects, the palette's swatches) — walk the tree instead of guessing paths
list_available_patterns / _effects / _modulators instantiable classes from the LX registry (modulators carry global/device flags — where they may be added)
list_modulations one modulation engine's live modulators and wirings — global side panel by default, or a device's own chain via scope. Defaults to a compact detail: summary (wiring graph only); pass detail: full for OSC addresses, running state, and per-modulation range/polarity
get_parameter one parameter: value, type, range, options, units, and its OSC address. Parameters with live modulations report the effective value plus the knob's baseValue and modulated: true
get_palette the global color system: active swatch colors (mode + effective color), saved swatches with recallPath (fire to recall, honors the transition time), auto-cycle state. Mutations: see the palette & snapshots section
list_snapshots saved snapshots (whole-look captures) plus the snapshot engine's recall-scoping and transition settings, all with settable paths
get_views model views (see below)
get_frame a PNG render of the current output (include_image/grid/width control token cost) — visual feedback without screen access
get_component_doc what a pattern/effect/modulator does — generated behavior docs from the semantic catalog, with a bytecode-hash stale flag so the answer is honest when code has changed. list_available_* entries carry documented flags

Read & set parameters

set_parameter {path, value} dispatches on the parameter's runtime type (number / integer / boolean / string) and rejects what can't be set sanely: aggregate parameters (set a color's .../hue, .../saturation, .../brightness components instead), computed read-only parameters, out-of-range enum indices (LX would silently wrap), and momentary triggers (see fire_trigger). Discrete/selector parameters also accept an option name string{"value": "Cylinder"} maps a device to a view by label, no index lookup needed. The response echoes the base value, so set-then-verify works even while modulation rides on top.

class arguments everywhere (add_pattern, add_effect, add_modulator, add_channel, get_component_doc) accept either the full class name or the short name the list_available_* tools return; an ambiguous short name errors listing the candidates.

Build structure: channels, patterns, effect chains

tool what it does
add_channel {class?} / remove_channel {path} mixer channels, optionally seeded with a first pattern (LX moves UI focus to a new channel)
add_pattern {containerPath, class, index?} / remove_pattern / move_pattern {path, index} manage a channel's pattern list
activate_pattern {path} switch the active pattern (PLAYLIST mode; BLEND-mode channels layer patterns via their enabled params instead)
add_effect {containerPath, class} / remove_effect / move_effect {path, index} effect chains — run serially in list order — on channels, the master bus, or an individual pattern

Structural paths are 1-based and reindex on remove/insert — re-list rather than reusing cached paths.

Map macro knobs (and any modulation)

tool what it does
add_modulator {class, scope?} add e.g. a MacroKnobs bank or a VariableLFO — to the global side panel, or inside a pattern/effect's own chain via scope. Response lists every parameter with its path and OSC address
remove_modulator {path} delete a modulator; wirings it sources are removed with it (one undoable step)
wire_modulator {sourcePath, targetPath, scope?, range?} undoable continuous mapping, e.g. macro1 → fader or LFO → twist. Pass range (-1..1) to give the wiring depth immediately — a wiring without range is inert. Engine inferred from the source; adjust later via the returned rangePath/polarityPath
wire_trigger {sourcePath, targetPath, scope?} boolean pulse wiring (e.g. a MacroTriggers macro → a toggle)
remove_modulation {path} unwire either kind by the path the wire call returned
fire_trigger {path} pulse a momentary trigger (not undoable — it's an action, and the value auto-resets). Under launch quantization the response says pending: true; don't re-fire

The typical macro-mapping flow:

add_modulator {class: heronarts.lx.modulator.MacroKnobs}      → knob bank + 8 OSC addresses
wire_modulator {sourcePath: <bank>/macro1, targetPath: <fader path>} → undoable mapping
set_parameter {path: <bank>/macro1, value: 0.75}             → turn the knob

MIDI mapping

tool what it does
list_midi_devices discovered MIDI input/output ports — each input's three independent routing flags (channelEnabled forwards notes/CCs to channel and modulator devices, controlEnabled feeds the mapping layer below, syncEnabled drives the engine tempo when get_tempo reports clockSource: MIDI) plus connected state. Ports are addressed by 0-based index (no canonical path) — indices shift as devices connect/disconnect, so re-list before reusing one
list_midi_mappings parameter mappings driven by incoming MIDI (type note/cc, channel, number → targetPath). Only inputs with controlEnabled actually apply them. Addressed by 0-based index — indices shift when a mapping is removed, so re-list before reusing one
list_midi_surfaces instantiated control surfaces (e.g. an APC40, a MidiFighterTwister) — two-way hardware LX drives with a dedicated protocol, distinct from the ad-hoc mappings above. Addressed by 0-based index
add_midi_mapping {type, number, channel, targetPath} map an incoming note-on or CC to a parameter by its canonical path; fires on channel+pitch/cc identity, not a specific velocity/value. Most numeric/bounded/toggle/discrete parameters can be targeted — aggregate parameters (color, MIDI filter) are rejected, map their component paths instead. Undoable
remove_midi_mapping {index} delete a mapping by its list_midi_mappings index; remaining mappings reindex afterward — re-list before reusing one. Undoable
set_midi_input {index, ...flags} set one or more of an input's routing flags by its list_midi_devices index; unset flags are left unchanged. Not undoable — LX has no undo command for these flags
set_midi_surface_enabled {index, enabled} enable/disable a control surface by its list_midi_surfaces index. Not undoable — LX has no undo command for surface enablement

Palette & snapshots: colors and whole looks

The palette is read-write: beyond setting an individual color's .../hue / .../saturation / .../brightness via set_parameter, swatches themselves can be managed. Snapshots capture the entire current state — mixer, patterns, effects, modulation — as a recallable look.

tool what it does
save_swatch capture the active swatch's current colors as a new saved swatch (returns its path)
set_swatch {path} apply a saved swatch onto the active colors — same effect as firing its recallPath (including the transition fade), but undoable
remove_swatch {path} / move_swatch {path, index} manage the saved-swatch list
add_color / remove_color add/remove a color slot on a swatch (active swatch by default); a swatch always keeps at least one color
add_snapshot {label?} capture the current mixer/pattern/effect/modulation state as a snapshot
recall_snapshot {path, immediate?} restore a snapshot — fades over the engine's transition time unless immediate. What a recall touches is governed by the engine's recall-scoping toggles (see list_snapshots). Caution (LX behavior): Cmd-Z after a recall does not restore the previous parameter values
update_snapshot {path} recapture the current state into an existing snapshot
remove_snapshot {path} delete a snapshot

Model views: spatial composition

Views are named subsets of the model ("Cube Interior", "Faces Exterior"), defined by a tag selector; every channel, pattern, and effect has a view parameter that clips its rendering to one — Default inherits from the parent (effect → pattern → channel → whole model). This is how one project paints different geometry with different content — or applies an effect to only part of the model.

tool what it does
get_views every view definition (selector, enabled/priority, normalization/orientation, live match counts), which devices currently use each view, and the model's tag vocabulary selectors compose from
add_view {label, selector, normalization?, orientation?} create a view; the response's numGroups/numFixtures immediately show what the selector matched (a warning flags zero matches)
remove_view {path} delete a view. Caution (LX behavior, undo does not fix it): devices mapped to the removed view silently reassign to whatever view takes over that index — remap them to Default first

Selectors are a small CSS-like language over model tags — space for descendant, , union, & intersect, ; separate groups, * group-by, tag[n-m] index ranges (full grammar in the get_views description). Map a device with set_parameter on its view path using the view's label:

add_view {label: "Front+Back", selector: "cubeFrontExterior ; cubeBackExterior", orientation: "group"}
set_parameter {path: /lx/mixer/channel/1/pattern/1/view, value: "Front+Back"}

OSC

Parameter payloads carry the address an OSC controller must send to. For most parameters it equals the canonical path, but modulator knobs answer at label-based addresses (/lx/modulation/Knobs/macro1, not .../modulator/1/macro1) — renaming a modulator moves its OSC address. Details and hazards: docs/osc-addressing.md. Ports are in get_project_info (defaults: 3030 receive / 4040 transmit).

Architecture

The jar embeds an HTTP MCP server (official Java MCP SDK, streamable-HTTP on embedded Tomcat) inside the LX runtime as an LXPlugin. Any MCP-speaking client — Claude Code, Claude Desktop, Cursor, Codex, custom orchestrators — connects to it directly and calls tools that mutate LX state in-process. No separate Node server, no .lxp file editing, no file watcher. Mutations route through LXCommand, so every change gets undo for free (exceptions — swatch recall, trigger fires, snapshot recall (an LX quirk: the undo entry captures post-recall values) — are called out in their tool descriptions), and are serialized onto the LX engine thread. The filesystem touchpoints are ~/.chromatik-mcp/status.json (written on startup for endpoint discovery) and the optional ~/.chromatik-mcp/config.json (fixed port / bind host). Default bind is 127.0.0.1 only; there is no authentication layer, so non-loopback binds are at your own risk (a startup warning says as much).

tool handler  ──> domain primitive  ──> LXCommand.perform(...)   (mutation with undo)
(MCP-shaped)     (intent, narrow)   ──> direct lx.engine.* edit  (mutation without undo)
                                    ──> read lx.engine.*         (read-only)

License

MIT. Note the LX framework this plugin targets is separately licensed (free for non-commercial use — see lx.studio/license); this license covers only the chromatik-mcp code.

About

A drop-in LX/Chromatik package for AI-driven show composition over MCP.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages