Hypercolor Control
Hypercolor exposes three control surfaces. Choose the right one before acting.
- MCP tools are the canonical agent surface: eighteen typed tools the daemon serves directly, reachable in Claude Code as
mcp__hypercolor__<tool>. Prefer them when they are connected. - Bare
hypercoloris the Rust system CLI for the daemon onlocalhost:9420. It covers everything MCP does not: devices, driver and device control surfaces, layouts, library, audio, permissions, config, and daemon lifecycle. bunx hypercolorinside an effect workspace is the Bun authoring CLI for building, validating, and installing HTML effects.
MCP tools
The daemon mounts its MCP server over Streamable HTTP at /mcp, and only when the
daemon config carries [mcp] enabled = true. Until that flag is set the endpoint 404s
and the CLI is the only route in.
Eighteen tools, five resources, four prompt templates. Selector arguments (query,
name, zone, layer, device, effect_id) all resolve in one fixed order: exact
serialized ID, then exact case-insensitive name, then a unique case-insensitive name
substring. An ambiguous substring fails with the candidate list instead of guessing.
Read-only:
get_status(no arguments): active effect, brightness, connected device count, FPS metrics, audio and screen input status, uptimeget_devices(statusone ofall,connected,disconnected;driver_id;backend_id)list_effects(category,audio_reactive,query,limitdefault 20,offsetdefault 0)list_scenes(enabled_only)get_layout(no arguments): device positions, zones, topologyget_audio_state(no arguments): levels, beat detection, spectrumget_sensor_data(labeloptional): CPU, GPU, memory, raw component temperaturesdiagnose(no arguments): canonical checks, summary counts, captured status snapshotopenrgb_status(no arguments): host installation, SDK probes, permissions, ownership, and native-first coverage
Mutating, not destructive:
set_brightness(brightness0-100, required)set_output_power(stateone ofrunning,paused, required) pauses or resumes all output without discarding the active effect, controls, preset provenance, or scene stateadjust_controls(zoneandlayerrequired, plusvaluesandclear_bindings) atomically patches one live layercreate_scene(namerequired, plusdescription,enabled,mutation_modeone oflive,snapshot)
Destructive. These five are the ones the tool definitions flag destructive, because
each replaces or clears state that no single-field undo restores. Read current state
before calling them:
set_effect(queryrequired, pluscontrolsandtransition) replaces the target zone's whole layer stack with one effect.transitionis an object,{"type": "cut"}, never the bare string.set_color(colorrequired, plusbrightness) sets a solid color across the LED pipeline.coloraccepts CSS names, hex,rgb(),hsl(), or a natural-language description.clear_zone(zoneoptional) clears one non-display zone's layer stack. Omittingzoneclears every non-display zone and quiesces output.activate_scene(namerequired, plustransition_msdefault 1000)set_display_face(devicerequired, pluseffect_id,clear,scopeone ofdefault,scene, andcontrols)
Resources: hypercolor://state, hypercolor://devices, hypercolor://effects,
hypercolor://scenes, hypercolor://audio. Prompt templates: mood_lighting,
troubleshoot, setup_automation, openrgb_setup.
Control values have two wire forms
They are not interchangeable, and sending the wrong one is a hard parse failure.
adjust_controls takes canonical typed control values, adjacently tagged as
{"kind": ..., "value": ...}. Its own arguments wrap them alongside the two
selectors it needs:
{
"zone": "Desk",
"layer": "1f0a7c2e-4b91-4f0e-9a6d-2c8f5b31d7e4",
"values": { "speed": { "kind": "float", "value": 0.5 }, "trails": { "kind": "bool", "value": true } },
"clear_bindings": ["speed"]
}layer resolves by ID, exact name, or unique name substring, but layers that
set_effect, hypercolor effects activate, and POST /effects/{id}/apply
create carry no name at all, and the selector admits unnamed candidates only at
the ID stage. So in practice you read GET /api/v1/scene and pass the layer's
UUID. A name works only for a layer somebody explicitly named.
The REST route PATCH /api/v1/scene/zones/{zone}/layers/{layer}/controls takes
the same control values but a different body. PatchControlsRequest is
deny_unknown_fields over exactly values and clear_bindings; the zone and
layer live in the path, so including them in the body is a hard parse failure,
not an ignored extra:
{
"values": { "speed": { "kind": "float", "value": 0.5 } },
"clear_bindings": ["speed"]
}Kinds are null, bool, int, float, text, secret_ref, ip, mac, duration,
color_rgb, color_rgba, color_linear, gradient, rect, enum, flags, list,
map, and unknown. The deserializer denies unknown fields, so a bare number where a
tagged object belongs fails outright rather than falling back.
set_effect and set_display_face take the other form: raw effect JSON, bare values
admitted against the addressed control's own schema.
{ "query": "Aurora", "controls": { "speed": 0.5, "palette": "Ocean" } }Scene and zone model
There is no single global "active effect". The live scene is a document of zones, and
each zone owns an ordered bottom-to-top layer stack. A zone carries a role (primary,
display, or custom), its own brightness and enabled flag, the device segments
assigned to it, and an optional zone-scoped layout override. A layer is the addressable
unit: it has a stable ID, an optional name, a source (an effect with its controls and
control bindings, among others), blend mode, opacity, transform, and enabled flag.
That shape drives how the surfaces behave:
set_effectandhypercolor effects activatereplace the target zone's layer stack. Omitting a zone targets the primary zone, created if the scene has none.adjust_controlsneeds both a zone and a layer, because a zone can stack several effect layers.hypercolor effects patchandhypercolor effects resetresolve the zone and layer for you: they read the live scene, pick the primary zone (or the first zone), and take the topmost effect layer in it. When a zone stacks more than one effect, address the layer explicitly throughadjust_controlsor the REST route.hypercolor effects stopclears the whole live scene, not one zone.clear_zoneis the scoped version.- Display faces live in
display-role zones and are assigned by device, throughset_display_face.
Read the live document with hypercolor effects list -j for the catalog and
GET /api/v1/scene for the zone and layer tree, whose layer IDs are what the patch
routes expect.
Default workflow
Start with state discovery before changing anything:
hypercolor status
hypercolor effects list
hypercolor scenes activeWhen parsing output in automation, prefer JSON:
hypercolor status -j
hypercolor effects list -j
hypercolor library presets list -j--json and its -j shorthand are global flags, so they work on every subcommand.
Daemon control
Core runtime commands:
hypercolor status
hypercolor status --watch
hypercolor status --watch --interval 2
hypercolor effects list --search aurora
hypercolor effects info "Aurora"
hypercolor effects activate "Aurora" --param speed=6 --param palette=\"Ocean\"
hypercolor effects patch --param speed=8
hypercolor effects reset
hypercolor effects pause
hypercolor effects resume
hypercolor effects stop
hypercolor effects rescan
hypercolor brightness get
hypercolor brightness set 45
hypercolor scenes list
hypercolor scenes snapshot "Evening"
hypercolor scenes activate "Movie Night"
hypercolor scenes deactivate
hypercolor diagnose --systemhypercolor status --watch subscribes to the daemon's event WebSocket and re-renders on
state change, rate-limited to one render per --interval seconds (default 1). It is the right way to observe a
change landing; do not poll hypercolor status in a loop.
effects pause and effects resume patch global output power and are the
non-destructive alternative to effects stop. Pausing holds the active effect, its
controls, and the scene exactly as they are, so resuming needs no re-apply. effects stop clears the live scene and cannot be undone that way.
The nine effects subcommands are list, activate, stop, pause, resume,
info, patch, reset, and rescan. Spatial layout work is a separate top-level
command tree:
hypercolor layouts list
hypercolor layouts active
hypercolor layouts show "Desk"
hypercolor layouts preview "Desk"
hypercolor layouts apply "Desk"Beyond effects, scenes, and brightness, the CLI also carries devices, controls,
drivers, layouts, audio, access, library, server, servers, service,
config, diagnose, completions, and tui. Run hypercolor <command> --help before
guessing at a subcommand or flag.
OpenRGB fallback setup
Call openrgb_status or use the openrgb_setup prompt before recommending the
bridge. The CLI exposes hypercolor openrgb status, hints, partition,
start, stop, and resize <device> <zone> <size>. Native drivers retain
ownership until the user disables them. Ask for hub LED counts before resizing.
Partition, start, and stop verify the daemon instance on the local filesystem; a forwarded remote daemon cannot authorize local process control. Resize saves a single zone without overwriting concurrent updates to other zones. A saved change may await bridge reconciliation; do not report physical output as verified until it has been checked.
Host-input and screen-capture permissions
Screen-reactive and keyboard-reactive effects need OS-level authorization that the daemon cannot grant itself. When those inputs stay dark, request them explicitly rather than restarting anything:
hypercolor access authorize-input-monitoring
hypercolor access authorize-screen-recording
hypercolor access choose-screen-sourceThe three map onto POST /api/v1/input/authorize, POST /api/v1/capture/authorize, and
PUT /api/v1/capture/source. Nothing prompts for them implicitly at startup, so the
request has to be explicit. Each reports the grant owner that actually holds the
permission, which
is the fastest way to see that a grant landed on the app sidecar or a launchd service
rather than the daemon you are talking to. choose-screen-source presents the platform
picker, so it needs an owner that can show UI and returns the daemon's typed refusal when
that owner is headless.
Diagnostics workflow
When diagnosing a running daemon, query telemetry before asking for pasted logs:
just diagnose
just diagnose -- --json
hypercolor diagnose --system -j
curl -s -X POST http://127.0.0.1:9420/api/v1/diagnose \
-H 'content-type: application/json' -d '{"system":true}'Use just windows-diagnose only when Windows service/PawnIO/SMBus environment state matters; the daemon render/output telemetry itself is cross-platform.
Read these fields first for LED jank:
snapshot.render.latest_frame.output_frame_sourceiscurrent_frame,published_frame, orrouted_reusegpu_sample_stale,gpu_sample_deferred,gpu_sample_retry_hit,gpu_sample_queue_saturated,gpu_sample_wait_blockedsample_us,push_us,publish_us,devices_written,total_ledssnapshot.device_output.items[]:backend_id,fps_sent,fps_queued,frames_dropped,avg_queue_wait_ms,avg_write_ms,last_errorsnapshot.usb: USB actor display-lane wait counters
Interpretation:
- Smooth display previews with LED jank usually means LED sampling/output freshness, not effect rendering.
gpu_sample_stale=truewithoutput_frame_source=published_framemeans LEDs reused older LED data while the visual path may still be smooth.output_frame_source=current_framewithgpu_sample_retry_hit=true, low sample/push times, andwake_latewarnings usually means the app is ready but the OS woke the render thread late. On Windows, inspect active compiler/linker jobs before changing rendering or device code.fps_queuedabovefps_sent, risingframes_dropped, or high queue/write time points to device-output pressure.- Drops on queues capped below render FPS are normal latest-frame replacement when
fps_sentis near that queue's target and write latency/errors are clean. - Multiple USB devices janking together points upstream or shared queue pressure; one device with errors points at that driver/transport.
Effect authoring commands
The Bun authoring CLI has exactly four commands: build, validate, install, and
add. Anything else prints the help text and exits 1.
bunx hypercolor build --all
bunx hypercolor build --all --watch
bunx hypercolor build effects/aurora.ts --minify
bunx hypercolor validate dist/aurora.html
bunx hypercolor validate dist/aurora.html --strict --json
bunx hypercolor install dist/aurora.html
bunx hypercolor install dist/aurora.html --daemon
bunx hypercolor add ember --template canvasThere is no dev server and no watch-mode preview. build --all --watch is the iteration
loop: it rebuilds on every .ts or .glsl change under the entry roots and runs until
interrupted. Preview the result in the real daemon or app after installing it.
Scaffolded TypeScript workspaces expose the same flow through package scripts:
bun run build
bun run validate
bun run ship
bun run ship:daemonbun run ship copies validated artifacts into the user effects directory.
bun run ship:daemon uploads through POST /api/v1/effects/install. The plain-HTML
workspace template ships only validate, ship, and ship:daemon, since its effects
are hand-authored HTML with nothing to bundle.
Install workflow
Preferred install sequence for built HTML effects:
bunx hypercolor validate dist/aurora.html
bunx hypercolor install dist/aurora.html --daemon
hypercolor effects rescan
hypercolor effects activate "Aurora"If no daemon is running yet, local install still works:
bunx hypercolor install dist/aurora.htmlThat writes into $XDG_DATA_HOME/hypercolor/effects/user/, which the daemon
will pick up on boot or via hypercolor effects rescan.
Behavioral guidance
- Inspect first. Do not guess the active scene, its zones, or brightness.
- Reach for the MCP tools when they are connected, and the CLI when they are not or when the work is outside their eighteen.
- Prefer targeted actions over restarting the daemon.
- Prefer
pauseoverstop, andclear_zoneover a whole-scene clear, whenever the narrower action does the job. - Validate HTML artifacts before installing them.
- Use JSON output when another tool or script needs to consume the result.
- After installing a new effect, confirm it appears in
hypercolor effects listbefore applying it. - If a control patch fails, read the control definitions from
hypercolor effects info <name> -j(the table view omits them) and retry with values that match the declared types and the right wire form.
Example playbooks
List and activate an effect
hypercolor effects list -j
hypercolor effects activate "Aurora" --param speed=7
hypercolor statusTweak a running effect
hypercolor effects info "Aurora"
hypercolor effects patch --param speed=4 --param brightness=80
hypercolor statusTweak one layer in a multi-zone scene
Read the zone and layer IDs first, then patch the layer you actually mean.
curl -s http://127.0.0.1:9420/api/v1/scene | jq '.data.zones[] | {id, name, role, layers: [.layers[] | {id, name}]}'Then call adjust_controls with that zone and layer, or the equivalent REST route with
canonical tagged values.
Install a fresh artifact from an SDK workspace
cd /path/to/effect-workspace
bun run build
bunx hypercolor validate dist/aurora.html
bunx hypercolor install dist/aurora.html --daemon
hypercolor effects list --search auroraRecover after a manual file copy
cp dist/aurora.html ~/.local/share/hypercolor/effects/user/
hypercolor effects rescan
hypercolor effects info "Aurora"