mixxx-mcp
README.md
# most LLMs are DJs
Boiler room but it's a datacenter evaporative cooling tower.
Fork of [cloudygetty-ai/mixxx-mcp](https://github.com/cloudygetty-ai/mixxx-mcp),
an MCP server that lets AI agents control [Mixxx](https://mixxx.org) over a
virtual MIDI port.
## How it works
```
Claude ──MCP──▶ python server ──▶ virtual MIDI port "mixxx-mcp"
│ CC messages
▼
Mixxx controller script (JS)
engine.setValue(...)
```
That's the **write path**, and it's the only path. Upstream also promised a
read path (the Mixxx JS posting state back out), but Mixxx's embedded JS
engine has no `XMLHttpRequest` and no sockets, so it can never work — the
state tools (`get_deck_state` etc.) always come back empty. The agent reads
**Mixxx's log file** instead (every command echoes a `[mixxx-mcp] SET ...`
line, and track loads show up as analyzer lines) plus **Mixxx's SQLite
library DB** for titles, BPMs, and durations. The log flushes minutes late,
so the agent mixes on wall-clock math and confirms after the fact. Flying
the decks on log-tail and SQL is half the fun.
## What's different from upstream
- `mixxx-mcp.js`: top-level `const` → `var` + explicit `globalThis` export
(Mixxx's QJSEngine can't see top-level const), and `setTimeout` →
`engine.beginTimer` (there is no setTimeout in Mixxx JS).
- `mixxx-mcp.midi.xml`: regenerated so all 128 CCs script-bind to
`MixxxMCP.handleCC` — the original bound control names directly, which
Mixxx rejects as an invalid mapping and silently disables.
- Track-loading controls added (CCs 117–123): library cursor next/prev and
load-to-deck. This is what lets the agent choose its own tracks.
- `main.py` logs to stderr (it was corrupting the MCP stdio transport).
- The HTTP state server (dead weight, see above) accepts
`MIXXX_MCP_STATE_PORT` so a second instance doesn't crash on a busy port.
- `.claude/skills/run-mixxx-mcp/`: an agent skill + `driver.py` harness —
bring-up checks, deploy, a log watcher, library queries, a smoke test, and
a standalone MCP client REPL for driving Mixxx with no registration at all.
- `scripts/energy.sh`: ffmpeg RMS energy profiler (~1.3 s per track) that
finds where a track's intro ends and its outro begins, so transitions land
while the track still has life in it, not at a fixed "70 seconds before
the end."
## Quickstart (macOS)
Tested on an Intel iMac with Mixxx 2.5.6 from the official DMG.
```bash
python3.12 -m venv .venv
.venv/bin/pip install mcp python-osc python-rtmidi
# register with Claude Code (user scope)
claude mcp add mixxx-mcp -s user -- $PWD/.venv/bin/python $PWD/main.py
# copy the controller mapping into Mixxx's (sandboxed) controllers dir
.venv/bin/python .claude/skills/run-mixxx-mcp/driver.py deploy
```
Bring-up order matters:
1. Start the MCP server first — it creates the MIDI port. (A Claude Code
session does this for you.)
2. Then start Mixxx.
3. Then — every time Mixxx **or** the server restarts — untick and re-tick
**Enabled** under Preferences ▸ Controllers ▸ mixxx-mcp. Mixxx never
reattaches on its own; commands sent to a stale port report success and
vanish into the void. This is the number-one gotcha.
4. Verify: `driver.py status`, send a control, then look for the SET line
with `driver.py log` (patience — the log buffers).
## Tools
| Tool | Description |
|------|-------------|
| `play(deck)` / `stop(deck)` / `cue(deck)` | Transport |
| `sync(deck)` | Enable sync (can only enable — disable via `send_control` `sync_enabled 0`) |
| `set_volume(deck, value)` | Channel fader 0.0–1.0 |
| `set_crossfader(value)` | Crossfader −1.0–1.0 |
| `set_eq(deck, low, mid, high)` | EQ bands 0.0–4.0 |
| `set_pregain(deck, value)` | Trim 0.0–4.0 |
| `set_rate(deck, value)` / `nudge_tempo(...)` | Pitch / tempo nudge |
| `set_loop` / `exit_loop` / `halve_loop` / `double_loop` | Beat loops |
| `set_hotcue` / `goto_hotcue` / `clear_hotcue` | Hotcues 1–8 |
| `beatjump(deck, beats)` | Jump ±N beats |
| `toggle_effect` / `set_effect_mix` | Effects |
| `send_control(group, key, value)` | Raw escape hatch — anything in the [Mixxx controls reference](https://manual.mixxx.org/latest/en/chapters/appendix/mixxx_controls.html) |
| `get_deck_state` / `get_mixer_state` / `get_all_state` | Always empty (see above) — read the log instead |
## Credits
Forked from [cloudygetty-ai/mixxx-mcp](https://github.com/cloudygetty-ai/mixxx-mcp)
[Mixxx](https://mixxx.org)
## License
All rights reserved. You must only use this in violation of my intellectual property rights.