A drop-in LX/Chromatik package that lets an agent read, explain, compose into, and debug a running Chromatik show over MCP.
Status: the tool surface works end-to-end — discovery, parameters, tempo, modulation wiring, channels/groups/patterns/effect chains, mixer performance controls (crossfader, cue/aux), MIDI mapping and templates, palette (read-write), snapshots, model views, fixtures & output wiring, render previews, OSC addressing, project/model save, arrange-timeline composition authoring (markers, locators, lanes, automation), and a generated semantic catalog of what each component does. See the generated tool reference for the authoritative current inventory, 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).
# 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.
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. This is the highlight reel — the complete generated surface (including model & fixture editing, project/model save, and batched operations) is on the tool reference.
| 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 and 100 wirings per page; pass nextCursor back as cursor for the next page, or 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 |
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.
undo and redo expose Chromatik's shared linear command history one step at a time.
Their responses name the command that moved and report whether another undo or redo is
available. The history is global to the running engine — it may include changes from the
UI or another MCP client, not only the current agent session. If an upstream command fails
while undoing or redoing, LX clears both history stacks and may leave partially changed
state; the MCP error reports the post-failure availability so callers know to stop and
inspect the affected area.
| tool | what it does |
|---|---|
add_channel {class?} / remove_channel {path} / move_channel {path, index} |
add, remove, or reorder mixer channels and group blocks; move destinations are 0-based post-removal indices and preserve group membership |
group_channels {paths} / ungroup_channel {path} / ungroup_channels {path} |
create a group bus over an explicit channel set, pull out one member, or dissolve the group. Group creation is the exception: LX has no explicit-list command, so it is not undoable; both ungroup operations are undoable |
add_pattern {containerPath, class, index?} / remove_pattern / move_pattern {path, index} |
manage a channel's or PatternRack'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/group/ungroup — re-list rather than reusing cached paths. Group members remain flat top-level channel paths; their group field in list_channels identifies membership.
Grouping moves main/aux focus and selection to the new group. Pulling out a focused member
moves focus with it; pulling out the final member leaves an empty group bus, which must be
dissolved separately with ungroup_channels.
| 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} / move_modulator {path, index} |
delete or reorder a modulator in its global/device-local engine; moving uses a 0-based destination index and shifts canonical paths |
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
| 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 |
list_midi_templates |
instantiated per-project MIDI templates (e.g. an Akai MPD218) whose named knob/pad parameters can be discovered at the returned canonical path and used as modulation/trigger sources |
add_midi_template {class} |
add a registered MIDI template by full/simple class, template name, or expected device name (e.g. AkaiMPD218, Akai MPD218, or MPD218); LX selects matching connected I/O automatically. Undoable |
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 |
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 |
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"}
The LX 1.2.2 arrange composition (/lx/timeline/composition; most tools also accept a grid clip /lx/mixer/channel/N/clip/M) is fully authorable — 26 tools:
| tool | what it does |
|---|---|
get_composition / get_clip / list_clip_lanes / get_clip_lane |
read the timeline: markers, transport state, lanes, and a paged event window |
set_clip_marker |
move insertMarker (this IS timeline scrubbing), the loop/play markers, or truncate the length — setters clamp silently and echo the cursor read back |
add_locator / list_locators / move_locator / remove_locator / go_locator |
named position markers (1-indexed, re-sorted by position) plus transport jump |
add_clip_lane / remove_clip_lane / move_clip_lane / set_clip_lane_visible |
automation-lane lifecycle: parameter lanes record one parameter, pattern lanes a channel's pattern changes |
add_automation_point / set_automation_point / remove_automation_point |
insert/edit/delete automation points — normalized value, cursor, interpolation curve, shape, with an atCursor guard on edits |
remove_clip_range / collapse_clip_range |
delete every event in a cursor range on one lane, or flatten it to its boundary points |
add_audio_lane / add_notes_lane / add_clip_note / set_clip_note |
an audio backing track (WAV/AIFF) and annotation lanes |
launch_clip / stop_clip / set_composition_arm |
transport and record-arm (transport is not an LXCommand upstream — not undoable) |
Timeline positions are cursor objects — exactly one of {millis}, {beatCount[, beatBasis]}, {bars, beats, sixteenths}, or {at: <origin>, offsetBeats/offsetMillis} — and every mutation echoes the cursor read back from the engine after silent clamping ({millis, beatCount, beatBasis, formatted}): trust the echo, never your request. Lane paths (<clipPath>/lane/<n>) and event indices are positional — re-list after mutations. Worked flow: docs/usage-examples.md.
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).
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 — trigger fires, including firing a swatch's recallPath (set_swatch applies the same swatch undoably); group_channels, which LX has no explicit-list command to invert; and 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)
Java 25 and Maven (the published LX 1.2.2 jars require 25); Node 20 only if you touch the docs site or the agent plugin.
git clone https://github.com/oveddan/chromatik-mcp.git
cd chromatik-mcp
package/scripts/build-gate.sh # compile + the full headless test suite
cd package && mvn install -Pinstall # drop the jar into ~/Chromatik/Packages/Use build-gate.sh rather than raw mvn package — it keeps the full log on disk and
prints a one-line summary, and carries a watchdog for a known macOS CoreMIDI deadlock.
package/scripts/verify-load.sh is the headless plugin-load gate; several docs artifacts
are generated and gated against drift. Full guide — repo layout, testing conventions,
drift gates, catalog regeneration, and the conventions a change has to hold to:
docs/development.md.
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.