Skip to content

Repository files navigation

Suffuse: controlled light spreading through space

Suffuse

An MCP server and Agent Skill for safe Govee control.

Suffuse gives AI agents two complementary control surfaces for Govee lights: a local MCP server with typed tools, and an Agent Skill that supplies the workflow for using those tools safely. Together they cover inspection, planning, control, evidence, and restoration.

Both surfaces keep credentials out of tool arguments, begin with read-only inspection, and require explicit confirmation before an agent can change a physical device. A deterministic CLI and the portable LightScore format are their shared infrastructure. Perceptual palettes and color-theory helpers are optional authoring tools, not the core product.

Suffuse uses Govee's documented Developer Platform. It is an unofficial community project and is not affiliated with or endorsed by Govee.

MCP + Agent Skill

Surface Role
MCP server Exposes twelve typed tools over local stdio and enforces the runtime write gates
Agent Skill Teaches Codex, Claude Code, and OpenClaw when to inspect, compile, confirm, apply, restore, and report incomplete evidence

What the Govee API key unlocks

A Govee Developer API key turns Suffuse from an offline authoring and test package into a live, account-aware lighting interface. It lets the local MCP server ask Govee which supported devices belong to the account, what each installed device can do, its current queryable state, and which scenes it can run.

Without a key With a key
Validate LightScore documents Discover the account's supported Govee devices
List bundled optional palettes Inspect capabilities reported by the installed unit and firmware
Compile and test against fixtures Read current queryable state and native or DIY scene catalogs
Inspect MCP metadata and install the Agent Skill Compile against live hardware, then apply or restore when separately authorized

The key authenticates Govee cloud access; it does not enable physical writes by itself. Suffuse still requires its server-level write switch, confirmation on the individual tool call, and a private restoration receipt.

Apply in Govee Home under Settings → Apply for API Key, then submit the requested information. Generating a replacement key invalidates older active keys for the same account, so update every service that uses the old key.

MCP tools

Tool What it does Default
govee_inspect_devices Reads installed devices and advertised capabilities, with household identifiers redacted Read-only
govee_list_scenes Reads native or DIY scene names for one exact device Read-only
govee_validate_score Validates lighting intent locally Read-only
govee_compile_score Discovers the selected device and returns a redacted execution plan Read-only
govee_apply_score Applies a compiled look and writes a private restore receipt Write-gated
govee_restore Restores the queryable prior state from a receipt Write-gated
govee_list_palettes Lists the optional bundled palettes without contacting Govee Read-only
govee_lan_scan Discovers Govee devices on the local network, keyless, and reports per-LED support Read-only
govee_lan_paint Paints a gradient or solid LightScore over the LAN, per-LED on verified fixtures Write-gated
govee_lan_release Ends a per-LED hold early, returning the device to its own effects Write-gated
govee_ble_scan Discovers Govee peripherals over Bluetooth LE — keyless and Wi-Fi-free Read-only
govee_ble_set One-shot power/colour/brightness over BLE with a declared restore point Write-gated

Inspect the MCP metadata without starting a connection:

node mcp/server.mjs --describe

Run the local stdio server:

GOVEE_API_KEY="your-private-key" node mcp/server.mjs

Configure an MCP client to run node with the absolute path to mcp/server.mjs, forwarding GOVEE_API_KEY from the client process environment. Do not put the key in checked-in MCP configuration. See the MCP reference for connection and deployment boundaries.

Install the Agent Skill

Preview the managed discovery links:

node scripts/manage.mjs plan --target all --scope user

Install links while keeping this checkout as the single source of truth:

node scripts/manage.mjs install --target all --scope user
node scripts/manage.mjs doctor

See cross-agent distribution for project-scoped installation and conservative removal.

Safety model

The normal MCP connection cannot change a light. Physical writes require both:

  • GOVEE_MCP_ALLOW_WRITES=1 in the server process; and
  • confirmWrite: true on the individual tool call.

Every write also requires an absolute private receipt path. Experimental segment operations require a third acknowledgement, confirmSegments: true, until that exact unit and firmware have visibly demonstrated independent segment control.

This separates five facts that device-control tools often blur together:

  1. the intent an agent authored;
  2. the capabilities an installed device advertised;
  3. the operations Suffuse compiled;
  4. the requests Govee accepted; and
  5. what the hardware visibly rendered.

An accepted API request is not treated as proof of visible output. Restoration is reported as partial when Govee exposes global state but not the previous per-segment arrangement.

Quick start

Suffuse requires Node.js 20 or newer.

git clone https://github.com/andyed/suffuse.git
cd suffuse
npm install
npm test
node mcp/server.mjs --describe

Store the Govee Developer API key outside the repository and expose it only through the process environment. Never paste it into a score, receipt, command argument, issue, or chat transcript.

Node 20+ can load a private environment file directly:

node --env-file=/private/path/govee.env mcp/server.mjs

LightScore and CLI

MCP tools exchange a provider-neutral LightScore rather than raw Govee commands. A score targets a device by SKU or name and describes power, brightness, color, a named scene, or an ordered gradient plus declared fallbacks.

{
  "version": 1,
  "name": "Dusk gradient",
  "target": {
    "sku": "H608B",
    "name": "String Downlights"
  },
  "power": "on",
  "look": {
    "kind": "gradient",
    "brightness": 45,
    "palette": "dusk",
    "fallbackScene": "Sunset"
  },
  "fallback": ["scene", "solid"],
  "restore": { "mode": "capture" }
}

The score contains no API key or stable device identifier. Compilation fails if the target is ambiguous or no advertised capability or declared fallback can safely express the request. See the LightScore reference for the schema.

The CLI is useful for diagnostics, fixtures, and direct inspection of the layer underneath MCP:

node scripts/govee.mjs inspect --sku H608B

node scripts/govee.mjs compile \
  --score tests/fixtures/dusk-gradient.score.json \
  --capabilities tests/fixtures/segmented-device.json

node scripts/govee.mjs apply --score /private/path/look.score.json

The last command remains a redacted preview because --confirm-write is absent. After a direct request to change the lights:

node scripts/govee.mjs apply \
  --score /private/path/look.score.json \
  --confirm-write \
  --receipt /private/path/look.receipt.json

node scripts/govee.mjs restore \
  --receipt /private/path/look.receipt.json \
  --confirm-write

LAN control — keyless, per-LED, bounded

Suffuse 0.5 adds a local-network transport beside the cloud path: the documented LAN API (scan, devStatus, colorwc) plus the undocumented per-LED razer channel that Razer Synapse, Govee Desktop, SignalRGB and LedFx use. The same contract applies — LightScore in, write gates on, private receipt out — with a different wire underneath. The LAN API's measured failure modes — colorwc queues unboundedly past ~10 Hz, devStatus returns stale replies while sending, multicast discovery is intermittent per device — are documented with methods in references/lan.md.

node scripts/govee.mjs lan scan
node scripts/govee.mjs lan paint --score dusk.score.json --confirm-write \
  --confirm-segments --receipt /private/receipts/dusk.json --hold-seconds 1800
node scripts/govee.mjs lan release --ip 192.168.1.34 --confirm-write

What makes this path different, and how Suffuse holds the line on each:

  • It is keyless. No API key protects the user's lights on the LAN, so every write sits behind the same --confirm-write gate as the cloud path, and per-LED writes additionally require --confirm-segments.
  • The per-LED channel is mute. No acknowledgment, no queryable segment state, no error surface — a malformed frame and a perfect one look identical from the socket. Suffuse therefore validates everything before the wire (encoder self-checks against independently published constants at every load; the length field is 16-bit big-endian, which every prose description of this protocol gets wrong), and reports verified: false on every per-LED result: it claims what was sent, never that the device obeyed.
  • The pixel count must match the fixture exactly. A wrong count is silently ignored and reads as an unsupported device, so only hardware-verified SKU profiles compile to per-LED (H608B: 15 pixels, chase-verified). Unknown SKUs refuse rather than guess.
  • Gradient bands are the only per-LED authoring surface — deliberately. Bands compile to pixels in authored order, so adjacent pixels stay adjacent in hue by construction. Fixtures with fewer zones than pixels interpolate between supplied stops the long way round the hue wheel; an arbitrary-array API would let an agent trip that without knowing. The schema makes it inexpressible.
  • Looks are bounded in time. The hardware reverts to its own effects ~60s after per-LED data stops (a dead-man, not a bug). --hold-seconds makes the duration explicit: a detached keepalive maintains the look, then the device hands itself back — default 15 minutes, hard cap 4 hours. An abandoned agent session cannot hold a room indefinitely.
  • Writers contend. A whole-device color command repaints every pixel and flattens a live per-LED look (measured; last writer wins). While a hold is active, Suffuse refuses whole-device writes to that device with a structured error instead of letting the two silently fight.
  • Restore still works, keyless. devStatus answers over LAN, so pre-write whole-device state is captured into the receipt before painting, and restore replays it. Prior per-LED looks are not queryable on any channel; the receipt says so rather than overclaiming.

This resolves the negative recorded below for the H608B: the cloud segmentedColorRgb path never rendered on it, but the same fixture paints 15 independent pixels over LAN. The advertised capability and the reachable one are different surfaces.

The razer channel is not documented by Govee or Razer — it is a reverse-engineered protocol with no stability promise, and Suffuse's claims about it are tagged by evidence class in references/lan-razer.md: what was verified on hardware, what is corroborated by independent implementations (OpenRGB, LedFx), and what remains a working guess.

For SKUs Suffuse has never touched, node scripts/portfolio.mjs <SKU> queries a mined portfolio of 371 SKUs — community priors harvested from four open-source integrations with per-fact provenance, anchored against this project's hardware measurements. One rule governs it: a mined positive is trustworthy, a mined negative is only "unknown" (the H612B renders 10 discrete pixels despite Govee's own database saying supportRazer: 0). See references/device-portfolio.md.

What Suffuse reaches — and what it does not

Wire Status What it gives you
Cloud Platform API Supported, needs a Govee Developer key Device/scene inspection, capability-aware compile, apply, restore — one-shot and rate-limited
LAN (colorwc + razer) Supported, keyless Whole-device color and per-LED painting on profiled fixtures; 260 of the 371 portfolio SKUs advertise LAN support
Bluetooth LE Supported, keyless, one-shot Power, colour, brightness on BLE-reachable lamps — the only local path to the H6009 class; needs the optional @stoprocent/noble dependency

Where the effort goes. Suffuse's centre of gravity is the keyless LAN path — per-LED razer painting against hardware-verified profiles — and the perceptual colour pipeline that decides what those pixels show: OKLCH recipes, conserved lightness, capability-aware compilation. That combination is what the authors run daily against a real room, and it is where this project is genuinely ahead. The cloud path works and is gated identically, but it is the least-exercised surface here; if something drifts, it will drift there first (#3 tracks verifying it end-to-end against live devices — reports welcome).

BLE control — keyless, Wi-Fi-free, one-shot

Some SKUs are BLE-only (six are flagged that way in the mined portfolio), and a larger class — the H6009 bulb is the reference example — is cloud-listed but has no LAN Control at all. BLE is the only keyless path to that hardware, and it needs no Wi-Fi, no multicast, no /24 sweep. The transport implements the hardware-measured contract in references/ble.md: full service discovery with a connect settle, the RGBWW 0x0D colour opcode (the widely documented 0x02 is accepted and visibly inert), a one-second settle after power-on (the lamp's controller boots and silently drops whatever arrives meanwhile), and a write drain before disconnect (tearing the link down immediately destroys the queued packet — measured).

node scripts/govee.mjs ble scan
node scripts/govee.mjs ble set --name H6009 --color '#ffb46e' --brightness 35 \
  --confirm-write --receipt /private/receipts/lamp.json

The boundaries, each a hardware fact rather than a policy choice:

  • One-shot only, on purpose. A BLE lamp accepts one central at a time; a held connection locks the owner's own Govee Home app out, and a connected peripheral stops advertising. Suffuse borrows the lamp for the seconds a write takes and hands it back. There are no BLE holds.
  • No state readback exists. The notify characteristic rejects reads on real hardware, so prior state cannot be captured. The receipt records a declared restore point (warm white) and says so honestly; restore returns the lamp to that declared neutral, not to its previous look.
  • Writes are unacknowledged. Results report what was sent (verified: false), never that the lamp obeyed — same honesty rule as the per-LED LAN channel.
  • The dependency is optional. @stoprocent/noble is a native module; if it is absent or cannot build, every other Suffuse surface keeps working and the ble commands fail with a structured remediation. On macOS, Bluetooth permission belongs to the responsible process (usually your terminal app) — an unsigned process touching CoreBluetooth is killed without a prompt, not asked. Dry-runs never touch the radio.
  • Opcode claims are per-SKU. The colour opcode is verified on the H6009 (RGBWW). Other SKUs are driven with the same frame on a stated assumption; a lamp that powers and dims but ignores colour is the signature of a wrong per-class opcode — file a compatibility report.

Prior art, for BLE control embedded in a larger system or per-model opcode research: homebridge-govee, chvolkmann/govee_btled, egold555/Govee-Reverse-Engineering, and wez/govee2mqtt's ptReal BLE-over-LAN tunnel.

Optional palette authoring

Suffuse can resolve named palettes, preserve exact authored 3-5-band colors, or compile constrained OKLCH recipes with gamut and adjacency diagnostics. These helpers make an agent's color choices reproducible; they are not required for device inspection, scene control, solid colors, apply, or restore.

node scripts/govee.mjs palettes

See palette authoring and color theory when a task actually needs custom color work.

Psychodeli+

Suffuse grew out of Room Lights for Psychodeli+, a live music visualizer that lets compatible Govee lights join the screen's evolving palette.

Platform Current availability
iPhone and iPad Available on the App Store
Android Closed test: join the psychodeli-testers Google Group, then opt in through Google Play
Desktop apps Closed beta
Web Product information at psychodeli.com

Suffuse is a separate developer tool, not the real-time Psychodeli+ rendering engine. High-rate music- or screen-reactive lighting belongs in a dedicated local runtime, not a cloud API request loop.

Genealogy

Two lineages meet here, and crediting both keeps the project honest about what it invented — which is neither the protocol nor the colour science, but the contract around them.

The per-LED channel originated as a proprietary integration: Govee firmware implementing a private protocol for Razer Synapse's Chroma ecosystem, later used by Govee's own Desktop app. Adam Honse reverse-engineered it for OpenRGB (GPL-2.0-or-later, December 2023) — the write-up where the byte layout first became public. LedFx (GPL-3.0) implemented it independently for audio-reactive rendering; SignalRGB's community Govee.js addons spread it through the ambient-lighting scene; and LumiSync (MIT) published its own research and a permissively licensed implementation. Suffuse's implementation was re-derived from the byte layout as protocol fact, hardware-verified, and adds what none of the roughly three dozen public implementations carry: typed tools, write gates, receipts, bounded holds, and evidence-classed claims. The full chain, with each claim tagged by its source, is references/lan-razer.md.

The control discipline comes from Psychodeli+'s Room Lights: a year of driving real fixtures over cloud, LAN, and BLE is where the hard lessons were measured — commands that queue rather than drop, devices that fail silently when a pixel count is wrong, writers that flatten each other's work, restores that cannot be captured on a mute channel. Suffuse is those lessons extracted, given a schema, and pointed at agents.

Evidence and boundaries

  • Compile against fresh device capabilities; never promise a feature from a SKU or marketing page alone.
  • The public API can activate advertised DIY scenes but does not document creating or uploading arbitrary DIY effects.
  • Suffuse does not scrape Govee Home or reproduce private authentication.
  • The current provider is Govee. LightScore keeps vendor resources out of authored intent so another provider can implement the same MCP contract.

One recorded H608B result is negative: the device advertised segmented color and accepted correctly chunked requests but did not visibly render independent colors. Its advertised Sunset Glow scene did render. See device evidence for the precise boundary.

Resolved for the LAN path (2026-08-15): the same fixture renders 15 independent pixels over the local razer channel — verified by a discrete chase, every pixel individually lit. The cloud negative stands; the capability was real and the advertised route to it was not.

Because the per-LED channel is mute, "verified" is a claim only a camera or an eye can make — so Suffuse ships the instrument: verify/ is a camera-in-the-loop harness that drives known patterns (sync clapper, per-fixture calibration, split, discrete chase, latency step) and scores a phone recording into PASS/WEAK/FAIL. It is how a contributor promotes a RAZER_PROFILES entry to verified: true on hardware the authors have never seen, and its analyzer self-tests against synthetic footage — including the negative cases — in CI.

Development

npm ci
npm test
npm run doctor
npm run mcp:describe
npm pack --dry-run

Contributions must preserve typed MCP contracts, capability-driven compilation, redacted output, explicit write gates, receipts for physical changes, and the distinction between API acceptance and visible evidence.

Before adding another Govee transport, check the ecosystem notes. Prefer an explicit adapter to silently forking or reimplementing mature work such as govee2mqtt.

The repository identity is documented in assets/brand. For sensitive reports, follow SECURITY.md.

License

MIT

About

MCP server and Agent Skill to inspect, control, and restore Govee lights safely.

Topics

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages